dsh-workbuddy-xdpool 0.2.0 → 0.3.0

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,1294 +1,1335 @@
1
- import { createHash } from "node:crypto";
2
- import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
- import { homedir, platform } from "node:os";
4
- import { join, resolve } from "node:path";
5
- import z from "@deepseek-ai/schemastery";
6
- import "@earendil-works/pi-ai";
7
- import "@earendil-works/pi-ai/api/openai-completions.lazy";
8
- import "@deepseek-ai/dsh-llm";
9
- import "@deepseek-ai/dsh-llm-pi-ai";
10
- //#region src/accounts.ts
11
- /**
12
- * Account pool: discovers every WorkBuddy credential snapshot the desktop app
13
- * has left on this machine and hands out one healthy account per request,
14
- * rotating away from any account the upstream has rate-limited.
15
- *
16
- * Discovery is read-only: the desktop app's files are never written. Each
17
- * account is keyed by its billing identity (`uin`, falling back to `uid`), so
18
- * re-logging the same account refreshes in place instead of creating a duplicate.
19
- *
20
- * @module dsh-workbuddy-xdpool/accounts
21
- */
22
- /** Live auth file name the WorkBuddy desktop app writes. */
23
- const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
24
- /** Snapshot files left behind by previous logins share this prefix. */
25
- const SNAPSHOT_PREFIX = "workbuddy-desktop.";
26
- /** Env override for the auth file or its directory. */
27
- const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
28
- function nonEmptyEnv(value) {
29
- return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
30
- }
31
- /**
32
- * Platform-default directories holding the desktop app's auth files.
33
- * Windows probes Local before Roaming; a redirected profile still resolves
34
- * through the env location.
35
- */
36
- function defaultDesktopAuthDirs(platform = process.platform, home = homedir(), env = process.env) {
37
- if (platform === "darwin") return [join(home, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth")];
38
- if (platform === "win32") {
39
- const local = nonEmptyEnv(env["LOCALAPPDATA"]) ?? join(home, "AppData", "Local");
40
- const roaming = nonEmptyEnv(env["APPDATA"]) ?? join(home, "AppData", "Roaming");
41
- return [join(local, "CodeBuddyExtension", "Data", "Public", "auth"), join(roaming, "CodeBuddyExtension", "Data", "Public", "auth")];
42
- }
43
- if (platform === "linux") {
44
- const config = nonEmptyEnv(env["XDG_CONFIG_HOME"]) ?? join(home, ".config");
45
- return [join(config, "CodeBuddyExtension", "Data", "Public", "auth")];
46
- }
47
- return [];
48
- }
49
- /** Normalize an expiry that may arrive in seconds or milliseconds. */
50
- function expiryToMs(value) {
51
- if (value <= 0) return 0;
52
- return value > 0xe8d4a51000 ? value : value * 1e3;
53
- }
54
- function optionalString(value) {
55
- return typeof value === "string" && value !== "" ? value : void 0;
56
- }
57
- /**
58
- * Parse a WorkBuddy auth document. Accepts the nested desktop shape
59
- * `{"auth":{...},"account":{...}}` and the flat panel shape; returns undefined
60
- * when there is no usable access token.
61
- */
62
- function parseWorkBuddyAuth(text, sourcePath) {
63
- let parsed;
64
- try {
65
- parsed = JSON.parse(text);
66
- } catch {
67
- return;
68
- }
69
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
70
- const document = parsed;
71
- let auth;
72
- let identity;
73
- if (typeof document["auth"] === "object" && document["auth"] !== null) {
74
- auth = document["auth"];
75
- identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : {};
76
- } else {
77
- auth = document;
78
- identity = document;
79
- }
80
- const accessToken = typeof auth["accessToken"] === "string" ? auth["accessToken"] : "";
81
- if (accessToken === "") return void 0;
82
- const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
83
- if (refreshExpiresAtMs !== void 0 && refreshExpiresAtMs > 0 && refreshExpiresAtMs < Date.now()) return;
84
- return {
85
- accessToken,
86
- refreshToken: typeof auth["refreshToken"] === "string" ? auth["refreshToken"] : "",
87
- expiresAtMs: typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0,
88
- ...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
89
- ...optionalString(identity["nickname"]) === void 0 ? {} : { nickname: optionalString(identity["nickname"]) },
90
- ...optionalString(identity["uin"]) === void 0 ? {} : { uin: optionalString(identity["uin"]) },
91
- ...optionalString(identity["uid"]) === void 0 ? {} : { uid: optionalString(identity["uid"]) },
92
- ...optionalString(identity["enterpriseId"]) === void 0 ? {} : { enterpriseId: optionalString(identity["enterpriseId"]) },
93
- domain: typeof auth["domain"] === "string" ? auth["domain"] : "",
94
- sourcePath
95
- };
96
- }
97
- /**
98
- * Stable account id. `uin` is the billing identity the upstream keys on and
99
- * survives re-login; `uid` is the fallback.
100
- */
101
- function workbuddyAccountId(credential) {
102
- const stable = credential.uin ?? credential.uid ?? credential.nickname ?? "unknown";
103
- return createHash("sha256").update(`workbuddy\0${stable}`).digest("hex").slice(0, 16);
104
- }
105
- /** Human label; distinguishes same-nickname accounts by uid prefix. */
106
- function accountLabel(credential) {
107
- const name = credential.nickname ?? "WorkBuddy";
108
- const discriminator = (credential.uid ?? credential.uin ?? "").slice(0, 8);
109
- return discriminator === "" ? name : `${name}#${discriminator}`;
110
- }
111
- /** List the auth files in one directory: the live file plus every snapshot. */
112
- async function authFilesIn(dir) {
113
- let entries;
114
- try {
115
- entries = await readdir(dir);
116
- } catch {
117
- return [];
118
- }
119
- const files = entries.filter((name) => {
120
- if (name === "workbuddy-desktop.info") return true;
121
- return name.startsWith(SNAPSHOT_PREFIX) && name.endsWith(".info");
122
- });
123
- files.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
124
- return files.map((name) => join(dir, name));
125
- }
126
- async function readCredential(path) {
127
- try {
128
- return parseWorkBuddyAuth(await readFile(path, "utf8"), path);
129
- } catch {
130
- return;
131
- }
132
- }
133
- /** Every directory the pool should scan, in probe order. */
134
- function candidateAuthDirs(env = process.env) {
135
- const dirs = [];
136
- const override = nonEmptyEnv(env[WORKBUDDY_AUTH_FILE_ENV]);
137
- if (override !== void 0) dirs.push(override.toLowerCase().endsWith(".info") ? resolve(override, "..") : override);
138
- dirs.push(...defaultDesktopAuthDirs(process.env["DSH_TEST_PLATFORM"]));
139
- return dirs;
140
- }
141
- /**
142
- * Read-only pool of every discovered WorkBuddy account, with rate-limit
143
- * cooldown and round-robin failover.
144
- */
145
- var WorkBuddyAccountPool = class {
146
- logger;
147
- authDirs;
148
- cooldownMs;
149
- client;
150
- refreshMarginMs;
151
- accounts = [];
152
- cursor = 0;
153
- lastScanAtMs = 0;
154
- preferredId;
155
- refreshInflight = /* @__PURE__ */ new Map();
156
- constructor(options = {}) {
157
- this.logger = options.logger;
158
- this.authDirs = options.authDirs ?? candidateAuthDirs();
159
- this.cooldownMs = options.cooldownMs ?? 6e4;
160
- this.client = options.client;
161
- this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
162
- }
163
- /**
164
- * Re-apply configuration that only affects discovery and cooldown policy,
165
- * without rebuilding the pool. A later `scan()` uses the new auth dirs and
166
- * cooldown window; existing accounts keep their in-memory state.
167
- */
168
- applyConfig(options) {
169
- if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
170
- if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
171
- }
172
- /** Rescan the auth directories and merge newly discovered accounts. */
173
- async scan() {
174
- const found = [];
175
- for (const dir of this.authDirs) for (const file of await authFilesIn(dir)) {
176
- const credential = await readCredential(file);
177
- if (credential !== void 0) found.push(credential);
178
- }
179
- const byId = /* @__PURE__ */ new Map();
180
- for (const account of this.accounts) byId.set(account.id, account);
181
- for (const credential of found) {
182
- const id = workbuddyAccountId(credential);
183
- const existing = byId.get(id);
184
- if (existing === void 0) {
185
- byId.set(id, {
186
- id,
187
- label: accountLabel(credential),
188
- credential,
189
- cooldownUntilMs: 0,
190
- modelCooldowns: {},
191
- rateLimitHits: 0
192
- });
193
- continue;
194
- }
195
- if ((credential.expiresAtMs ?? 0) > (existing.credential.expiresAtMs ?? 0)) byId.set(id, {
196
- ...existing,
197
- credential,
198
- label: accountLabel(credential)
199
- });
200
- }
201
- this.accounts = [...byId.values()];
202
- this.lastScanAtMs = Date.now();
203
- return this.accounts;
204
- }
205
- /** All accounts, cooldown state included. */
206
- list() {
207
- return this.accounts;
208
- }
209
- /**
210
- * Accounts currently eligible to serve a request.
211
- *
212
- * With a `modelId`, an account is eligible when it is not account-wide cooled
213
- * AND that model is not cooling on it — so a 429 on `hy4-preview` only keeps
214
- * that model out while `hy3` on the same account stays usable. Without a
215
- * model id the legacy account-wide check applies (callers that cannot name a
216
- * model, e.g. CLI diagnostics).
217
- */
218
- available(now, modelId) {
219
- return this.accounts.filter((account) => {
220
- if (account.cooldownUntilMs > now) return false;
221
- if (modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now) return false;
222
- return true;
223
- });
224
- }
225
- /**
226
- * Pick the next usable account for an optional model. Scans on first use,
227
- * and rescans when every known account is cooling down — a fresh desktop
228
- * login is the usual way out of an exhausted pool. A preferred
229
- * (user-selected) account that is healthy is tried first; otherwise the
230
- * cursor round-robins so consecutive requests spread across accounts and a
231
- * still-cooling preferred account is skipped.
232
- */
233
- async acquire(modelId) {
234
- if (this.accounts.length === 0) await this.scan();
235
- let pool = this.available(Date.now(), modelId);
236
- if (pool.length === 0) {
237
- await this.scan();
238
- pool = this.available(Date.now(), modelId);
239
- }
240
- if (pool.length === 0) return void 0;
241
- let start = this.cursor % pool.length;
242
- if (this.preferredId !== void 0) {
243
- const preferredIndex = pool.findIndex((account) => account.id === this.preferredId);
244
- if (preferredIndex !== -1) start = preferredIndex;
245
- }
246
- for (let step = 0; step < pool.length; step += 1) {
247
- const account = pool[(start + step) % pool.length];
248
- if (account === void 0) continue;
249
- await this.ensureFresh(account);
250
- this.cursor = (start + step + 1) % pool.length;
251
- return account;
252
- }
253
- return pool[0];
254
- }
255
- /** Pin the account the plugin card should prefer; tokens stay out of settings. */
256
- prefer(accountId) {
257
- this.preferredId = accountId;
258
- }
259
- /** Best-effort refresh of one account after a session-dead upstream answer. */
260
- async refreshAccount(accountId) {
261
- const account = this.accounts.find((item) => item.id === accountId);
262
- if (account === void 0) return;
263
- await this.ensureFresh(account);
264
- }
265
- /**
266
- * Refresh the account's access token when it is within the margin (or already
267
- * expired), in-flight de-duped per account. A failed refresh keeps the
268
- * existing token when it has not yet expired, so an unreachable refresh
269
- * endpoint never takes down a working session.
270
- */
271
- async ensureFresh(account) {
272
- if (this.client === void 0) return;
273
- const credential = account.credential;
274
- if (!(credential.expiresAtMs <= 0 || credential.expiresAtMs <= Date.now() + this.refreshMarginMs)) return;
275
- const existing = this.refreshInflight.get(account.id);
276
- if (existing !== void 0) {
277
- await existing;
278
- return;
279
- }
280
- const run = (async () => {
281
- if (credential.refreshToken === "") {
282
- if (credential.expiresAtMs > Date.now() + 3e4) return;
283
- this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} token expired with no refresh token; sign in again`);
284
- return;
285
- }
286
- try {
287
- const outcome = await this.client.refreshToken(credential);
288
- account.credential = {
289
- ...credential,
290
- accessToken: outcome.accessToken,
291
- ...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
292
- expiresAtMs: outcome.expiresInSec !== void 0 ? Date.now() + outcome.expiresInSec * 1e3 : credential.expiresAtMs,
293
- ...outcome.domain === void 0 || outcome.domain === "" ? {} : { domain: outcome.domain }
294
- };
295
- this.logger?.info?.(`dsh-workbuddy-xdpool: refreshed token for ${account.label}`);
296
- } catch (error) {
297
- if (credential.expiresAtMs > Date.now() + 3e4) this.logger?.warn?.(`dsh-workbuddy-xdpool: token refresh failed but token still valid for ${account.label}`, error);
298
- else this.logger?.error?.(`dsh-workbuddy-xdpool: token refresh failed and token expired for ${account.label}`, error);
299
- }
300
- })();
301
- this.refreshInflight.set(account.id, run);
302
- try {
303
- await run;
304
- } finally {
305
- this.refreshInflight.delete(account.id);
306
- }
307
- }
308
- /**
309
- * Mark an account (or one of its models) rate-limited.
310
- *
311
- * With `modelId`, only that model on the account is cooled — the account's
312
- * other models stay in rotation, matching the upstream's per-model rate
313
- * limit ("可切换其他模型继续使用"). Without a model id the whole account is
314
- * cooled, which callers should reserve for limits that truly span every model.
315
- */
316
- penalize(accountId, resetAtMs, modelId) {
317
- const account = this.accounts.find((item) => item.id === accountId);
318
- if (account === void 0) return;
319
- account.rateLimitHits += 1;
320
- const until = resetAtMs ?? Date.now() + this.cooldownMs;
321
- if (modelId !== void 0 && modelId !== "") {
322
- account.modelCooldowns[modelId] = Math.max(account.modelCooldowns[modelId] ?? 0, until);
323
- this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} rate-limited on model ${modelId}; cooling that model until ${new Date(until).toISOString()}`);
324
- return;
325
- }
326
- account.cooldownUntilMs = Math.max(account.cooldownUntilMs, until);
327
- this.logger?.warn(`dsh-workbuddy-xdpool: account ${account.label} rate-limited; cooling until ${new Date(until).toISOString()}`);
328
- }
329
- /** Clear all cooldowns (account-wide and per-model), e.g. from a reset command. */
330
- resetCooldowns() {
331
- for (const account of this.accounts) {
332
- account.cooldownUntilMs = 0;
333
- account.modelCooldowns = {};
334
- account.rateLimitHits = 0;
335
- }
336
- }
337
- /** Diagnostics snapshot. Account-wide cooling count (per-model cooling excluded:
338
- * the account as a whole stays usable when only one model is limited). */
339
- status() {
340
- const now = Date.now();
341
- return {
342
- count: this.accounts.length,
343
- cooling: this.accounts.filter((account) => account.cooldownUntilMs > now).length,
344
- lastScanAtMs: this.lastScanAtMs
345
- };
346
- }
347
- };
348
- //#endregion
349
- //#region src/catalog.ts
350
- /** Static fallback used before the first live catalog fetch. */
351
- const FALLBACK_WORKBUDDY_MODELS = [
352
- {
353
- id: "glm-5.3",
354
- name: "GLM-5.3",
355
- contextWindow: 2e5,
356
- maxOutputTokens: 128e3,
357
- supportsImages: true
358
- },
359
- {
360
- id: "glm-5.3-flash",
361
- name: "GLM-5.3-Flash",
362
- contextWindow: 2e5,
363
- maxOutputTokens: 128e3,
364
- supportsImages: true
365
- },
366
- {
367
- id: "glm-5.2",
368
- name: "GLM-5.2",
369
- contextWindow: 2e5,
370
- maxOutputTokens: 128e3,
371
- supportsImages: true
372
- },
373
- {
374
- id: "glm-5.1",
375
- name: "GLM-5.1",
376
- contextWindow: 2e5,
377
- maxOutputTokens: 128e3,
378
- supportsImages: false
379
- },
380
- {
381
- id: "deepseek-v4-pro",
382
- name: "DeepSeek-V4-Pro",
383
- contextWindow: 2e5,
384
- maxOutputTokens: 128e3,
385
- supportsImages: true
386
- },
387
- {
388
- id: "deepseek-v4-flash",
389
- name: "DeepSeek-V4-Flash",
390
- contextWindow: 2e5,
391
- maxOutputTokens: 128e3,
392
- supportsImages: true
393
- },
394
- {
395
- id: "kimi-k3",
396
- name: "Kimi-K3",
397
- contextWindow: 2e5,
398
- maxOutputTokens: 128e3,
399
- supportsImages: true
400
- },
401
- {
402
- id: "minimax-m3",
403
- name: "MiniMax-M3",
404
- contextWindow: 2e5,
405
- maxOutputTokens: 128e3,
406
- supportsImages: true
407
- },
408
- {
409
- id: "hy3",
410
- name: "Hy3",
411
- contextWindow: 2e5,
412
- maxOutputTokens: 128e3,
413
- supportsImages: true
414
- },
415
- {
416
- id: "hy4-preview",
417
- name: "Hy4-Preview",
418
- contextWindow: 1e6,
419
- maxOutputTokens: 128e3,
420
- supportsImages: true
421
- }
422
- ];
423
- /** Live catalog with a static fallback behind it. */
424
- var WorkBuddyCatalog = class {
425
- models = FALLBACK_WORKBUDDY_MODELS;
426
- listeners = /* @__PURE__ */ new Set();
427
- current() {
428
- return this.models;
429
- }
430
- /** Replace the catalog and notify the adapter to rebuild its model list. */
431
- update(models) {
432
- if (models.length === 0) return;
433
- this.models = models;
434
- for (const listener of this.listeners) listener();
435
- }
436
- /** Restore the static fallback, e.g. when the upstream stops answering. */
437
- reset() {
438
- this.models = FALLBACK_WORKBUDDY_MODELS;
439
- for (const listener of this.listeners) listener();
440
- }
441
- onChange(listener) {
442
- this.listeners.add(listener);
443
- return () => this.listeners.delete(listener);
444
- }
445
- find(id) {
446
- return this.models.find((model) => model.id === id);
447
- }
448
- /** Replace the catalog from the live upstream list; keeps the fallback if empty. */
449
- updateFromUpstream(models) {
450
- this.update(catalogFromUpstream(models));
451
- }
452
- };
453
- /** Convert one upstream catalog entry into the plugin's model-info shape. */
454
- function toModelInfo(model) {
455
- return {
456
- id: model.id,
457
- name: model.name,
458
- contextWindow: model.contextWindow,
459
- maxOutputTokens: model.maxTokens,
460
- supportsImages: model.multimodal ?? false,
461
- ...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
462
- ...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
463
- };
464
- }
465
- /** Map the live upstream list, falling back to the static list when empty. */
466
- function catalogFromUpstream(models) {
467
- if (models.length === 0) return FALLBACK_WORKBUDDY_MODELS;
468
- return models.map(toModelInfo);
469
- }
470
- //#endregion
471
- //#region src/upstream.ts
472
- /** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
473
- const CN_CHAT_BASE = "https://copilot.tencent.com";
474
- /** Billing/console base — `www.codebuddy.cn` is the billing origin. */
475
- const CN_BILLING_BASE = "https://www.codebuddy.cn";
476
- /** Global base for `workbuddy.ai` logins. */
477
- const GLOBAL_BASE = "https://www.workbuddy.ai";
478
- /** Client UA the desktop CLI uses. */
479
- const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
480
- const JSON_TIMEOUT_MS = 3e4;
481
- const ERROR_BODY_LIMIT = 4096;
482
- /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
483
- const HARD_CREDIT_MARKERS = [
484
- "insufficient credit",
485
- "no credit",
486
- "credit exhausted",
487
- "out of credit",
488
- "quota exceeded",
489
- "quota exhaust",
490
- "payment required",
491
- "credit not enough",
492
- "not enough credit",
493
- "积分不足",
494
- "额度不足",
495
- "余额不足",
496
- "积分用完",
497
- "额度用尽",
498
- "没有积分"
499
- ];
500
- /** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
501
- const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
502
- /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
503
- function regionOf(domain) {
504
- const lowered = domain.trim().toLowerCase();
505
- if (lowered === "workbuddy.ai" || lowered.endsWith(".workbuddy.ai")) return "global";
506
- return "cn";
507
- }
508
- function chatBase(credential) {
509
- return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_CHAT_BASE;
510
- }
511
- function billingBase(credential) {
512
- return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
513
- }
514
- function originReferer(credential) {
515
- return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
516
- }
517
- /** Headers every upstream request shares. */
518
- function commonHeaders(credential) {
519
- return {
520
- "Accept": "application/json, text/plain, */*",
521
- "X-Requested-With": "XMLHttpRequest",
522
- "Origin": originReferer(credential),
523
- "Referer": `${originReferer(credential)}/`,
524
- "User-Agent": CLIENT_UA
525
- };
526
- }
527
- /** Chat request headers, including the X-No-* conventions the official CLI uses. */
528
- function chatHeaders(credential) {
529
- return {
530
- ...commonHeaders(credential),
531
- "Content-Type": "application/json",
532
- "Authorization": `Bearer ${credential.accessToken}`,
533
- ...credential.uid === "" || credential.uid === void 0 ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
534
- ...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
535
- ...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
536
- "X-Product": "SaaS"
537
- };
538
- }
539
- /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
540
- function refreshHeaders(credential) {
541
- const headers = {
542
- ...commonHeaders(credential),
543
- "X-Refresh-Token": credential.refreshToken,
544
- "X-Auth-Refresh-Source": "workbuddy"
545
- };
546
- if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") headers["X-Enterprise-Id"] = credential.enterpriseId;
547
- return headers;
548
- }
549
- /** Billing request headers. */
550
- function billingHeaders(credential) {
551
- const headers = {
552
- "Authorization": `Bearer ${credential.accessToken}`,
553
- "Accept": "application/json",
554
- "Content-Type": "application/json"
555
- };
556
- if (credential.uid !== "" && credential.uid !== void 0) headers["X-User-Id"] = credential.uid;
557
- if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") {
558
- headers["X-Enterprise-Id"] = credential.enterpriseId;
559
- headers["X-Tenant-Id"] = credential.enterpriseId;
560
- }
561
- if (credential.domain !== "") headers["X-Domain"] = credential.domain;
562
- return headers;
563
- }
564
- async function readEnvelope(response) {
565
- const text = await response.text();
566
- let parsed;
567
- try {
568
- parsed = JSON.parse(text);
569
- } catch {
570
- throw new Error(`workbuddy upstream returned non-JSON (http ${response.status}): ${text.slice(0, 160)}`);
571
- }
572
- if (typeof parsed !== "object" || parsed === null) throw new Error(`workbuddy upstream returned an unexpected document (http ${response.status})`);
573
- const document = parsed;
574
- return {
575
- code: typeof document["code"] === "number" ? document["code"] : 0,
576
- msg: typeof document["msg"] === "string" ? document["msg"] : "",
577
- data: "data" in document ? document["data"] : void 0
578
- };
579
- }
580
- /** Fail an envelope whose business code is non-zero, classified like HTTP errors. */
581
- function envelopeError(status, envelope) {
582
- const kind = classifyUpstreamError(status, envelope.msg);
583
- return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
584
- }
585
- /**
586
- * Classify an upstream failure from its HTTP status and body excerpt.
587
- * Body markers win over status, because the upstream reuses 400/200 for
588
- * several distinct conditions.
589
- */
590
- function classifyUpstreamError(status, body) {
591
- if (status === 402) return "hard_credit";
592
- const lower = body.toLowerCase();
593
- for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
594
- for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
595
- if (status === 429) return "soft_rate";
596
- if (body.includes("soft_rate") || body.includes("\"code\":6004") || body.includes("频率限制")) return "soft_rate";
597
- if (status === 404) return "not_found";
598
- if (status >= 500) return "server";
599
- return "client";
600
- }
601
- /** Parse the upstream's `credits` string into a multiplier. */
602
- function parseCreditMultiplier(value) {
603
- if (typeof value !== "string") return void 0;
604
- const match = /x\s*([0-9]*\.?[0-9]+)/iu.exec(value);
605
- if (match === null) return void 0;
606
- const parsed = Number(match[1]);
607
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
608
- }
609
- /** Parse the upstream's `reasoning` object; unknown shapes degrade to `{}`. */
610
- function parseReasoning(value) {
611
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
612
- const raw = value;
613
- const supportedEfforts = Array.isArray(raw["supportedEfforts"]) ? raw["supportedEfforts"].filter((effort) => typeof effort === "string") : void 0;
614
- const defaultEffort = typeof raw["defaultEffort"] === "string" ? raw["defaultEffort"] : void 0;
615
- const canDisableThinking = typeof raw["canDisableThinking"] === "boolean" ? raw["canDisableThinking"] : void 0;
616
- if (supportedEfforts === void 0 && defaultEffort === void 0 && canDisableThinking === void 0) return;
617
- return {
618
- ...supportedEfforts === void 0 || supportedEfforts.length === 0 ? {} : { supportedEfforts },
619
- ...defaultEffort === void 0 ? {} : { defaultEffort },
620
- ...canDisableThinking === void 0 ? {} : { canDisableThinking }
621
- };
622
- }
623
- /** Parse one catalog entry; entries without usable token limits are dropped. */
624
- function parseUpstreamModel(value) {
625
- if (typeof value !== "object" || value === null) return void 0;
626
- const raw = value;
627
- const id = typeof raw["id"] === "string" ? raw["id"] : "";
628
- if (id === "" || raw["disabled"] === true) return void 0;
629
- const input = typeof raw["maxInputTokens"] === "number" ? raw["maxInputTokens"] : 0;
630
- const output = typeof raw["maxOutputTokens"] === "number" ? raw["maxOutputTokens"] : 0;
631
- if (input <= 0 || output <= 0) return void 0;
632
- const name = typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : id;
633
- const descriptionZh = typeof raw["descriptionZh"] === "string" && raw["descriptionZh"] !== "" ? raw["descriptionZh"] : void 0;
634
- const descriptionEn = typeof raw["descriptionEn"] === "string" && raw["descriptionEn"] !== "" ? raw["descriptionEn"] : void 0;
635
- const creditMultiplier = parseCreditMultiplier(raw["credits"]);
636
- const reasoning = parseReasoning(raw["reasoning"]);
637
- const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
638
- return {
639
- id,
640
- name,
641
- contextWindow: input,
642
- maxTokens: output,
643
- ...creditMultiplier === void 0 ? {} : { creditMultiplier },
644
- ...reasoning === void 0 ? {} : { reasoning },
645
- ...descriptionZh === void 0 ? {} : { descriptionZh },
646
- ...descriptionEn === void 0 ? {} : { descriptionEn },
647
- ...supportsToolCall === void 0 ? {} : { supportsToolCall }
648
- };
649
- }
650
- var WorkBuddyUpstreamClient = class {
651
- fetchImpl;
652
- clientVersion;
653
- constructor(options = {}) {
654
- this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
655
- this.clientVersion = options.clientVersion ?? "2.0.4";
656
- }
657
- /**
658
- * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
659
- * force `stream: true` (the upstream rejects non-streaming), convert the
660
- * DSH `developer` role into `system` (upstream rejects `developer` with
661
- * business code 11128), and flatten `tool_choice` into its string form.
662
- */
663
- prepareChatBody(raw) {
664
- let body;
665
- try {
666
- body = JSON.parse(raw);
667
- } catch {
668
- return raw;
669
- }
670
- if (typeof body !== "object" || body === null || Array.isArray(body)) return raw;
671
- const obj = body;
672
- obj["stream"] = true;
673
- delete obj["stream_options"];
674
- if (Array.isArray(obj["messages"])) for (const value of obj["messages"]) {
675
- if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
676
- const message = value;
677
- if (message["role"] === "developer") message["role"] = "system";
678
- }
679
- const choice = obj["tool_choice"];
680
- if (typeof choice === "string") {
681
- if (choice.trim().toLowerCase() === "none") {
682
- delete obj["tool_choice"];
683
- delete obj["tools"];
684
- delete obj["functions"];
685
- }
686
- } else if (typeof choice === "object" && choice !== null && !Array.isArray(choice)) {
687
- const wrapped = choice;
688
- const type = typeof wrapped["type"] === "string" ? wrapped["type"].trim().toLowerCase() : "";
689
- if (type === "none") {
690
- delete obj["tool_choice"];
691
- delete obj["tools"];
692
- delete obj["functions"];
693
- } else if (type === "auto" || type === "required") obj["tool_choice"] = type;
694
- else if (type === "function") {
695
- const fn = typeof wrapped["function"] === "object" && wrapped["function"] !== null ? wrapped["function"] : void 0;
696
- let name = typeof fn?.["name"] === "string" ? fn["name"] : "";
697
- if (name === "" && typeof wrapped["name"] === "string") name = wrapped["name"];
698
- obj["tool_choice"] = name.trim() !== "" ? name.trim() : "auto";
699
- } else delete obj["tool_choice"];
700
- }
701
- return JSON.stringify(obj);
702
- }
703
- /** Forward one chat completion. Never throws for upstream failures. */
704
- async chatStream(credential, prepared, signal) {
705
- let response;
706
- try {
707
- response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
708
- method: "POST",
709
- headers: chatHeaders(credential),
710
- body: prepared,
711
- ...signal === void 0 ? {} : { signal }
712
- });
713
- } catch (error) {
714
- return {
715
- ok: false,
716
- kind: "server",
717
- status: 0,
718
- message: `transport error: ${String(error)}`
719
- };
720
- }
721
- if (response.ok) return {
722
- ok: true,
723
- response
724
- };
725
- const text = (await response.text().catch(() => "")).slice(0, ERROR_BODY_LIMIT);
726
- return {
727
- ok: false,
728
- kind: classifyUpstreamError(response.status, text),
729
- status: response.status,
730
- message: text
731
- };
732
- }
733
- /** POST the token-refresh endpoint; the caller merges the outcome. */
734
- async refreshToken(credential) {
735
- const response = await this.fetchImpl(`${chatBase(credential)}/v2/plugin/auth/token/refresh`, {
736
- method: "POST",
737
- headers: refreshHeaders(credential),
738
- signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
739
- });
740
- const envelope = await readEnvelope(response);
741
- if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
742
- const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
743
- const accessToken = typeof data["accessToken"] === "string" ? data["accessToken"] : "";
744
- if (accessToken === "") throw new Error("workbuddy token refresh returned no accessToken; sign in again in the WorkBuddy app");
745
- const outcome = { accessToken };
746
- if (typeof data["refreshToken"] === "string" && data["refreshToken"] !== "") outcome.refreshToken = data["refreshToken"];
747
- if (typeof data["expiresIn"] === "number" && data["expiresIn"] > 0) outcome.expiresInSec = data["expiresIn"];
748
- if (typeof data["domain"] === "string" && data["domain"] !== "") outcome.domain = data["domain"];
749
- return outcome;
750
- }
751
- /** GET the personal model catalog, keeping the `cli` agent's models only. */
752
- async fetchModels(credential, signal) {
753
- const response = await this.fetchImpl(`${chatBase(credential)}/console/enterprises/personal/models`, {
754
- headers: {
755
- "Authorization": `Bearer ${credential.accessToken}`,
756
- "Accept": "application/json",
757
- "Origin": originReferer(credential),
758
- "Referer": `${originReferer(credential)}/`,
759
- "User-Agent": CLIENT_UA
760
- },
761
- ...signal === void 0 ? {} : { signal }
762
- });
763
- const envelope = await readEnvelope(response);
764
- if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
765
- const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
766
- const rawModels = Array.isArray(data["models"]) ? data["models"] : [];
767
- const agents = Array.isArray(data["agents"]) ? data["agents"] : [];
768
- let cliIds;
769
- for (const agent of agents) if (typeof agent === "object" && agent !== null) {
770
- const wrapped = agent;
771
- if (wrapped["name"] === "cli" && Array.isArray(wrapped["models"])) {
772
- cliIds = wrapped["models"].filter((id) => typeof id === "string");
773
- break;
774
- }
775
- }
776
- const byId = /* @__PURE__ */ new Map();
777
- for (const model of rawModels) {
778
- const parsed = parseUpstreamModel(model);
779
- if (parsed !== void 0) byId.set(parsed.id, parsed);
780
- }
781
- const models = (cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()]).map((id) => byId.get(id)).filter((model) => model !== void 0);
782
- if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
783
- return models;
784
- }
785
- /** Read-only credits query, aggregated by package. Does not consume credits. */
786
- async fetchCredits(credential) {
787
- const now = /* @__PURE__ */ new Date();
788
- const fmt = (date) => {
789
- const p = (n) => n.toString().padStart(2, "0");
790
- return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
791
- };
792
- const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/get-user-resource`, {
793
- method: "POST",
794
- headers: billingHeaders(credential),
795
- body: JSON.stringify({
796
- PageNumber: 1,
797
- PageSize: 100,
798
- ProductCode: "p_tcaca",
799
- Status: [0, 3],
800
- PackageEndTimeRangeBegin: fmt(now),
801
- PackageEndTimeRangeEnd: fmt(new Date(now.getTime() + 3185136e6))
802
- }),
803
- signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
804
- });
805
- const envelope = await readEnvelope(response);
806
- if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
807
- const wrapper = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
808
- const data = typeof wrapper["Response"] === "object" && wrapper["Response"] !== null ? wrapper["Response"] : {};
809
- const inner = typeof data["Data"] === "object" && data["Data"] !== null ? data["Data"] : {};
810
- const rawAccounts = Array.isArray(inner["Accounts"]) ? inner["Accounts"] : [];
811
- let total = 0;
812
- let nearestExpiryMs;
813
- let expiringSoon = 0;
814
- const SOON_MS = 2592e5;
815
- const parseDate = (raw) => {
816
- if (typeof raw === "number" && raw > 0xe8d4a51000) return raw;
817
- if (typeof raw === "string" && raw !== "") {
818
- const parsed = Date.parse(raw);
819
- if (!Number.isNaN(parsed)) return parsed;
820
- }
821
- };
822
- const packages = [];
823
- for (const raw of rawAccounts) {
824
- if (typeof raw !== "object" || raw === null) continue;
825
- const account = raw;
826
- const num = (key) => typeof account[key] === "number" ? account[key] : 0;
827
- const monthly = num("CapacityType") === 4;
828
- const size = monthly ? num("CycleCapacitySize") : num("CapacitySize");
829
- const remain = monthly ? num("CycleCapacityRemain") : num("CapacityRemain");
830
- const capped = remain < 0 ? 0 : remain;
831
- const cycleEndMs = parseDate(account["CycleEndTime"]);
832
- const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
833
- const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
834
- if (!monthly && (capped <= 0 || expiresAtMs !== void 0 && expiresAtMs <= Date.now())) continue;
835
- total += capped;
836
- if (expiresAtMs !== void 0) {
837
- if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
838
- if (expiresAtMs - Date.now() <= SOON_MS) expiringSoon += capped;
839
- }
840
- packages.push({
841
- packageName: typeof account["PackageName"] === "string" ? account["PackageName"] : "(unnamed)",
842
- remain: capped,
843
- size,
844
- monthly,
845
- ...refreshAtMs === void 0 ? {} : { refreshAtMs },
846
- ...expiresAtMs === void 0 ? {} : { expiresAtMs }
847
- });
848
- }
849
- return {
850
- total,
851
- packages,
852
- expiringSoon,
853
- ...nearestExpiryMs === void 0 ? {} : { nearestExpiryMs }
854
- };
855
- }
856
- /** Query today's check-in status without changing account state. */
857
- async fetchCheckinStatus(credential) {
858
- const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/checkin-activity-status`, {
859
- method: "POST",
860
- headers: billingHeaders(credential),
861
- body: "{}",
862
- signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
863
- });
864
- const envelope = await readEnvelope(response);
865
- if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
866
- const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
867
- const num = (key) => typeof data[key] === "number" ? data[key] : 0;
868
- return {
869
- active: data["active"] === true,
870
- todayCheckedIn: data["today_checked_in"] === true,
871
- streakDays: num("streak_days"),
872
- dailyCredit: num("daily_credit"),
873
- todayCredit: num("today_credit"),
874
- isStreakDay: data["is_streak_day"] === true,
875
- nextStreakDay: num("next_streak_day"),
876
- streakBonusDays: num("streak_bonus_days"),
877
- streakBonusCredit: num("streak_bonus_credit"),
878
- ...typeof data["claim_button_text"] === "string" && data["claim_button_text"] !== "" ? { claimButtonText: data["claim_button_text"] } : {}
879
- };
880
- }
881
- /** Claim today's check-in reward. The browser route guards this mutation. */
882
- async claimDailyCheckin(credential) {
883
- const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/daily-checkin`, {
884
- method: "POST",
885
- headers: billingHeaders(credential),
886
- body: "{}",
887
- signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
888
- });
889
- const envelope = await readEnvelope(response);
890
- if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
891
- const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
892
- const numberField = (key) => typeof data[key] === "number" ? data[key] : 0;
893
- return {
894
- credit: numberField("credit"),
895
- streakDays: numberField("streak_days"),
896
- isStreakDay: data["is_streak_day"] === true
897
- };
898
- }
899
- /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
900
- async credits(credential) {
901
- try {
902
- return {
903
- ok: true,
904
- data: await this.fetchCredits(credential)
905
- };
906
- } catch (error) {
907
- return {
908
- ok: false,
909
- message: String(error)
910
- };
911
- }
912
- }
913
- };
914
- //#endregion
915
- //#region src/status.ts
916
- /** Format the per-model credit multipliers. */
917
- function formatRates(status) {
918
- const lines = ["Model credit multipliers:"];
919
- for (const model of status.models) {
920
- const rate = model.multiplier === void 0 ? "x?" : `x${model.multiplier.toFixed(2)}`;
921
- lines.push(` ${model.name.padEnd(20)} ${rate}`);
922
- }
923
- return lines.join("\n");
924
- }
925
- z.object({
926
- authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)"),
927
- cooldownMs: z.number().step(1).min(1e3).description("Rate-limit cooldown per account, in milliseconds")
928
- });
929
- /** Assemble the runtime objects without registering anything. */
930
- function createCore(logger) {
931
- const client = new WorkBuddyUpstreamClient();
932
- return {
933
- pool: new WorkBuddyAccountPool({
934
- ...logger === void 0 ? {} : { logger },
935
- client
936
- }),
937
- catalog: new WorkBuddyCatalog(),
938
- client
939
- };
940
- }
941
- //#endregion
942
- //#region src/bin.ts
943
- /**
944
- * Command-line diagnostics and account management.
945
- *
946
- * dsh plugin --profile desktop exec dsh-workbuddy-xdpool <command>
947
- *
948
- * @module dsh-workbuddy-xdpool/bin
949
- */
950
- /** Directory holding imported account snapshots. */
951
- const ACCOUNT_DIR_NAME = ".workbuddy-xdpool";
952
- /** Snapshot files are named by the md5 prefix of their key, so any key is safe. */
953
- function snapshotPath(key, dir) {
954
- return join(dir, `${createHash("md5").update(key).digest("hex").slice(0, 8)}.json`);
955
- }
956
- function dshHome() {
957
- const fromEnv = process.env["DSH_HOME"];
958
- if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv.trim();
959
- return join(homedir(), ".dsh");
960
- }
961
- function accountDir() {
962
- return join(dshHome(), ACCOUNT_DIR_NAME);
963
- }
964
- function usage() {
965
- return [
966
- "dsh-workbuddy-xdpool — multi-account WorkBuddy provider for DeepSeek Harness",
967
- "",
968
- "Usage: dsh-workbuddy-xdpool <command> [options]",
969
- "",
970
- "Commands:",
971
- " status Account pool, credits, and shim state (add --credits, --json, --rates)",
972
- " doctor Diagnose discovery, cooldowns, and upstream reachability",
973
- " accounts List discovered accounts (add --json)",
974
- " import <key> Snapshot the current desktop login as <key> (add --force)",
975
- " remove <key> Delete one imported snapshot",
976
- " login Guide for adding another account (desktop app is single-sign-in)",
977
- " checkin [all|<acct>] Daily check-in: report status, or collect with `all` / a label",
978
- " reset Clear all rate-limit cooldowns immediately",
979
- "",
980
- "Options:",
981
- " --json Machine-readable output",
982
- " --credits Query remaining credits (read-only; does not consume)",
983
- " --rates Show per-model credit multipliers",
984
- " --force Overwrite an existing snapshot"
985
- ].join("\n");
986
- }
987
- /** Live auth files, in probe order. */
988
- function liveCandidates() {
989
- const fromEnv = process.env["WORKBUDDY_AUTH_FILE"];
990
- if (typeof fromEnv === "string" && fromEnv.trim() !== "") return [resolve(fromEnv.trim())];
991
- return defaultDesktopAuthDirs().map((dir) => join(dir, WORKBUDDY_LIVE_FILENAME));
992
- }
993
- async function readJsonFile(path) {
994
- try {
995
- return JSON.parse(await readFile(path, "utf8"));
996
- } catch {
997
- return;
998
- }
999
- }
1000
- async function commandStatus(args) {
1001
- const asJson = args.includes("--json");
1002
- const withCredits = args.includes("--credits");
1003
- const withRates = args.includes("--rates");
1004
- const core = createCore();
1005
- const accounts = await core.pool.scan();
1006
- const status = {
1007
- ok: accounts.length > 0,
1008
- accounts: accounts.map((account) => {
1009
- const now = Date.now();
1010
- const modelCooling = Object.entries(account.modelCooldowns).filter(([, until]) => until > now).map(([modelId, until]) => ({
1011
- modelId,
1012
- until: new Date(until).toISOString()
1013
- }));
1014
- return {
1015
- id: account.id,
1016
- label: account.label,
1017
- domain: account.credential.domain,
1018
- ...account.credential.expiresAtMs === 0 ? {} : { expiresAt: new Date(account.credential.expiresAtMs).toISOString() },
1019
- cooling: account.cooldownUntilMs > now,
1020
- ...modelCooling.length === 0 ? {} : { modelCooldowns: modelCooling },
1021
- rateLimitHits: account.rateLimitHits,
1022
- sourcePath: account.credential.sourcePath
1023
- };
1024
- }),
1025
- cooling: accounts.filter((account) => account.cooldownUntilMs > Date.now()).length,
1026
- models: core.catalog.current().map((model) => ({
1027
- id: model.id,
1028
- name: model.name,
1029
- multiplier: model.multiplier
1030
- })),
1031
- shim: { running: false }
1032
- };
1033
- if (asJson) console.log(JSON.stringify(status, null, 2));
1034
- else console.log(`WorkBuddy XD Pool: ${status.accounts.length} account(s), ${status.cooling} cooling\n` + status.accounts.map((account) => {
1035
- const flag = account.cooling ? "⏸ " : "▶ ";
1036
- const hits = account.rateLimitHits > 0 ? ` (hits ${account.rateLimitHits})` : "";
1037
- const modelCooling = (account.modelCooldowns ?? []).map((mc) => `\n model-cool: ${mc.modelId} until ${mc.until}`).join("");
1038
- return `${flag}${account.label} [${account.domain || "cn"}]${hits}\n ${account.sourcePath}${modelCooling}`;
1039
- }).join("\n"));
1040
- if (withRates) console.log(`\n${formatRates(status)}`);
1041
- if (withCredits) {
1042
- const first = accounts.find((account) => account.cooldownUntilMs <= Date.now());
1043
- if (first === void 0) console.log("\nCredits: skipped (every account is cooling down)");
1044
- else try {
1045
- const credits = await core.client.fetchCredits(first.credential);
1046
- console.log(`\nCredits for ${first.label}: ` + JSON.stringify(credits));
1047
- } catch (error) {
1048
- console.log(`\nCredits for ${first.label}: query failed — ${String(error).slice(0, 200)}`);
1049
- }
1050
- }
1051
- return status.ok ? 0 : 1;
1052
- }
1053
- /**
1054
- * Daily check-in from the terminal.
1055
- *
1056
- * `checkin` with no argument reports every account's status; `checkin all`
1057
- * collects wherever a reward is still available, and `checkin <label>` targets
1058
- * one account. A collection is never attempted twice for the same account on
1059
- * the same day — the status is re-read first and an already-collected account
1060
- * is reported as such.
1061
- */
1062
- async function commandCheckin(args) {
1063
- const asJson = args.includes("--json");
1064
- const target = args.find((arg) => !arg.startsWith("--"));
1065
- const core = createCore();
1066
- if ((await core.pool.scan()).length === 0) {
1067
- console.error("No WorkBuddy account discovered. Sign in with the WorkBuddy desktop app first.");
1068
- return 1;
1069
- }
1070
- const wanted = target === void 0 || target === "all" ? core.pool.list() : core.pool.list().filter((account) => account.label === target || account.id === target || account.id.startsWith(target));
1071
- if (wanted.length === 0) {
1072
- console.error(`No account matches "${target}". Run \`accounts\` to list labels.`);
1073
- return 1;
1074
- }
1075
- const rows = [];
1076
- for (const account of wanted) {
1077
- const row = {
1078
- label: account.label,
1079
- active: false,
1080
- alreadyCheckedIn: false,
1081
- streakDays: 0
1082
- };
1083
- try {
1084
- const status = await core.client.fetchCheckinStatus(account.credential);
1085
- row.active = status.active;
1086
- row.alreadyCheckedIn = status.todayCheckedIn;
1087
- row.streakDays = status.streakDays;
1088
- if (target !== void 0 && status.active && !status.todayCheckedIn) {
1089
- const claim = await core.client.claimDailyCheckin(account.credential);
1090
- row.claimed = claim.credit;
1091
- row.streakDays = claim.streakDays;
1092
- row.alreadyCheckedIn = true;
1093
- }
1094
- } catch (error) {
1095
- row.error = String(error).slice(0, 200);
1096
- }
1097
- rows.push(row);
1098
- }
1099
- if (asJson) {
1100
- console.log(JSON.stringify(rows, null, 2));
1101
- return rows.every((row) => row.error === void 0) ? 0 : 1;
1102
- }
1103
- console.log("Daily check-in:");
1104
- for (const row of rows) {
1105
- if (row.error !== void 0) {
1106
- console.log(` ! ${row.label}: query failed — ${row.error}`);
1107
- continue;
1108
- }
1109
- if (!row.active) {
1110
- console.log(` – ${row.label}: no check-in activity`);
1111
- continue;
1112
- }
1113
- if (row.claimed !== void 0) {
1114
- console.log(` + ${row.label}: collected ${row.claimed} credit(s) (streak ${row.streakDays})`);
1115
- continue;
1116
- }
1117
- console.log(row.alreadyCheckedIn ? ` ✓ ${row.label}: already checked in today (streak ${row.streakDays})` : ` · ${row.label}: not checked in today — run \`checkin all\` to collect`);
1118
- }
1119
- if (target === void 0) console.log("\nAdd `all` (or an account label) to collect.");
1120
- return rows.every((row) => row.error === void 0) ? 0 : 1;
1121
- }
1122
- async function commandDoctor() {
1123
- const lines = [];
1124
- let healthy = true;
1125
- lines.push(`dsh-workbuddy-xdpool doctor`);
1126
- lines.push(` platform : ${platform()}`);
1127
- lines.push(` dsh home : ${dshHome()}`);
1128
- lines.push("");
1129
- lines.push("Credential discovery:");
1130
- for (const candidate of liveCandidates()) {
1131
- const raw = await readJsonFile(candidate);
1132
- const credential = raw === void 0 ? void 0 : parseWorkBuddyAuth(JSON.stringify(raw), candidate);
1133
- lines.push(` ${credential === void 0 ? "✗" : "✓"} ${candidate}`);
1134
- }
1135
- const dirs = defaultDesktopAuthDirs();
1136
- for (const dir of dirs) {
1137
- const { readdir } = await import("node:fs/promises");
1138
- let names = [];
1139
- try {
1140
- names = await readdir(dir);
1141
- } catch {
1142
- lines.push(` - ${dir} (absent)`);
1143
- continue;
1144
- }
1145
- const snapshots = names.filter((name) => name.startsWith("workbuddy-desktop.") && name.endsWith(".info"));
1146
- lines.push(` ✓ ${dir} → ${snapshots.length} snapshot(s)`);
1147
- }
1148
- lines.push("");
1149
- const accounts = await createCore().pool.scan();
1150
- lines.push(`Accounts discovered: ${accounts.length}`);
1151
- if (accounts.length === 0) {
1152
- healthy = false;
1153
- lines.push(" ✗ none — sign in on the WorkBuddy desktop app, then run `import <key>`");
1154
- }
1155
- for (const account of accounts) {
1156
- const state = account.cooldownUntilMs > Date.now() ? "cooling" : "ready";
1157
- lines.push(` ✓ ${account.label} (${state}, hits ${account.rateLimitHits})`);
1158
- }
1159
- lines.push("");
1160
- lines.push(`Imported snapshots: ${accountDir()}`);
1161
- const { readdir: readdir2 } = await import("node:fs/promises");
1162
- let imported = [];
1163
- try {
1164
- imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
1165
- } catch {}
1166
- lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
1167
- console.log(lines.join("\n"));
1168
- return healthy ? 0 : 1;
1169
- }
1170
- async function commandImport(args) {
1171
- const key = args.filter((arg) => !arg.startsWith("--"))[0];
1172
- if (key === void 0) {
1173
- console.error("usage: dsh-workbuddy-xdpool import <key> [--force]");
1174
- return 2;
1175
- }
1176
- const force = args.includes("--force");
1177
- let source;
1178
- for (const candidate of liveCandidates()) {
1179
- const raw = await readJsonFile(candidate);
1180
- if (raw === void 0) continue;
1181
- if (parseWorkBuddyAuth(JSON.stringify(raw), candidate) !== void 0) {
1182
- source = candidate;
1183
- break;
1184
- }
1185
- }
1186
- if (source === void 0) {
1187
- console.error(`No signed-in WorkBuddy desktop session found. Sign in on the desktop app first
1188
- (looked in: ${liveCandidates().join(", ")})`);
1189
- return 1;
1190
- }
1191
- const dir = accountDir();
1192
- await mkdir(dir, { recursive: true });
1193
- const target = snapshotPath(key, dir);
1194
- const { access } = await import("node:fs/promises");
1195
- let exists = false;
1196
- try {
1197
- await access(target);
1198
- exists = true;
1199
- } catch {}
1200
- if (exists && !force) {
1201
- console.error(`Snapshot "${key}" already exists. Re-run with --force to overwrite.`);
1202
- return 1;
1203
- }
1204
- await copyFile(source, target);
1205
- const credential = parseWorkBuddyAuth(await readFile(target, "utf8"), target);
1206
- const label = credential === void 0 ? "unknown" : (credential.nickname ?? "WorkBuddy") + `#${workbuddyAccountId(credential).slice(0, 8)}`;
1207
- console.log(`Imported "${key}" → ${label}\n saved: ${target}`);
1208
- return 0;
1209
- }
1210
- async function commandRemove(args) {
1211
- const key = args.filter((arg) => !arg.startsWith("--"))[0];
1212
- if (key === void 0) {
1213
- console.error("usage: dsh-workbuddy-xdpool remove <key>");
1214
- return 2;
1215
- }
1216
- const target = snapshotPath(key, accountDir());
1217
- const { unlink } = await import("node:fs/promises");
1218
- try {
1219
- await unlink(target);
1220
- console.log(`Removed snapshot "${key}"`);
1221
- return 0;
1222
- } catch {
1223
- console.error(`No snapshot named "${key}"`);
1224
- return 1;
1225
- }
1226
- }
1227
- function commandLogin() {
1228
- console.log([
1229
- "Adding another WorkBuddy account",
1230
- "",
1231
- "The WorkBuddy desktop app holds one signed-in account at a time, so each",
1232
- "additional account is captured as a snapshot after you switch login:",
1233
- "",
1234
- " 1. Open the WorkBuddy desktop app and sign in (scan the QR code).",
1235
- " 2. Run: dsh plugin --profile desktop exec dsh-workbuddy-xdpool import <key>",
1236
- " 3. Sign in with the next account in the desktop app.",
1237
- " 4. Run the import command again with a different <key>.",
1238
- " 5. Restart DSH Desktop; every imported account joins the rotation pool.",
1239
- "",
1240
- "Snapshots live in " + accountDir() + " and store the desktop app's tokens",
1241
- "verbatim — protect that directory like a password."
1242
- ].join("\n"));
1243
- return 0;
1244
- }
1245
- async function commandReset() {
1246
- const core = createCore();
1247
- await core.pool.scan();
1248
- core.pool.resetCooldowns();
1249
- console.log("Cleared all rate-limit cooldowns.");
1250
- return 0;
1251
- }
1252
- async function commandAccounts(args) {
1253
- const asJson = args.includes("--json");
1254
- const accounts = await createCore().pool.scan();
1255
- if (asJson) console.log(JSON.stringify(accounts.map((account) => ({
1256
- id: account.id,
1257
- label: account.label,
1258
- cooling: account.cooldownUntilMs > Date.now(),
1259
- rateLimitHits: account.rateLimitHits
1260
- })), null, 2));
1261
- else if (accounts.length === 0) console.log("No imported accounts. Run `import <key>` after signing in on the desktop app.");
1262
- else console.log(accounts.map((account) => `${account.cooldownUntilMs > Date.now() ? "⏸" : "▶"} ${account.label}`).join("\n"));
1263
- return 0;
1264
- }
1265
- /** Entry point; returns the process exit code. */
1266
- async function main(argv) {
1267
- const [command, ...rest] = argv;
1268
- switch (command) {
1269
- case void 0:
1270
- case "help":
1271
- case "--help":
1272
- case "-h":
1273
- console.log(usage());
1274
- return 0;
1275
- case "status": return commandStatus(rest);
1276
- case "doctor": return commandDoctor();
1277
- case "accounts": return commandAccounts(rest);
1278
- case "import": return commandImport(rest);
1279
- case "remove": return commandRemove(rest);
1280
- case "login": return commandLogin();
1281
- case "logout": return commandRemove(["default", ...rest]);
1282
- case "reset": return commandReset();
1283
- case "checkin": return commandCheckin(rest);
1284
- default:
1285
- console.error(`unknown command: ${command}\n\n${usage()}`);
1286
- return 2;
1287
- }
1288
- }
1289
- main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
1290
- console.error(error);
1291
- process.exit(1);
1292
- });
1293
- //#endregion
1294
- export { main, writeFile };
1
+ import { createHash } from "node:crypto";
2
+ import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { homedir, platform } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import "@earendil-works/pi-ai";
7
+ import "@earendil-works/pi-ai/api/openai-completions.lazy";
8
+ import "@deepseek-ai/dsh-llm";
9
+ import "@deepseek-ai/dsh-llm-pi-ai";
10
+ //#region src/upstream.ts
11
+ /** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
12
+ const CN_CHAT_BASE = "https://copilot.tencent.com";
13
+ /** Billing/console base — `www.codebuddy.cn` is the billing origin. */
14
+ const CN_BILLING_BASE = "https://www.codebuddy.cn";
15
+ /** Global base for `workbuddy.ai` logins. */
16
+ const GLOBAL_BASE = "https://www.workbuddy.ai";
17
+ /** Client UA the desktop CLI uses. */
18
+ const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
19
+ const JSON_TIMEOUT_MS = 3e4;
20
+ const ERROR_BODY_LIMIT = 4096;
21
+ /** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
22
+ const HARD_CREDIT_MARKERS = [
23
+ "insufficient credit",
24
+ "no credit",
25
+ "credit exhausted",
26
+ "out of credit",
27
+ "quota exceeded",
28
+ "quota exhaust",
29
+ "payment required",
30
+ "credit not enough",
31
+ "not enough credit",
32
+ "积分不足",
33
+ "额度不足",
34
+ "余额不足",
35
+ "积分用完",
36
+ "额度用尽",
37
+ "没有积分"
38
+ ];
39
+ /** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
40
+ const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
41
+ function regionOf(domain) {
42
+ const lowered = domain.trim().toLowerCase();
43
+ if (lowered.endsWith(".workbuddy.ai") || lowered.endsWith(".workbuddy.cc")) return "global";
44
+ if (lowered === "workbuddy.ai" || lowered === "workbuddy.cc") return "global";
45
+ return "cn";
46
+ }
47
+ function chatBase(credential) {
48
+ return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_CHAT_BASE;
49
+ }
50
+ function billingBase(credential) {
51
+ return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
52
+ }
53
+ function originReferer(credential) {
54
+ return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
55
+ }
56
+ /** Headers every upstream request shares. */
57
+ function commonHeaders(credential) {
58
+ return {
59
+ "Accept": "application/json, text/plain, */*",
60
+ "X-Requested-With": "XMLHttpRequest",
61
+ "Origin": originReferer(credential),
62
+ "Referer": `${originReferer(credential)}/`,
63
+ "User-Agent": CLIENT_UA
64
+ };
65
+ }
66
+ /** Chat request headers, including the X-No-* conventions the official CLI uses. */
67
+ function chatHeaders(credential) {
68
+ return {
69
+ ...commonHeaders(credential),
70
+ "Content-Type": "application/json",
71
+ "Authorization": `Bearer ${credential.accessToken}`,
72
+ ...credential.uid === "" || credential.uid === void 0 ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
73
+ ...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
74
+ ...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
75
+ "X-Product": "SaaS"
76
+ };
77
+ }
78
+ /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
79
+ function refreshHeaders(credential) {
80
+ const headers = {
81
+ ...commonHeaders(credential),
82
+ "X-Refresh-Token": credential.refreshToken,
83
+ "X-Auth-Refresh-Source": "workbuddy"
84
+ };
85
+ if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") headers["X-Enterprise-Id"] = credential.enterpriseId;
86
+ return headers;
87
+ }
88
+ /** Billing request headers. */
89
+ function billingHeaders(credential) {
90
+ const headers = {
91
+ "Authorization": `Bearer ${credential.accessToken}`,
92
+ "Accept": "application/json",
93
+ "Content-Type": "application/json"
94
+ };
95
+ if (credential.uid !== "" && credential.uid !== void 0) headers["X-User-Id"] = credential.uid;
96
+ if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") {
97
+ headers["X-Enterprise-Id"] = credential.enterpriseId;
98
+ headers["X-Tenant-Id"] = credential.enterpriseId;
99
+ }
100
+ if (credential.domain !== "") headers["X-Domain"] = credential.domain;
101
+ return headers;
102
+ }
103
+ async function readEnvelope(response) {
104
+ const text = await response.text();
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(text);
108
+ } catch {
109
+ throw new Error(`workbuddy upstream returned non-JSON (http ${response.status}): ${text.slice(0, 160)}`);
110
+ }
111
+ if (typeof parsed !== "object" || parsed === null) throw new Error(`workbuddy upstream returned an unexpected document (http ${response.status})`);
112
+ const document = parsed;
113
+ return {
114
+ code: typeof document["code"] === "number" ? document["code"] : 0,
115
+ msg: typeof document["msg"] === "string" ? document["msg"] : "",
116
+ data: "data" in document ? document["data"] : void 0
117
+ };
118
+ }
119
+ /** Fail an envelope whose business code is non-zero, classified like HTTP errors. */
120
+ function envelopeError(status, envelope) {
121
+ const kind = classifyUpstreamError(status, envelope.msg);
122
+ return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
123
+ }
124
+ /**
125
+ * Classify an upstream failure from its HTTP status and body excerpt.
126
+ * Body markers win over status, because the upstream reuses 400/200 for
127
+ * several distinct conditions.
128
+ */
129
+ function classifyUpstreamError(status, body) {
130
+ if (status === 402) return "hard_credit";
131
+ const lower = body.toLowerCase();
132
+ for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
133
+ for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
134
+ if (status === 429) return "soft_rate";
135
+ if (body.includes("soft_rate") || body.includes("\"code\":6004") || body.includes("频率限制")) return "soft_rate";
136
+ if (status === 404) return "not_found";
137
+ if (status >= 500) return "server";
138
+ return "client";
139
+ }
140
+ /** Parse the upstream's `credits` string into a multiplier. */
141
+ function parseCreditMultiplier(value) {
142
+ if (typeof value !== "string") return void 0;
143
+ const match = /x\s*([0-9]*\.?[0-9]+)/iu.exec(value);
144
+ if (match === null) return void 0;
145
+ const parsed = Number(match[1]);
146
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
147
+ }
148
+ /** Parse the upstream's `reasoning` object; unknown shapes degrade to `{}`. */
149
+ function parseReasoning(value) {
150
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
151
+ const raw = value;
152
+ const supportedEfforts = Array.isArray(raw["supportedEfforts"]) ? raw["supportedEfforts"].filter((effort) => typeof effort === "string") : void 0;
153
+ const defaultEffort = typeof raw["defaultEffort"] === "string" ? raw["defaultEffort"] : void 0;
154
+ const canDisableThinking = typeof raw["canDisableThinking"] === "boolean" ? raw["canDisableThinking"] : void 0;
155
+ if (supportedEfforts === void 0 && defaultEffort === void 0 && canDisableThinking === void 0) return;
156
+ return {
157
+ ...supportedEfforts === void 0 || supportedEfforts.length === 0 ? {} : { supportedEfforts },
158
+ ...defaultEffort === void 0 ? {} : { defaultEffort },
159
+ ...canDisableThinking === void 0 ? {} : { canDisableThinking }
160
+ };
161
+ }
162
+ /** Parse one catalog entry; entries without usable token limits are dropped. */
163
+ function parseUpstreamModel(value) {
164
+ if (typeof value !== "object" || value === null) return void 0;
165
+ const raw = value;
166
+ const id = typeof raw["id"] === "string" ? raw["id"] : "";
167
+ if (id === "" || raw["disabled"] === true) return void 0;
168
+ const input = typeof raw["maxInputTokens"] === "number" ? raw["maxInputTokens"] : 0;
169
+ const output = typeof raw["maxOutputTokens"] === "number" ? raw["maxOutputTokens"] : 0;
170
+ if (input <= 0 || output <= 0) return void 0;
171
+ const name = typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : id;
172
+ const descriptionZh = typeof raw["descriptionZh"] === "string" && raw["descriptionZh"] !== "" ? raw["descriptionZh"] : void 0;
173
+ const descriptionEn = typeof raw["descriptionEn"] === "string" && raw["descriptionEn"] !== "" ? raw["descriptionEn"] : void 0;
174
+ const creditMultiplier = parseCreditMultiplier(raw["credits"]);
175
+ const reasoning = parseReasoning(raw["reasoning"]);
176
+ const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
177
+ return {
178
+ id,
179
+ name,
180
+ contextWindow: input,
181
+ maxTokens: output,
182
+ ...creditMultiplier === void 0 ? {} : { creditMultiplier },
183
+ ...reasoning === void 0 ? {} : { reasoning },
184
+ ...descriptionZh === void 0 ? {} : { descriptionZh },
185
+ ...descriptionEn === void 0 ? {} : { descriptionEn },
186
+ ...supportsToolCall === void 0 ? {} : { supportsToolCall }
187
+ };
188
+ }
189
+ var WorkBuddyUpstreamClient = class {
190
+ fetchImpl;
191
+ clientVersion;
192
+ constructor(options = {}) {
193
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
194
+ this.clientVersion = options.clientVersion ?? "2.0.4";
195
+ }
196
+ /**
197
+ * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
198
+ * force `stream: true` (the upstream rejects non-streaming), convert the
199
+ * DSH `developer` role into `system` (upstream rejects `developer` with
200
+ * business code 11128), and flatten `tool_choice` into its string form.
201
+ */
202
+ prepareChatBody(raw) {
203
+ let body;
204
+ try {
205
+ body = JSON.parse(raw);
206
+ } catch {
207
+ return raw;
208
+ }
209
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return raw;
210
+ const obj = body;
211
+ obj["stream"] = true;
212
+ delete obj["stream_options"];
213
+ if (Array.isArray(obj["messages"])) for (const value of obj["messages"]) {
214
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
215
+ const message = value;
216
+ if (message["role"] === "developer") message["role"] = "system";
217
+ }
218
+ const choice = obj["tool_choice"];
219
+ if (typeof choice === "string") {
220
+ if (choice.trim().toLowerCase() === "none") {
221
+ delete obj["tool_choice"];
222
+ delete obj["tools"];
223
+ delete obj["functions"];
224
+ }
225
+ } else if (typeof choice === "object" && choice !== null && !Array.isArray(choice)) {
226
+ const wrapped = choice;
227
+ const type = typeof wrapped["type"] === "string" ? wrapped["type"].trim().toLowerCase() : "";
228
+ if (type === "none") {
229
+ delete obj["tool_choice"];
230
+ delete obj["tools"];
231
+ delete obj["functions"];
232
+ } else if (type === "auto" || type === "required") obj["tool_choice"] = type;
233
+ else if (type === "function") {
234
+ const fn = typeof wrapped["function"] === "object" && wrapped["function"] !== null ? wrapped["function"] : void 0;
235
+ let name = typeof fn?.["name"] === "string" ? fn["name"] : "";
236
+ if (name === "" && typeof wrapped["name"] === "string") name = wrapped["name"];
237
+ obj["tool_choice"] = name.trim() !== "" ? name.trim() : "auto";
238
+ } else delete obj["tool_choice"];
239
+ }
240
+ return JSON.stringify(obj);
241
+ }
242
+ /** Forward one chat completion. Never throws for upstream failures. */
243
+ async chatStream(credential, prepared, signal) {
244
+ let response;
245
+ try {
246
+ response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
247
+ method: "POST",
248
+ headers: chatHeaders(credential),
249
+ body: prepared,
250
+ ...signal === void 0 ? {} : { signal }
251
+ });
252
+ } catch (error) {
253
+ return {
254
+ ok: false,
255
+ kind: "server",
256
+ status: 0,
257
+ message: `transport error: ${String(error)}`
258
+ };
259
+ }
260
+ if (response.ok) return {
261
+ ok: true,
262
+ response
263
+ };
264
+ const text = (await response.text().catch(() => "")).slice(0, ERROR_BODY_LIMIT);
265
+ return {
266
+ ok: false,
267
+ kind: classifyUpstreamError(response.status, text),
268
+ status: response.status,
269
+ message: text
270
+ };
271
+ }
272
+ /** POST the token-refresh endpoint; the caller merges the outcome. */
273
+ async refreshToken(credential) {
274
+ const response = await this.fetchImpl(`${chatBase(credential)}/v2/plugin/auth/token/refresh`, {
275
+ method: "POST",
276
+ headers: refreshHeaders(credential),
277
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
278
+ });
279
+ const envelope = await readEnvelope(response);
280
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
281
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
282
+ const accessToken = typeof data["accessToken"] === "string" ? data["accessToken"] : "";
283
+ if (accessToken === "") throw new Error("workbuddy token refresh returned no accessToken; sign in again in the WorkBuddy app");
284
+ const outcome = { accessToken };
285
+ if (typeof data["refreshToken"] === "string" && data["refreshToken"] !== "") outcome.refreshToken = data["refreshToken"];
286
+ if (typeof data["expiresIn"] === "number" && data["expiresIn"] > 0) outcome.expiresInSec = data["expiresIn"];
287
+ if (typeof data["domain"] === "string" && data["domain"] !== "") outcome.domain = data["domain"];
288
+ return outcome;
289
+ }
290
+ /** GET the personal model catalog, keeping the `cli` agent's models only. */
291
+ async fetchModels(credential, signal) {
292
+ const response = await this.fetchImpl(`${chatBase(credential)}/console/enterprises/personal/models`, {
293
+ headers: {
294
+ "Authorization": `Bearer ${credential.accessToken}`,
295
+ "Accept": "application/json",
296
+ "Origin": originReferer(credential),
297
+ "Referer": `${originReferer(credential)}/`,
298
+ "User-Agent": CLIENT_UA
299
+ },
300
+ ...signal === void 0 ? {} : { signal }
301
+ });
302
+ const envelope = await readEnvelope(response);
303
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
304
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
305
+ const rawModels = Array.isArray(data["models"]) ? data["models"] : [];
306
+ const agents = Array.isArray(data["agents"]) ? data["agents"] : [];
307
+ let cliIds;
308
+ for (const agent of agents) if (typeof agent === "object" && agent !== null) {
309
+ const wrapped = agent;
310
+ if (wrapped["name"] === "cli" && Array.isArray(wrapped["models"])) {
311
+ cliIds = wrapped["models"].filter((id) => typeof id === "string");
312
+ break;
313
+ }
314
+ }
315
+ const byId = /* @__PURE__ */ new Map();
316
+ for (const model of rawModels) {
317
+ const parsed = parseUpstreamModel(model);
318
+ if (parsed !== void 0) byId.set(parsed.id, parsed);
319
+ }
320
+ const models = (cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()]).map((id) => byId.get(id)).filter((model) => model !== void 0);
321
+ if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
322
+ return models;
323
+ }
324
+ /** Read-only credits query, aggregated by package. Does not consume credits. */
325
+ async fetchCredits(credential) {
326
+ const now = /* @__PURE__ */ new Date();
327
+ const fmt = (date) => {
328
+ const p = (n) => n.toString().padStart(2, "0");
329
+ return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
330
+ };
331
+ const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/get-user-resource`, {
332
+ method: "POST",
333
+ headers: billingHeaders(credential),
334
+ body: JSON.stringify({
335
+ PageNumber: 1,
336
+ PageSize: 100,
337
+ ProductCode: "p_tcaca",
338
+ Status: [0, 3],
339
+ PackageEndTimeRangeBegin: fmt(now),
340
+ PackageEndTimeRangeEnd: fmt(new Date(now.getTime() + 3185136e6))
341
+ }),
342
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
343
+ });
344
+ const envelope = await readEnvelope(response);
345
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
346
+ const wrapper = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
347
+ const data = typeof wrapper["Response"] === "object" && wrapper["Response"] !== null ? wrapper["Response"] : {};
348
+ const inner = typeof data["Data"] === "object" && data["Data"] !== null ? data["Data"] : {};
349
+ const rawAccounts = Array.isArray(inner["Accounts"]) ? inner["Accounts"] : [];
350
+ let total = 0;
351
+ let nearestExpiryMs;
352
+ let expiringSoon = 0;
353
+ const SOON_MS = 2592e5;
354
+ const parseDate = (raw) => {
355
+ if (typeof raw === "number" && raw > 0xe8d4a51000) return raw;
356
+ if (typeof raw === "string" && raw !== "") {
357
+ const parsed = Date.parse(raw);
358
+ if (!Number.isNaN(parsed)) return parsed;
359
+ }
360
+ };
361
+ const packages = [];
362
+ for (const raw of rawAccounts) {
363
+ if (typeof raw !== "object" || raw === null) continue;
364
+ const account = raw;
365
+ const num = (key) => typeof account[key] === "number" ? account[key] : 0;
366
+ const monthly = num("CapacityType") === 4;
367
+ const size = monthly ? num("CycleCapacitySize") : num("CapacitySize");
368
+ const remain = monthly ? num("CycleCapacityRemain") : num("CapacityRemain");
369
+ const capped = remain < 0 ? 0 : remain;
370
+ const cycleEndMs = parseDate(account["CycleEndTime"]);
371
+ const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
372
+ const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
373
+ if (!monthly && (capped <= 0 || expiresAtMs !== void 0 && expiresAtMs <= Date.now())) continue;
374
+ total += capped;
375
+ if (expiresAtMs !== void 0) {
376
+ if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
377
+ if (expiresAtMs - Date.now() <= SOON_MS) expiringSoon += capped;
378
+ }
379
+ packages.push({
380
+ packageName: typeof account["PackageName"] === "string" ? account["PackageName"] : "(unnamed)",
381
+ remain: capped,
382
+ size,
383
+ monthly,
384
+ ...refreshAtMs === void 0 ? {} : { refreshAtMs },
385
+ ...expiresAtMs === void 0 ? {} : { expiresAtMs }
386
+ });
387
+ }
388
+ return {
389
+ total,
390
+ packages,
391
+ expiringSoon,
392
+ ...nearestExpiryMs === void 0 ? {} : { nearestExpiryMs }
393
+ };
394
+ }
395
+ /** Query today's check-in status without changing account state. */
396
+ async fetchCheckinStatus(credential) {
397
+ const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/checkin-activity-status`, {
398
+ method: "POST",
399
+ headers: billingHeaders(credential),
400
+ body: "{}",
401
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
402
+ });
403
+ const envelope = await readEnvelope(response);
404
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
405
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
406
+ const num = (key) => typeof data[key] === "number" ? data[key] : 0;
407
+ return {
408
+ active: data["active"] === true,
409
+ todayCheckedIn: data["today_checked_in"] === true,
410
+ streakDays: num("streak_days"),
411
+ dailyCredit: num("daily_credit"),
412
+ todayCredit: num("today_credit"),
413
+ isStreakDay: data["is_streak_day"] === true,
414
+ nextStreakDay: num("next_streak_day"),
415
+ streakBonusDays: num("streak_bonus_days"),
416
+ streakBonusCredit: num("streak_bonus_credit"),
417
+ ...typeof data["claim_button_text"] === "string" && data["claim_button_text"] !== "" ? { claimButtonText: data["claim_button_text"] } : {}
418
+ };
419
+ }
420
+ /** Claim today's check-in reward. The browser route guards this mutation. */
421
+ async claimDailyCheckin(credential) {
422
+ const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/daily-checkin`, {
423
+ method: "POST",
424
+ headers: billingHeaders(credential),
425
+ body: "{}",
426
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
427
+ });
428
+ const envelope = await readEnvelope(response);
429
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
430
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
431
+ const numberField = (key) => typeof data[key] === "number" ? data[key] : 0;
432
+ return {
433
+ credit: numberField("credit"),
434
+ streakDays: numberField("streak_days"),
435
+ isStreakDay: data["is_streak_day"] === true
436
+ };
437
+ }
438
+ /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
439
+ async credits(credential) {
440
+ try {
441
+ return {
442
+ ok: true,
443
+ data: await this.fetchCredits(credential)
444
+ };
445
+ } catch (error) {
446
+ return {
447
+ ok: false,
448
+ message: String(error)
449
+ };
450
+ }
451
+ }
452
+ };
453
+ //#endregion
454
+ //#region src/accounts.ts
455
+ /**
456
+ * Account pool: discovers every WorkBuddy credential snapshot the desktop app
457
+ * has left on this machine and hands out one healthy account per request,
458
+ * rotating away from any account the upstream has rate-limited.
459
+ *
460
+ * Discovery is read-only: the desktop app's files are never written. Each
461
+ * account is keyed by its billing identity (`uin`, falling back to `uid`), so
462
+ * re-logging the same account refreshes in place instead of creating a duplicate.
463
+ *
464
+ * @module dsh-workbuddy-xdpool/accounts
465
+ */
466
+ /** Live auth file name the WorkBuddy desktop app writes. */
467
+ const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
468
+ /** Snapshot files left behind by previous logins share this prefix. */
469
+ const SNAPSHOT_PREFIX = "workbuddy-desktop.";
470
+ /** Env override for the auth file or its directory. */
471
+ const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
472
+ function nonEmptyEnv(value) {
473
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
474
+ }
475
+ /**
476
+ * Platform-default directories holding the desktop app's auth files.
477
+ * Windows probes Local before Roaming; a redirected profile still resolves
478
+ * through the env location.
479
+ */
480
+ function defaultDesktopAuthDirs(platform = process.platform, home = homedir(), env = process.env) {
481
+ if (platform === "darwin") return [join(home, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth")];
482
+ if (platform === "win32") {
483
+ const local = nonEmptyEnv(env["LOCALAPPDATA"]) ?? join(home, "AppData", "Local");
484
+ const roaming = nonEmptyEnv(env["APPDATA"]) ?? join(home, "AppData", "Roaming");
485
+ return [join(local, "CodeBuddyExtension", "Data", "Public", "auth"), join(roaming, "CodeBuddyExtension", "Data", "Public", "auth")];
486
+ }
487
+ if (platform === "linux") {
488
+ const config = nonEmptyEnv(env["XDG_CONFIG_HOME"]) ?? join(home, ".config");
489
+ return [join(config, "CodeBuddyExtension", "Data", "Public", "auth")];
490
+ }
491
+ return [];
492
+ }
493
+ /** Normalize an expiry that may arrive in seconds or milliseconds. */
494
+ function expiryToMs(value) {
495
+ if (value <= 0) return 0;
496
+ return value > 0xe8d4a51000 ? value : value * 1e3;
497
+ }
498
+ function optionalString(value) {
499
+ return typeof value === "string" && value !== "" ? value : void 0;
500
+ }
501
+ /**
502
+ * Parse a WorkBuddy auth document. Accepts the nested desktop shape
503
+ * `{"auth":{...},"account":{...}}` and the flat panel shape; returns undefined
504
+ * when there is no usable access token.
505
+ */
506
+ function parseWorkBuddyAuth(text, sourcePath) {
507
+ let parsed;
508
+ try {
509
+ parsed = JSON.parse(text);
510
+ } catch {
511
+ return;
512
+ }
513
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
514
+ const document = parsed;
515
+ let auth;
516
+ let identity;
517
+ if (typeof document["auth"] === "object" && document["auth"] !== null) {
518
+ auth = document["auth"];
519
+ identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : {};
520
+ } else {
521
+ auth = document;
522
+ identity = document;
523
+ }
524
+ const accessToken = typeof auth["accessToken"] === "string" ? auth["accessToken"] : "";
525
+ if (accessToken === "") return void 0;
526
+ const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
527
+ if (refreshExpiresAtMs !== void 0 && refreshExpiresAtMs > 0 && refreshExpiresAtMs < Date.now()) return;
528
+ return {
529
+ accessToken,
530
+ refreshToken: typeof auth["refreshToken"] === "string" ? auth["refreshToken"] : "",
531
+ expiresAtMs: typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0,
532
+ ...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
533
+ ...optionalString(identity["nickname"]) === void 0 ? {} : { nickname: optionalString(identity["nickname"]) },
534
+ ...optionalString(identity["uin"]) === void 0 ? {} : { uin: optionalString(identity["uin"]) },
535
+ ...optionalString(identity["uid"]) === void 0 ? {} : { uid: optionalString(identity["uid"]) },
536
+ ...optionalString(identity["enterpriseId"]) === void 0 ? {} : { enterpriseId: optionalString(identity["enterpriseId"]) },
537
+ domain: typeof auth["domain"] === "string" ? auth["domain"] : "",
538
+ sourcePath
539
+ };
540
+ }
541
+ /**
542
+ * Stable account id. `uin` is the billing identity the upstream keys on and
543
+ * survives re-login; `uid` is the fallback.
544
+ */
545
+ function workbuddyAccountId(credential) {
546
+ const stable = credential.uin ?? credential.uid ?? credential.nickname ?? "unknown";
547
+ return createHash("sha256").update(`workbuddy\0${stable}`).digest("hex").slice(0, 16);
548
+ }
549
+ /** Human label; distinguishes same-nickname accounts by uid prefix. */
550
+ function accountLabel(credential) {
551
+ const name = credential.nickname ?? "WorkBuddy";
552
+ const discriminator = (credential.uid ?? credential.uin ?? "").slice(0, 8);
553
+ return discriminator === "" ? name : `${name}#${discriminator}`;
554
+ }
555
+ /** List the auth files in one directory: the live file plus every snapshot. */
556
+ async function authFilesIn(dir) {
557
+ let entries;
558
+ try {
559
+ entries = await readdir(dir);
560
+ } catch {
561
+ return [];
562
+ }
563
+ const files = entries.filter((name) => {
564
+ if (name === "workbuddy-desktop.info") return true;
565
+ return name.startsWith(SNAPSHOT_PREFIX) && name.endsWith(".info");
566
+ });
567
+ files.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
568
+ return files.map((name) => join(dir, name));
569
+ }
570
+ async function readCredential(path) {
571
+ try {
572
+ return parseWorkBuddyAuth(await readFile(path, "utf8"), path);
573
+ } catch {
574
+ return;
575
+ }
576
+ }
577
+ /** Every directory the pool should scan, in probe order. */
578
+ function candidateAuthDirs(env = process.env) {
579
+ const dirs = [];
580
+ const override = nonEmptyEnv(env[WORKBUDDY_AUTH_FILE_ENV]);
581
+ if (override !== void 0) dirs.push(override.toLowerCase().endsWith(".info") ? resolve(override, "..") : override);
582
+ dirs.push(...defaultDesktopAuthDirs(process.env["DSH_TEST_PLATFORM"]));
583
+ return dirs;
584
+ }
585
+ /**
586
+ * Read-only pool of every discovered WorkBuddy account, with rate-limit
587
+ * cooldown and round-robin failover.
588
+ */
589
+ var WorkBuddyAccountPool = class {
590
+ logger;
591
+ authDirs;
592
+ cooldownMs;
593
+ client;
594
+ refreshMarginMs;
595
+ accounts = [];
596
+ cursor = 0;
597
+ lastScanAtMs = 0;
598
+ preferredId;
599
+ refreshInflight = /* @__PURE__ */ new Map();
600
+ constructor(options = {}) {
601
+ this.logger = options.logger;
602
+ this.authDirs = options.authDirs ?? candidateAuthDirs();
603
+ this.cooldownMs = options.cooldownMs ?? 6e4;
604
+ this.client = options.client;
605
+ this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
606
+ }
607
+ /**
608
+ * Re-apply configuration that only affects discovery and cooldown policy,
609
+ * without rebuilding the pool. A later `scan()` uses the new auth dirs and
610
+ * cooldown window; existing accounts keep their in-memory state.
611
+ */
612
+ applyConfig(options) {
613
+ if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
614
+ if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
615
+ }
616
+ /** Rescan the auth directories and merge newly discovered accounts. */
617
+ async scan() {
618
+ const found = [];
619
+ for (const dir of this.authDirs) for (const file of await authFilesIn(dir)) {
620
+ const credential = await readCredential(file);
621
+ if (credential !== void 0) found.push(credential);
622
+ }
623
+ const byId = /* @__PURE__ */ new Map();
624
+ for (const account of this.accounts) byId.set(account.id, account);
625
+ for (const credential of found) {
626
+ const id = workbuddyAccountId(credential);
627
+ const existing = byId.get(id);
628
+ if (existing === void 0) {
629
+ byId.set(id, {
630
+ id,
631
+ label: accountLabel(credential),
632
+ credential,
633
+ cooldownUntilMs: 0,
634
+ modelCooldowns: {},
635
+ rateLimitHits: 0
636
+ });
637
+ continue;
638
+ }
639
+ if ((credential.expiresAtMs ?? 0) > (existing.credential.expiresAtMs ?? 0)) byId.set(id, {
640
+ ...existing,
641
+ credential,
642
+ label: accountLabel(credential)
643
+ });
644
+ }
645
+ this.accounts = [...byId.values()];
646
+ this.lastScanAtMs = Date.now();
647
+ return this.accounts;
648
+ }
649
+ /** All accounts, cooldown state included. */
650
+ list(region) {
651
+ if (region === void 0) return this.accounts;
652
+ return this.accounts.filter((account) => regionOf(account.credential.domain) === region);
653
+ }
654
+ /**
655
+ * Accounts currently eligible to serve a request.
656
+ *
657
+ * With a `modelId`, an account is eligible when it is not account-wide cooled
658
+ * AND that model is not cooling on it — so a 429 on `hy4-preview` only keeps
659
+ * that model out while `hy3` on the same account stays usable. Without a
660
+ * model id the legacy account-wide check applies (callers that cannot name a
661
+ * model, e.g. CLI diagnostics).
662
+ */
663
+ available(now, modelId, region) {
664
+ return this.accounts.filter((account) => {
665
+ if (account.cooldownUntilMs > now) return false;
666
+ if (modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now) return false;
667
+ if (region !== void 0 && regionOf(account.credential.domain) !== region) return false;
668
+ return true;
669
+ });
670
+ }
671
+ /**
672
+ * Pick the next usable account for an optional model. Scans on first use,
673
+ * and rescans when every known account is cooling down — a fresh desktop
674
+ * login is the usual way out of an exhausted pool. A preferred
675
+ * (user-selected) account that is healthy is tried first; otherwise the
676
+ * cursor round-robins so consecutive requests spread across accounts and a
677
+ * still-cooling preferred account is skipped.
678
+ */
679
+ async acquire(modelId, region) {
680
+ if (this.accounts.length === 0) await this.scan();
681
+ let pool = this.available(Date.now(), modelId, region);
682
+ if (pool.length === 0) {
683
+ await this.scan();
684
+ pool = this.available(Date.now(), modelId, region);
685
+ }
686
+ if (pool.length === 0) return void 0;
687
+ let start = this.cursor % pool.length;
688
+ if (this.preferredId !== void 0) {
689
+ const preferredIndex = pool.findIndex((account) => account.id === this.preferredId);
690
+ if (preferredIndex !== -1) start = preferredIndex;
691
+ }
692
+ for (let step = 0; step < pool.length; step += 1) {
693
+ const account = pool[(start + step) % pool.length];
694
+ if (account === void 0) continue;
695
+ await this.ensureFresh(account);
696
+ this.cursor = (start + step + 1) % pool.length;
697
+ return account;
698
+ }
699
+ return pool[0];
700
+ }
701
+ /** Pin the account the plugin card should prefer; tokens stay out of settings. */
702
+ prefer(accountId) {
703
+ this.preferredId = accountId;
704
+ }
705
+ /** Best-effort refresh of one account after a session-dead upstream answer. */
706
+ async refreshAccount(accountId) {
707
+ const account = this.accounts.find((item) => item.id === accountId);
708
+ if (account === void 0) return;
709
+ await this.ensureFresh(account);
710
+ }
711
+ /**
712
+ * Refresh the account's access token when it is within the margin (or already
713
+ * expired), in-flight de-duped per account. A failed refresh keeps the
714
+ * existing token when it has not yet expired, so an unreachable refresh
715
+ * endpoint never takes down a working session.
716
+ */
717
+ async ensureFresh(account) {
718
+ if (this.client === void 0) return;
719
+ const credential = account.credential;
720
+ if (!(credential.expiresAtMs <= 0 || credential.expiresAtMs <= Date.now() + this.refreshMarginMs)) return;
721
+ const existing = this.refreshInflight.get(account.id);
722
+ if (existing !== void 0) {
723
+ await existing;
724
+ return;
725
+ }
726
+ const run = (async () => {
727
+ if (credential.refreshToken === "") {
728
+ if (credential.expiresAtMs > Date.now() + 3e4) return;
729
+ this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} token expired with no refresh token; sign in again`);
730
+ return;
731
+ }
732
+ try {
733
+ const outcome = await this.client.refreshToken(credential);
734
+ account.credential = {
735
+ ...credential,
736
+ accessToken: outcome.accessToken,
737
+ ...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
738
+ expiresAtMs: outcome.expiresInSec !== void 0 ? Date.now() + outcome.expiresInSec * 1e3 : credential.expiresAtMs,
739
+ ...outcome.domain === void 0 || outcome.domain === "" ? {} : { domain: outcome.domain }
740
+ };
741
+ this.logger?.info?.(`dsh-workbuddy-xdpool: refreshed token for ${account.label}`);
742
+ } catch (error) {
743
+ if (credential.expiresAtMs > Date.now() + 3e4) this.logger?.warn?.(`dsh-workbuddy-xdpool: token refresh failed but token still valid for ${account.label}`, error);
744
+ else this.logger?.error?.(`dsh-workbuddy-xdpool: token refresh failed and token expired for ${account.label}`, error);
745
+ }
746
+ })();
747
+ this.refreshInflight.set(account.id, run);
748
+ try {
749
+ await run;
750
+ } finally {
751
+ this.refreshInflight.delete(account.id);
752
+ }
753
+ }
754
+ /**
755
+ * Mark an account (or one of its models) rate-limited.
756
+ *
757
+ * With `modelId`, only that model on the account is cooled — the account's
758
+ * other models stay in rotation, matching the upstream's per-model rate
759
+ * limit ("可切换其他模型继续使用"). Without a model id the whole account is
760
+ * cooled, which callers should reserve for limits that truly span every model.
761
+ */
762
+ penalize(accountId, resetAtMs, modelId) {
763
+ const account = this.accounts.find((item) => item.id === accountId);
764
+ if (account === void 0) return;
765
+ account.rateLimitHits += 1;
766
+ const until = resetAtMs ?? Date.now() + this.cooldownMs;
767
+ if (modelId !== void 0 && modelId !== "") {
768
+ account.modelCooldowns[modelId] = Math.max(account.modelCooldowns[modelId] ?? 0, until);
769
+ this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} rate-limited on model ${modelId}; cooling that model until ${new Date(until).toISOString()}`);
770
+ return;
771
+ }
772
+ account.cooldownUntilMs = Math.max(account.cooldownUntilMs, until);
773
+ this.logger?.warn(`dsh-workbuddy-xdpool: account ${account.label} rate-limited; cooling until ${new Date(until).toISOString()}`);
774
+ }
775
+ /** Clear all cooldowns (account-wide and per-model), e.g. from a reset command. */
776
+ resetCooldowns() {
777
+ for (const account of this.accounts) {
778
+ account.cooldownUntilMs = 0;
779
+ account.modelCooldowns = {};
780
+ account.rateLimitHits = 0;
781
+ }
782
+ }
783
+ /** Diagnostics snapshot. Account-wide cooling count (per-model cooling excluded:
784
+ * the account as a whole stays usable when only one model is limited). */
785
+ status() {
786
+ const now = Date.now();
787
+ return {
788
+ count: this.accounts.length,
789
+ cooling: this.accounts.filter((account) => account.cooldownUntilMs > now).length,
790
+ lastScanAtMs: this.lastScanAtMs
791
+ };
792
+ }
793
+ };
794
+ //#endregion
795
+ //#region src/catalog.ts
796
+ /** Static fallback used before the first live catalog fetch. */
797
+ const FALLBACK_WORKBUDDY_MODELS = [
798
+ {
799
+ id: "glm-5.3",
800
+ name: "GLM-5.3",
801
+ contextWindow: 2e5,
802
+ maxOutputTokens: 128e3,
803
+ supportsImages: true
804
+ },
805
+ {
806
+ id: "glm-5.3-flash",
807
+ name: "GLM-5.3-Flash",
808
+ contextWindow: 2e5,
809
+ maxOutputTokens: 128e3,
810
+ supportsImages: true
811
+ },
812
+ {
813
+ id: "glm-5.2",
814
+ name: "GLM-5.2",
815
+ contextWindow: 2e5,
816
+ maxOutputTokens: 128e3,
817
+ supportsImages: true
818
+ },
819
+ {
820
+ id: "glm-5.1",
821
+ name: "GLM-5.1",
822
+ contextWindow: 2e5,
823
+ maxOutputTokens: 128e3,
824
+ supportsImages: false
825
+ },
826
+ {
827
+ id: "deepseek-v4-pro",
828
+ name: "DeepSeek-V4-Pro",
829
+ contextWindow: 2e5,
830
+ maxOutputTokens: 128e3,
831
+ supportsImages: true
832
+ },
833
+ {
834
+ id: "deepseek-v4-flash",
835
+ name: "DeepSeek-V4-Flash",
836
+ contextWindow: 2e5,
837
+ maxOutputTokens: 128e3,
838
+ supportsImages: true
839
+ },
840
+ {
841
+ id: "kimi-k3",
842
+ name: "Kimi-K3",
843
+ contextWindow: 2e5,
844
+ maxOutputTokens: 128e3,
845
+ supportsImages: true
846
+ },
847
+ {
848
+ id: "minimax-m3",
849
+ name: "MiniMax-M3",
850
+ contextWindow: 2e5,
851
+ maxOutputTokens: 128e3,
852
+ supportsImages: true
853
+ },
854
+ {
855
+ id: "hy3",
856
+ name: "Hy3",
857
+ contextWindow: 2e5,
858
+ maxOutputTokens: 128e3,
859
+ supportsImages: true
860
+ },
861
+ {
862
+ id: "hy4-preview",
863
+ name: "Hy4-Preview",
864
+ contextWindow: 1e6,
865
+ maxOutputTokens: 128e3,
866
+ supportsImages: true
867
+ }
868
+ ];
869
+ /** Live catalog with a static fallback behind it. */
870
+ var WorkBuddyCatalog = class {
871
+ models = FALLBACK_WORKBUDDY_MODELS;
872
+ listeners = /* @__PURE__ */ new Set();
873
+ /** User's model selection. Empty object = follow the catalog unfiltered. */
874
+ selection = {};
875
+ current() {
876
+ return this.models;
877
+ }
878
+ /**
879
+ * The models DSH should actually offer, after applying the user's selection:
880
+ * disabled models are dropped, an explicit image list overrides the upstream
881
+ * capability flag, and a per-model budget caps the advertised window.
882
+ *
883
+ * An absent `enabledModelIds` means "everything" — a fresh install with no
884
+ * saved selection must not present an empty picker.
885
+ */
886
+ visible() {
887
+ const enabled = this.selection.enabledModelIds;
888
+ const allow = enabled === void 0 ? void 0 : new Set(enabled);
889
+ const images = this.selection.imageModelIds;
890
+ const imageSet = images === void 0 ? void 0 : new Set(images);
891
+ const budgets = this.selection.contextBudgets;
892
+ return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
893
+ const next = { ...model };
894
+ if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
895
+ const budget = budgets?.[model.id];
896
+ if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
897
+ return next;
898
+ });
899
+ }
900
+ /** Replace the catalog and notify the adapter to rebuild its model list. */
901
+ update(models) {
902
+ if (models.length === 0) return;
903
+ this.models = models;
904
+ this.notify();
905
+ }
906
+ /** Restore the static fallback, e.g. when the upstream stops answering. */
907
+ reset() {
908
+ this.models = FALLBACK_WORKBUDDY_MODELS;
909
+ this.notify();
910
+ }
911
+ /** Replace the user's selection; the adapter rebuilds from `visible()`. */
912
+ applySelection(selection) {
913
+ this.selection = selection;
914
+ this.notify();
915
+ }
916
+ /** The selection currently in force, for the card's save round-trip. */
917
+ currentSelection() {
918
+ return this.selection;
919
+ }
920
+ onChange(listener) {
921
+ this.listeners.add(listener);
922
+ return () => this.listeners.delete(listener);
923
+ }
924
+ find(id) {
925
+ return this.models.find((model) => model.id === id);
926
+ }
927
+ /** Replace the catalog from the live upstream list; keeps the fallback if empty. */
928
+ updateFromUpstream(models) {
929
+ this.update(catalogFromUpstream(models));
930
+ }
931
+ notify() {
932
+ for (const listener of this.listeners) listener();
933
+ }
934
+ };
935
+ /** Convert one upstream catalog entry into the plugin's model-info shape. */
936
+ function toModelInfo(model) {
937
+ return {
938
+ id: model.id,
939
+ name: model.name,
940
+ contextWindow: model.contextWindow,
941
+ maxOutputTokens: model.maxTokens,
942
+ supportsImages: model.multimodal ?? false,
943
+ ...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
944
+ ...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
945
+ };
946
+ }
947
+ /** Map the live upstream list, falling back to the static list when empty. */
948
+ function catalogFromUpstream(models) {
949
+ if (models.length === 0) return FALLBACK_WORKBUDDY_MODELS;
950
+ return models.map(toModelInfo);
951
+ }
952
+ //#endregion
953
+ //#region src/status.ts
954
+ /** Format the per-model credit multipliers. */
955
+ function formatRates(status) {
956
+ const lines = ["Model credit multipliers:"];
957
+ for (const model of status.models) {
958
+ const rate = model.multiplier === void 0 ? "x?" : `x${model.multiplier.toFixed(2)}`;
959
+ lines.push(` ${model.name.padEnd(20)} ${rate}`);
960
+ }
961
+ return lines.join("\n");
962
+ }
963
+ z.object({
964
+ authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
965
+ cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
966
+ enabledModelIds: z.array(z.string()).default([]).description("Model ids enabled in the picker (empty = all)"),
967
+ imageModelIds: z.array(z.string()).default([]).description("Model ids accepting image input (empty = follow upstream)"),
968
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Per-model context-window override, keyed by model id")
969
+ });
970
+ /** Assemble the runtime objects without registering anything. */
971
+ function createCore(logger) {
972
+ const client = new WorkBuddyUpstreamClient();
973
+ return {
974
+ pool: new WorkBuddyAccountPool({
975
+ ...logger === void 0 ? {} : { logger },
976
+ client
977
+ }),
978
+ catalog: new WorkBuddyCatalog(),
979
+ client
980
+ };
981
+ }
982
+ //#endregion
983
+ //#region src/bin.ts
984
+ /**
985
+ * Command-line diagnostics and account management.
986
+ *
987
+ * dsh plugin --profile desktop exec dsh-workbuddy-xdpool <command>
988
+ *
989
+ * @module dsh-workbuddy-xdpool/bin
990
+ */
991
+ /** Directory holding imported account snapshots. */
992
+ const ACCOUNT_DIR_NAME = ".workbuddy-xdpool";
993
+ /** Snapshot files are named by the md5 prefix of their key, so any key is safe. */
994
+ function snapshotPath(key, dir) {
995
+ return join(dir, `${createHash("md5").update(key).digest("hex").slice(0, 8)}.json`);
996
+ }
997
+ function dshHome() {
998
+ const fromEnv = process.env["DSH_HOME"];
999
+ if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv.trim();
1000
+ return join(homedir(), ".dsh");
1001
+ }
1002
+ function accountDir() {
1003
+ return join(dshHome(), ACCOUNT_DIR_NAME);
1004
+ }
1005
+ function usage() {
1006
+ return [
1007
+ "dsh-workbuddy-xdpool — multi-account WorkBuddy provider for DeepSeek Harness",
1008
+ "",
1009
+ "Usage: dsh-workbuddy-xdpool <command> [options]",
1010
+ "",
1011
+ "Commands:",
1012
+ " status Account pool, credits, and shim state (add --credits, --json, --rates)",
1013
+ " doctor Diagnose discovery, cooldowns, and upstream reachability",
1014
+ " accounts List discovered accounts (add --json)",
1015
+ " import <key> Snapshot the current desktop login as <key> (add --force)",
1016
+ " remove <key> Delete one imported snapshot",
1017
+ " login Guide for adding another account (desktop app is single-sign-in)",
1018
+ " checkin [all|<acct>] Daily check-in: report status, or collect with `all` / a label",
1019
+ " reset Clear all rate-limit cooldowns immediately",
1020
+ "",
1021
+ "Options:",
1022
+ " --json Machine-readable output",
1023
+ " --credits Query remaining credits (read-only; does not consume)",
1024
+ " --rates Show per-model credit multipliers",
1025
+ " --force Overwrite an existing snapshot"
1026
+ ].join("\n");
1027
+ }
1028
+ /** Live auth files, in probe order. */
1029
+ function liveCandidates() {
1030
+ const fromEnv = process.env["WORKBUDDY_AUTH_FILE"];
1031
+ if (typeof fromEnv === "string" && fromEnv.trim() !== "") return [resolve(fromEnv.trim())];
1032
+ return defaultDesktopAuthDirs().map((dir) => join(dir, WORKBUDDY_LIVE_FILENAME));
1033
+ }
1034
+ async function readJsonFile(path) {
1035
+ try {
1036
+ return JSON.parse(await readFile(path, "utf8"));
1037
+ } catch {
1038
+ return;
1039
+ }
1040
+ }
1041
+ async function commandStatus(args) {
1042
+ const asJson = args.includes("--json");
1043
+ const withCredits = args.includes("--credits");
1044
+ const withRates = args.includes("--rates");
1045
+ const core = createCore();
1046
+ const accounts = await core.pool.scan();
1047
+ const status = {
1048
+ ok: accounts.length > 0,
1049
+ accounts: accounts.map((account) => {
1050
+ const now = Date.now();
1051
+ const modelCooling = Object.entries(account.modelCooldowns).filter(([, until]) => until > now).map(([modelId, until]) => ({
1052
+ modelId,
1053
+ until: new Date(until).toISOString()
1054
+ }));
1055
+ return {
1056
+ id: account.id,
1057
+ label: account.label,
1058
+ domain: account.credential.domain,
1059
+ ...account.credential.expiresAtMs === 0 ? {} : { expiresAt: new Date(account.credential.expiresAtMs).toISOString() },
1060
+ cooling: account.cooldownUntilMs > now,
1061
+ ...modelCooling.length === 0 ? {} : { modelCooldowns: modelCooling },
1062
+ rateLimitHits: account.rateLimitHits,
1063
+ sourcePath: account.credential.sourcePath
1064
+ };
1065
+ }),
1066
+ cooling: accounts.filter((account) => account.cooldownUntilMs > Date.now()).length,
1067
+ models: core.catalog.current().map((model) => ({
1068
+ id: model.id,
1069
+ name: model.name,
1070
+ multiplier: model.multiplier
1071
+ })),
1072
+ shim: { running: false }
1073
+ };
1074
+ if (asJson) console.log(JSON.stringify(status, null, 2));
1075
+ else console.log(`WorkBuddy XD Pool: ${status.accounts.length} account(s), ${status.cooling} cooling\n` + status.accounts.map((account) => {
1076
+ const flag = account.cooling ? "⏸ " : "▶ ";
1077
+ const hits = account.rateLimitHits > 0 ? ` (hits ${account.rateLimitHits})` : "";
1078
+ const modelCooling = (account.modelCooldowns ?? []).map((mc) => `\n model-cool: ${mc.modelId} until ${mc.until}`).join("");
1079
+ return `${flag}${account.label} [${account.domain || "cn"}]${hits}\n ${account.sourcePath}${modelCooling}`;
1080
+ }).join("\n"));
1081
+ if (withRates) console.log(`\n${formatRates(status)}`);
1082
+ if (withCredits) {
1083
+ const first = accounts.find((account) => account.cooldownUntilMs <= Date.now());
1084
+ if (first === void 0) console.log("\nCredits: skipped (every account is cooling down)");
1085
+ else try {
1086
+ const credits = await core.client.fetchCredits(first.credential);
1087
+ console.log(`\nCredits for ${first.label}: ` + JSON.stringify(credits));
1088
+ } catch (error) {
1089
+ console.log(`\nCredits for ${first.label}: query failed — ${String(error).slice(0, 200)}`);
1090
+ }
1091
+ }
1092
+ return status.ok ? 0 : 1;
1093
+ }
1094
+ /**
1095
+ * Daily check-in from the terminal.
1096
+ *
1097
+ * `checkin` with no argument reports every account's status; `checkin all`
1098
+ * collects wherever a reward is still available, and `checkin <label>` targets
1099
+ * one account. A collection is never attempted twice for the same account on
1100
+ * the same day — the status is re-read first and an already-collected account
1101
+ * is reported as such.
1102
+ */
1103
+ async function commandCheckin(args) {
1104
+ const asJson = args.includes("--json");
1105
+ const target = args.find((arg) => !arg.startsWith("--"));
1106
+ const core = createCore();
1107
+ if ((await core.pool.scan()).length === 0) {
1108
+ console.error("No WorkBuddy account discovered. Sign in with the WorkBuddy desktop app first.");
1109
+ return 1;
1110
+ }
1111
+ const wanted = target === void 0 || target === "all" ? core.pool.list() : core.pool.list().filter((account) => account.label === target || account.id === target || account.id.startsWith(target));
1112
+ if (wanted.length === 0) {
1113
+ console.error(`No account matches "${target}". Run \`accounts\` to list labels.`);
1114
+ return 1;
1115
+ }
1116
+ const rows = [];
1117
+ for (const account of wanted) {
1118
+ const row = {
1119
+ label: account.label,
1120
+ active: false,
1121
+ alreadyCheckedIn: false,
1122
+ streakDays: 0
1123
+ };
1124
+ try {
1125
+ const status = await core.client.fetchCheckinStatus(account.credential);
1126
+ row.active = status.active;
1127
+ row.alreadyCheckedIn = status.todayCheckedIn;
1128
+ row.streakDays = status.streakDays;
1129
+ if (target !== void 0 && status.active && !status.todayCheckedIn) {
1130
+ const claim = await core.client.claimDailyCheckin(account.credential);
1131
+ row.claimed = claim.credit;
1132
+ row.streakDays = claim.streakDays;
1133
+ row.alreadyCheckedIn = true;
1134
+ }
1135
+ } catch (error) {
1136
+ row.error = String(error).slice(0, 200);
1137
+ }
1138
+ rows.push(row);
1139
+ }
1140
+ if (asJson) {
1141
+ console.log(JSON.stringify(rows, null, 2));
1142
+ return rows.every((row) => row.error === void 0) ? 0 : 1;
1143
+ }
1144
+ console.log("Daily check-in:");
1145
+ for (const row of rows) {
1146
+ if (row.error !== void 0) {
1147
+ console.log(` ! ${row.label}: query failed — ${row.error}`);
1148
+ continue;
1149
+ }
1150
+ if (!row.active) {
1151
+ console.log(` – ${row.label}: no check-in activity`);
1152
+ continue;
1153
+ }
1154
+ if (row.claimed !== void 0) {
1155
+ console.log(` + ${row.label}: collected ${row.claimed} credit(s) (streak ${row.streakDays})`);
1156
+ continue;
1157
+ }
1158
+ console.log(row.alreadyCheckedIn ? ` ✓ ${row.label}: already checked in today (streak ${row.streakDays})` : ` · ${row.label}: not checked in today — run \`checkin all\` to collect`);
1159
+ }
1160
+ if (target === void 0) console.log("\nAdd `all` (or an account label) to collect.");
1161
+ return rows.every((row) => row.error === void 0) ? 0 : 1;
1162
+ }
1163
+ async function commandDoctor() {
1164
+ const lines = [];
1165
+ let healthy = true;
1166
+ lines.push(`dsh-workbuddy-xdpool doctor`);
1167
+ lines.push(` platform : ${platform()}`);
1168
+ lines.push(` dsh home : ${dshHome()}`);
1169
+ lines.push("");
1170
+ lines.push("Credential discovery:");
1171
+ for (const candidate of liveCandidates()) {
1172
+ const raw = await readJsonFile(candidate);
1173
+ const credential = raw === void 0 ? void 0 : parseWorkBuddyAuth(JSON.stringify(raw), candidate);
1174
+ lines.push(` ${credential === void 0 ? "✗" : "✓"} ${candidate}`);
1175
+ }
1176
+ const dirs = defaultDesktopAuthDirs();
1177
+ for (const dir of dirs) {
1178
+ const { readdir } = await import("node:fs/promises");
1179
+ let names = [];
1180
+ try {
1181
+ names = await readdir(dir);
1182
+ } catch {
1183
+ lines.push(` - ${dir} (absent)`);
1184
+ continue;
1185
+ }
1186
+ const snapshots = names.filter((name) => name.startsWith("workbuddy-desktop.") && name.endsWith(".info"));
1187
+ lines.push(` ✓ ${dir} → ${snapshots.length} snapshot(s)`);
1188
+ }
1189
+ lines.push("");
1190
+ const accounts = await createCore().pool.scan();
1191
+ lines.push(`Accounts discovered: ${accounts.length}`);
1192
+ if (accounts.length === 0) {
1193
+ healthy = false;
1194
+ lines.push(" ✗ none — sign in on the WorkBuddy desktop app, then run `import <key>`");
1195
+ }
1196
+ for (const account of accounts) {
1197
+ const state = account.cooldownUntilMs > Date.now() ? "cooling" : "ready";
1198
+ lines.push(` ✓ ${account.label} (${state}, hits ${account.rateLimitHits})`);
1199
+ }
1200
+ lines.push("");
1201
+ lines.push(`Imported snapshots: ${accountDir()}`);
1202
+ const { readdir: readdir2 } = await import("node:fs/promises");
1203
+ let imported = [];
1204
+ try {
1205
+ imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
1206
+ } catch {}
1207
+ lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
1208
+ console.log(lines.join("\n"));
1209
+ return healthy ? 0 : 1;
1210
+ }
1211
+ async function commandImport(args) {
1212
+ const key = args.filter((arg) => !arg.startsWith("--"))[0];
1213
+ if (key === void 0) {
1214
+ console.error("usage: dsh-workbuddy-xdpool import <key> [--force]");
1215
+ return 2;
1216
+ }
1217
+ const force = args.includes("--force");
1218
+ let source;
1219
+ for (const candidate of liveCandidates()) {
1220
+ const raw = await readJsonFile(candidate);
1221
+ if (raw === void 0) continue;
1222
+ if (parseWorkBuddyAuth(JSON.stringify(raw), candidate) !== void 0) {
1223
+ source = candidate;
1224
+ break;
1225
+ }
1226
+ }
1227
+ if (source === void 0) {
1228
+ console.error(`No signed-in WorkBuddy desktop session found. Sign in on the desktop app first
1229
+ (looked in: ${liveCandidates().join(", ")})`);
1230
+ return 1;
1231
+ }
1232
+ const dir = accountDir();
1233
+ await mkdir(dir, { recursive: true });
1234
+ const target = snapshotPath(key, dir);
1235
+ const { access } = await import("node:fs/promises");
1236
+ let exists = false;
1237
+ try {
1238
+ await access(target);
1239
+ exists = true;
1240
+ } catch {}
1241
+ if (exists && !force) {
1242
+ console.error(`Snapshot "${key}" already exists. Re-run with --force to overwrite.`);
1243
+ return 1;
1244
+ }
1245
+ await copyFile(source, target);
1246
+ const credential = parseWorkBuddyAuth(await readFile(target, "utf8"), target);
1247
+ const label = credential === void 0 ? "unknown" : (credential.nickname ?? "WorkBuddy") + `#${workbuddyAccountId(credential).slice(0, 8)}`;
1248
+ console.log(`Imported "${key}" → ${label}\n saved: ${target}`);
1249
+ return 0;
1250
+ }
1251
+ async function commandRemove(args) {
1252
+ const key = args.filter((arg) => !arg.startsWith("--"))[0];
1253
+ if (key === void 0) {
1254
+ console.error("usage: dsh-workbuddy-xdpool remove <key>");
1255
+ return 2;
1256
+ }
1257
+ const target = snapshotPath(key, accountDir());
1258
+ const { unlink } = await import("node:fs/promises");
1259
+ try {
1260
+ await unlink(target);
1261
+ console.log(`Removed snapshot "${key}"`);
1262
+ return 0;
1263
+ } catch {
1264
+ console.error(`No snapshot named "${key}"`);
1265
+ return 1;
1266
+ }
1267
+ }
1268
+ function commandLogin() {
1269
+ console.log([
1270
+ "Adding another WorkBuddy account",
1271
+ "",
1272
+ "The WorkBuddy desktop app holds one signed-in account at a time, so each",
1273
+ "additional account is captured as a snapshot after you switch login:",
1274
+ "",
1275
+ " 1. Open the WorkBuddy desktop app and sign in (scan the QR code).",
1276
+ " 2. Run: dsh plugin --profile desktop exec dsh-workbuddy-xdpool import <key>",
1277
+ " 3. Sign in with the next account in the desktop app.",
1278
+ " 4. Run the import command again with a different <key>.",
1279
+ " 5. Restart DSH Desktop; every imported account joins the rotation pool.",
1280
+ "",
1281
+ "Snapshots live in " + accountDir() + " and store the desktop app's tokens",
1282
+ "verbatim — protect that directory like a password."
1283
+ ].join("\n"));
1284
+ return 0;
1285
+ }
1286
+ async function commandReset() {
1287
+ const core = createCore();
1288
+ await core.pool.scan();
1289
+ core.pool.resetCooldowns();
1290
+ console.log("Cleared all rate-limit cooldowns.");
1291
+ return 0;
1292
+ }
1293
+ async function commandAccounts(args) {
1294
+ const asJson = args.includes("--json");
1295
+ const accounts = await createCore().pool.scan();
1296
+ if (asJson) console.log(JSON.stringify(accounts.map((account) => ({
1297
+ id: account.id,
1298
+ label: account.label,
1299
+ cooling: account.cooldownUntilMs > Date.now(),
1300
+ rateLimitHits: account.rateLimitHits
1301
+ })), null, 2));
1302
+ else if (accounts.length === 0) console.log("No imported accounts. Run `import <key>` after signing in on the desktop app.");
1303
+ else console.log(accounts.map((account) => `${account.cooldownUntilMs > Date.now() ? "⏸" : "▶"} ${account.label}`).join("\n"));
1304
+ return 0;
1305
+ }
1306
+ /** Entry point; returns the process exit code. */
1307
+ async function main(argv) {
1308
+ const [command, ...rest] = argv;
1309
+ switch (command) {
1310
+ case void 0:
1311
+ case "help":
1312
+ case "--help":
1313
+ case "-h":
1314
+ console.log(usage());
1315
+ return 0;
1316
+ case "status": return commandStatus(rest);
1317
+ case "doctor": return commandDoctor();
1318
+ case "accounts": return commandAccounts(rest);
1319
+ case "import": return commandImport(rest);
1320
+ case "remove": return commandRemove(rest);
1321
+ case "login": return commandLogin();
1322
+ case "logout": return commandRemove(["default", ...rest]);
1323
+ case "reset": return commandReset();
1324
+ case "checkin": return commandCheckin(rest);
1325
+ default:
1326
+ console.error(`unknown command: ${command}\n\n${usage()}`);
1327
+ return 2;
1328
+ }
1329
+ }
1330
+ main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
1331
+ console.error(error);
1332
+ process.exit(1);
1333
+ });
1334
+ //#endregion
1335
+ export { main, writeFile };