dsh-workbuddy-xdpool 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +285 -0
- package/lib/bin.js +441 -51
- package/lib/client.js +764 -425
- package/lib/index.d.ts +209 -4
- package/lib/index.js +571 -83
- package/package.json +3 -3
package/lib/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir, rename, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { execFile, execFileSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -427,6 +427,7 @@ function parseUpstreamModel(value) {
|
|
|
427
427
|
const reasoning = parseReasoning(raw["reasoning"]);
|
|
428
428
|
const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
|
|
429
429
|
const supportsImages = typeof raw["supportsImages"] === "boolean" ? raw["supportsImages"] : void 0;
|
|
430
|
+
const tags = Array.isArray(raw["tags"]) ? raw["tags"].filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
430
431
|
return {
|
|
431
432
|
id,
|
|
432
433
|
name,
|
|
@@ -437,7 +438,8 @@ function parseUpstreamModel(value) {
|
|
|
437
438
|
...descriptionZh === void 0 ? {} : { descriptionZh },
|
|
438
439
|
...descriptionEn === void 0 ? {} : { descriptionEn },
|
|
439
440
|
...supportsToolCall === void 0 ? {} : { supportsToolCall },
|
|
440
|
-
...supportsImages === void 0 ? {} : { supportsImages }
|
|
441
|
+
...supportsImages === void 0 ? {} : { supportsImages },
|
|
442
|
+
...tags === void 0 || tags.length === 0 ? {} : { tags }
|
|
441
443
|
};
|
|
442
444
|
}
|
|
443
445
|
/**
|
|
@@ -1640,8 +1642,20 @@ const AAD_DOMAIN = Buffer.from("WB-AAD\0", "ascii");
|
|
|
1640
1642
|
const SYMMETRIC_SCHEME = "sym-v1";
|
|
1641
1643
|
/** Env override pointing at the WorkBuddy desktop executable. */
|
|
1642
1644
|
const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
|
|
1643
|
-
/**
|
|
1644
|
-
|
|
1645
|
+
/**
|
|
1646
|
+
* How long the app is given to answer with its key payload.
|
|
1647
|
+
*
|
|
1648
|
+
* 30s, not 10s: the child is the WorkBuddy Electron binary running as plain
|
|
1649
|
+
* Node, and its FIRST spawn on a cold machine costs several seconds on its own
|
|
1650
|
+
* (measured 4.5s here) before the endpoint security stack has warmed its scan
|
|
1651
|
+
* cache. Under load — a concurrent `pnpm install` from the market, a running
|
|
1652
|
+
* full-disk scan — that first spawn crosses a 10s budget, the fetch rejects,
|
|
1653
|
+
* `readAtRestKey` returns undefined, and every encrypted credential then reads
|
|
1654
|
+
* as `WorkBuddyEncryptedCredentialError` until the 60s negative cache expires.
|
|
1655
|
+
* A successful fetch is cached for the process lifetime, so the longer budget
|
|
1656
|
+
* is only ever paid once per process, and only when the app is present but slow.
|
|
1657
|
+
*/
|
|
1658
|
+
const KEY_FETCH_TIMEOUT_MS = 3e4;
|
|
1645
1659
|
/**
|
|
1646
1660
|
* Executable file names the desktop app ships under, in probe order.
|
|
1647
1661
|
*
|
|
@@ -1896,6 +1910,7 @@ function windowsFallbackAppPaths(env) {
|
|
|
1896
1910
|
}
|
|
1897
1911
|
roots.add(join(drive, "Program Files"));
|
|
1898
1912
|
roots.add(join(drive, "Program Files (x86)"));
|
|
1913
|
+
roots.add(drive);
|
|
1899
1914
|
}
|
|
1900
1915
|
for (const root of roots) for (const name of APP_EXECUTABLE_NAMES) {
|
|
1901
1916
|
out.push(join(root, "WorkBuddy", name));
|
|
@@ -2308,6 +2323,41 @@ async function readCredential(path) {
|
|
|
2308
2323
|
}
|
|
2309
2324
|
}
|
|
2310
2325
|
/**
|
|
2326
|
+
* The pool id a document would receive, WITHOUT decrypting anything.
|
|
2327
|
+
*
|
|
2328
|
+
* Used to honour the ignore list before the at-rest key lookup runs: `uin` and
|
|
2329
|
+
* `uid` are read as plain strings by {@link parseWorkBuddyAuth}, while
|
|
2330
|
+
* `nickname` is commonly encrypted — so this returns undefined for an account
|
|
2331
|
+
* whose identity lives only in the encrypted nickname, and the caller falls
|
|
2332
|
+
* back to the full parse for those.
|
|
2333
|
+
*/
|
|
2334
|
+
function cheapIdentityId(text) {
|
|
2335
|
+
let parsed;
|
|
2336
|
+
try {
|
|
2337
|
+
parsed = JSON.parse(text);
|
|
2338
|
+
} catch {
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2342
|
+
const document = parsed;
|
|
2343
|
+
const identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : document;
|
|
2344
|
+
const uin = typeof identity["uin"] === "string" && identity["uin"] !== "" ? identity["uin"] : void 0;
|
|
2345
|
+
const uid = typeof identity["uid"] === "string" && identity["uid"] !== "" ? identity["uid"] : void 0;
|
|
2346
|
+
if (uin === void 0 && uid === void 0) return void 0;
|
|
2347
|
+
return workbuddyAccountId({
|
|
2348
|
+
...uin === void 0 ? {} : { uin },
|
|
2349
|
+
...uid === void 0 ? {} : { uid }
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
/** {@link cheapIdentityId} for a file path; undefined when it cannot be read. */
|
|
2353
|
+
async function cheapIdentityIdFromFile(path) {
|
|
2354
|
+
try {
|
|
2355
|
+
return cheapIdentityId(await readFile(path, "utf8"));
|
|
2356
|
+
} catch {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2311
2361
|
* Build the field opener, or undefined when the app cannot supply its key.
|
|
2312
2362
|
*
|
|
2313
2363
|
* Split out so the key lookup is testable without a real desktop install, and so
|
|
@@ -2386,6 +2436,21 @@ var WorkBuddyAccountPool = class {
|
|
|
2386
2436
|
*/
|
|
2387
2437
|
disabledIds = /* @__PURE__ */ new Set();
|
|
2388
2438
|
/**
|
|
2439
|
+
* Account ids the user threw out of the pool for good.
|
|
2440
|
+
*
|
|
2441
|
+
* Enforced BEFORE the credential is parsed: `scan()` skips a file whose
|
|
2442
|
+
* identity is already ignored, so an ignored account costs no at-rest key
|
|
2443
|
+
* lookup (which spawns the desktop app on 5.6.0+) and cannot re-enter the pool
|
|
2444
|
+
* when the app writes a fresh sign-in for it. That is the difference from
|
|
2445
|
+
* {@link disabledIds}, which only filters at pick time and leaves the account
|
|
2446
|
+
* listed, readable and re-discoverable.
|
|
2447
|
+
*
|
|
2448
|
+
* The set is supplied by the host from the plugin's own ignore file, and is
|
|
2449
|
+
* replaced wholesale on every {@link applyIgnored} so removing an entry takes
|
|
2450
|
+
* effect on the next scan without a restart.
|
|
2451
|
+
*/
|
|
2452
|
+
ignoredIds = /* @__PURE__ */ new Set();
|
|
2453
|
+
/**
|
|
2389
2454
|
* Per-account credit floor, keyed by account id. 0 (or absent) means "spend
|
|
2390
2455
|
* it all".
|
|
2391
2456
|
*
|
|
@@ -2437,12 +2502,39 @@ var WorkBuddyAccountPool = class {
|
|
|
2437
2502
|
if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
|
|
2438
2503
|
if (options.creditReserves !== void 0) this.setCreditReserves(options.creditReserves);
|
|
2439
2504
|
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Replace the permanent ignore list.
|
|
2507
|
+
*
|
|
2508
|
+
* Also drops any already-discovered account that is now ignored, so the change
|
|
2509
|
+
* is visible without waiting for the next scan: the card refreshes its status
|
|
2510
|
+
* document right after the write, and an account still sitting in `accounts`
|
|
2511
|
+
* would keep showing up there.
|
|
2512
|
+
*/
|
|
2513
|
+
applyIgnored(ids) {
|
|
2514
|
+
this.ignoredIds = new Set(ids);
|
|
2515
|
+
if (this.ignoredIds.size === 0) return;
|
|
2516
|
+
this.accounts = this.accounts.filter((account) => !this.ignoredIds.has(account.id));
|
|
2517
|
+
}
|
|
2518
|
+
/** Whether this account has been thrown out of the pool for good. */
|
|
2519
|
+
isIgnored(accountId) {
|
|
2520
|
+
return this.ignoredIds.has(accountId);
|
|
2521
|
+
}
|
|
2522
|
+
/** Every ignored id currently in force, in insertion order. */
|
|
2523
|
+
ignoredIdsInOrder() {
|
|
2524
|
+
return [...this.ignoredIds];
|
|
2525
|
+
}
|
|
2440
2526
|
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
2441
2527
|
async scan() {
|
|
2442
2528
|
const found = [];
|
|
2443
2529
|
for (const dir of this.authDirs) for (const file of await authFilesIn(dir)) {
|
|
2530
|
+
if (this.ignoredIds.size > 0) {
|
|
2531
|
+
const cheapId = await cheapIdentityIdFromFile(file);
|
|
2532
|
+
if (cheapId !== void 0 && this.ignoredIds.has(cheapId)) continue;
|
|
2533
|
+
}
|
|
2444
2534
|
const credential = await readCredential(file);
|
|
2445
|
-
if (credential
|
|
2535
|
+
if (credential === void 0) continue;
|
|
2536
|
+
if (this.ignoredIds.size > 0 && this.ignoredIds.has(workbuddyAccountId(credential))) continue;
|
|
2537
|
+
found.push(credential);
|
|
2446
2538
|
}
|
|
2447
2539
|
const byId = /* @__PURE__ */ new Map();
|
|
2448
2540
|
for (const account of this.accounts) byId.set(account.id, account);
|
|
@@ -2783,28 +2875,44 @@ ${new Date(until).toISOString()}
|
|
|
2783
2875
|
};
|
|
2784
2876
|
//#endregion
|
|
2785
2877
|
//#region src/catalog.ts
|
|
2786
|
-
/**
|
|
2878
|
+
/**
|
|
2879
|
+
* Static fallback used before the first live catalog fetch, and whenever the
|
|
2880
|
+
* upstream cannot be reached.
|
|
2881
|
+
*
|
|
2882
|
+
* The multipliers are carried on purpose. Without them the provider's model
|
|
2883
|
+
* picker silently loses every rate and every free badge the moment the live
|
|
2884
|
+
* fetch fails — which reads to the user as "the plugin broke my model list"
|
|
2885
|
+
* rather than "the upstream is unreachable". The values are the ones the two
|
|
2886
|
+
* gateways actually advertise for these ids (`credits: "x0.79 credits"` and so
|
|
2887
|
+
* on), so a fallback row looks the same as a live one.
|
|
2888
|
+
*
|
|
2889
|
+
* `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
|
|
2890
|
+
* which is what turns on the free badge.
|
|
2891
|
+
*/
|
|
2787
2892
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
2788
2893
|
{
|
|
2789
2894
|
id: "glm-5.3",
|
|
2790
2895
|
name: "GLM-5.3",
|
|
2791
2896
|
contextWindow: 2e5,
|
|
2792
2897
|
maxOutputTokens: 128e3,
|
|
2793
|
-
supportsImages: true
|
|
2898
|
+
supportsImages: true,
|
|
2899
|
+
multiplier: .79
|
|
2794
2900
|
},
|
|
2795
2901
|
{
|
|
2796
2902
|
id: "glm-5.3-flash",
|
|
2797
2903
|
name: "GLM-5.3-Flash",
|
|
2798
2904
|
contextWindow: 2e5,
|
|
2799
2905
|
maxOutputTokens: 128e3,
|
|
2800
|
-
supportsImages: true
|
|
2906
|
+
supportsImages: true,
|
|
2907
|
+
multiplier: .06
|
|
2801
2908
|
},
|
|
2802
2909
|
{
|
|
2803
2910
|
id: "glm-5.2",
|
|
2804
2911
|
name: "GLM-5.2",
|
|
2805
2912
|
contextWindow: 2e5,
|
|
2806
2913
|
maxOutputTokens: 128e3,
|
|
2807
|
-
supportsImages: true
|
|
2914
|
+
supportsImages: true,
|
|
2915
|
+
multiplier: .79
|
|
2808
2916
|
},
|
|
2809
2917
|
{
|
|
2810
2918
|
id: "glm-5.1",
|
|
@@ -2832,7 +2940,8 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2832
2940
|
name: "Kimi-K3",
|
|
2833
2941
|
contextWindow: 2e5,
|
|
2834
2942
|
maxOutputTokens: 128e3,
|
|
2835
|
-
supportsImages: true
|
|
2943
|
+
supportsImages: true,
|
|
2944
|
+
multiplier: 1.62
|
|
2836
2945
|
},
|
|
2837
2946
|
{
|
|
2838
2947
|
id: "minimax-m3",
|
|
@@ -2846,14 +2955,16 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2846
2955
|
name: "Hy3",
|
|
2847
2956
|
contextWindow: 32e3,
|
|
2848
2957
|
maxOutputTokens: 8e3,
|
|
2849
|
-
supportsImages: true
|
|
2958
|
+
supportsImages: true,
|
|
2959
|
+
multiplier: 0
|
|
2850
2960
|
},
|
|
2851
2961
|
{
|
|
2852
2962
|
id: "hy4-preview",
|
|
2853
2963
|
name: "Hy4-Preview",
|
|
2854
2964
|
contextWindow: 1e6,
|
|
2855
2965
|
maxOutputTokens: 128e3,
|
|
2856
|
-
supportsImages: true
|
|
2966
|
+
supportsImages: true,
|
|
2967
|
+
multiplier: .29
|
|
2857
2968
|
}
|
|
2858
2969
|
];
|
|
2859
2970
|
/** Live catalog with a static fallback behind it. */
|
|
@@ -2931,7 +3042,8 @@ function toModelInfo(model) {
|
|
|
2931
3042
|
maxOutputTokens: model.maxTokens,
|
|
2932
3043
|
supportsImages: model.supportsImages ?? false,
|
|
2933
3044
|
...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
|
|
2934
|
-
...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
|
|
3045
|
+
...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts },
|
|
3046
|
+
...model.tags === void 0 || model.tags.length === 0 ? {} : { tags: model.tags }
|
|
2935
3047
|
};
|
|
2936
3048
|
}
|
|
2937
3049
|
/** Map the live upstream list, falling back to the static list when empty. */
|
|
@@ -3345,6 +3457,30 @@ function expertChatEvents(expert, conversationId, requestId) {
|
|
|
3345
3457
|
expert_industry_id: ""
|
|
3346
3458
|
} : event);
|
|
3347
3459
|
}
|
|
3460
|
+
//#endregion
|
|
3461
|
+
//#region src/status-paths.ts
|
|
3462
|
+
/**
|
|
3463
|
+
* The schedule every automation job falls back to.
|
|
3464
|
+
*
|
|
3465
|
+
* Shared by both halves on purpose. The host uses it when a configured hour
|
|
3466
|
+
* list arrives empty (the settings schema materializes "never configured" into
|
|
3467
|
+
* `[]`), and the card uses it when it writes the `automation` block back, so a
|
|
3468
|
+
* document that already holds an empty list is healed instead of being saved
|
|
3469
|
+
* back as an unrunnable schedule.
|
|
3470
|
+
*
|
|
3471
|
+
* This lives here rather than in `scheduler.ts` because the browser half cannot
|
|
3472
|
+
* import the host module: `scheduler.ts` pulls in `node:crypto` and the whole
|
|
3473
|
+
* upstream client, none of which exists in the browser bundle. Two hand-written
|
|
3474
|
+
* copies would drift, and the drift is invisible — the card would write a
|
|
3475
|
+
* schedule the scheduler does not run.
|
|
3476
|
+
*/
|
|
3477
|
+
const DEFAULT_AUTOMATION_HOURS = {
|
|
3478
|
+
checkin: [9],
|
|
3479
|
+
report: [10],
|
|
3480
|
+
tasks: [11],
|
|
3481
|
+
streak: [12],
|
|
3482
|
+
travel: [9, 21]
|
|
3483
|
+
};
|
|
3348
3484
|
/**
|
|
3349
3485
|
* The fallback record for the 腾讯轻量云 expert, used when the marketplace
|
|
3350
3486
|
* listing cannot be read. The id is the one the task is scored against.
|
|
@@ -3388,6 +3524,23 @@ const JOB_KINDS = [
|
|
|
3388
3524
|
"streak",
|
|
3389
3525
|
"travel"
|
|
3390
3526
|
];
|
|
3527
|
+
/**
|
|
3528
|
+
* Pick the hour list to run on: the configured one, or the default.
|
|
3529
|
+
*
|
|
3530
|
+
* An EMPTY list is treated as "not configured" rather than as "never run".
|
|
3531
|
+
* That distinction is the whole point: the settings schema materializes an
|
|
3532
|
+
* absent list into `[]` (a bare `z.array` with no `.default()`), so a config
|
|
3533
|
+
* that never mentioned the schedule arrives here looking exactly like one
|
|
3534
|
+
* deliberately set to nothing — and honouring the empty reading silently
|
|
3535
|
+
* disabled every job while the card still showed the switch as ON.
|
|
3536
|
+
*
|
|
3537
|
+
* A user who genuinely wants a job skipped can leave it out of the schedule
|
|
3538
|
+
* they save; they cannot express "off" with an empty array through the card
|
|
3539
|
+
* either way, because the card always writes a non-empty list.
|
|
3540
|
+
*/
|
|
3541
|
+
function hoursOrDefault(configured, fallback) {
|
|
3542
|
+
return configured !== void 0 && configured.length > 0 ? configured : fallback;
|
|
3543
|
+
}
|
|
3391
3544
|
const EMPTY_JOB_STATE = {
|
|
3392
3545
|
ok: 0,
|
|
3393
3546
|
failed: 0,
|
|
@@ -3563,11 +3716,11 @@ var WorkBuddyScheduler = class {
|
|
|
3563
3716
|
this.earningsDate = today;
|
|
3564
3717
|
}
|
|
3565
3718
|
this.enabled = options.enabled ?? false;
|
|
3566
|
-
this.checkinHours = options.checkinHours
|
|
3567
|
-
this.reportHours = options.reportHours
|
|
3568
|
-
this.taskHours = options.taskHours
|
|
3569
|
-
this.streakHours = options.streakHours
|
|
3570
|
-
this.travelHours = options.travelHours
|
|
3719
|
+
this.checkinHours = hoursOrDefault(options.checkinHours, DEFAULT_AUTOMATION_HOURS.checkin);
|
|
3720
|
+
this.reportHours = hoursOrDefault(options.reportHours, DEFAULT_AUTOMATION_HOURS.report);
|
|
3721
|
+
this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
3722
|
+
this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
3723
|
+
this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
3571
3724
|
}
|
|
3572
3725
|
/** Apply a new configuration; safe to call while running. */
|
|
3573
3726
|
/**
|
|
@@ -3595,11 +3748,11 @@ var WorkBuddyScheduler = class {
|
|
|
3595
3748
|
}
|
|
3596
3749
|
applyConfig(options) {
|
|
3597
3750
|
if (options.enabled !== void 0) this.enabled = options.enabled;
|
|
3598
|
-
if (options.checkinHours !== void 0) this.checkinHours = options.checkinHours;
|
|
3599
|
-
if (options.reportHours !== void 0) this.reportHours = options.reportHours;
|
|
3600
|
-
if (options.taskHours !== void 0) this.taskHours = options.taskHours;
|
|
3601
|
-
if (options.streakHours !== void 0) this.streakHours = options.streakHours;
|
|
3602
|
-
if (options.travelHours !== void 0) this.travelHours = options.travelHours;
|
|
3751
|
+
if (options.checkinHours !== void 0) this.checkinHours = hoursOrDefault(options.checkinHours, DEFAULT_AUTOMATION_HOURS.checkin);
|
|
3752
|
+
if (options.reportHours !== void 0) this.reportHours = hoursOrDefault(options.reportHours, DEFAULT_AUTOMATION_HOURS.report);
|
|
3753
|
+
if (options.taskHours !== void 0) this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
3754
|
+
if (options.streakHours !== void 0) this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
3755
|
+
if (options.travelHours !== void 0) this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
3603
3756
|
}
|
|
3604
3757
|
/** Hours for one job, used by the loop and the status document. */
|
|
3605
3758
|
hoursOf(kind) {
|
|
@@ -3830,12 +3983,16 @@ var WorkBuddyScheduler = class {
|
|
|
3830
3983
|
*/
|
|
3831
3984
|
persistEarnings() {
|
|
3832
3985
|
if (this.saveEarningsFn === void 0) return;
|
|
3986
|
+
const accounts = {};
|
|
3987
|
+
for (const [id, entry] of this.earnings) accounts[id] = entry;
|
|
3988
|
+
const ledger = {
|
|
3989
|
+
date: this.earningsDate,
|
|
3990
|
+
accounts
|
|
3991
|
+
};
|
|
3833
3992
|
try {
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
date: this.earningsDate,
|
|
3838
|
-
accounts
|
|
3993
|
+
const saved = this.saveEarningsFn(ledger);
|
|
3994
|
+
if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
|
|
3995
|
+
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
3839
3996
|
});
|
|
3840
3997
|
} catch (error) {
|
|
3841
3998
|
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
@@ -4258,6 +4415,137 @@ function formatRates(status) {
|
|
|
4258
4415
|
return lines.join("\n");
|
|
4259
4416
|
}
|
|
4260
4417
|
//#endregion
|
|
4418
|
+
//#region src/ignored.ts
|
|
4419
|
+
/**
|
|
4420
|
+
* The permanent ignore list: accounts the user has thrown out of the pool.
|
|
4421
|
+
*
|
|
4422
|
+
* Distinct from `disabledAccountIds`, which is a ROTATION preference. A disabled
|
|
4423
|
+
* account stays on the card, can be switched back on, and its credential keeps
|
|
4424
|
+
* being read on every scan. Ignoring is a statement about the ACCOUNT — "this
|
|
4425
|
+
* one is not mine any more" — so an ignored account is skipped BEFORE its
|
|
4426
|
+
* credential is decrypted, never appears among the pool's accounts, and does not
|
|
4427
|
+
* come back when the desktop app writes a fresh `.info` file for it.
|
|
4428
|
+
*
|
|
4429
|
+
* It lives in a plugin-owned file rather than in the settings document for one
|
|
4430
|
+
* practical reason: the CLI has no settings service. `dsh-workbuddy-xdpool
|
|
4431
|
+
* ignore <id>` runs without a host, so a settings-only list could be written by
|
|
4432
|
+
* the card and never by the CLI. One file, read by both halves, keeps the two
|
|
4433
|
+
* views of "which accounts are ignored" identical by construction.
|
|
4434
|
+
*
|
|
4435
|
+
* @module dsh-workbuddy-xdpool/ignored
|
|
4436
|
+
*/
|
|
4437
|
+
/** Directory holding this plugin's own state (imported snapshots, ignore list). */
|
|
4438
|
+
const PLUGIN_DATA_DIR_NAME = ".workbuddy-xdpool";
|
|
4439
|
+
/** File holding the permanent ignore list, inside {@link pluginDataDir}. */
|
|
4440
|
+
const IGNORED_FILE_NAME = "ignored.json";
|
|
4441
|
+
/**
|
|
4442
|
+
* The DSH home directory, honouring the same override the host uses.
|
|
4443
|
+
*
|
|
4444
|
+
* Shared by the CLI and the host so both halves resolve the same file: an
|
|
4445
|
+
* `ignore` written from the terminal has to be visible to the running plugin,
|
|
4446
|
+
* which is only true if they agree on where "home" is.
|
|
4447
|
+
*/
|
|
4448
|
+
function dshHome$1(env = process.env) {
|
|
4449
|
+
const fromEnv = env["DSH_HOME"];
|
|
4450
|
+
if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv.trim();
|
|
4451
|
+
return join(homedir(), ".dsh");
|
|
4452
|
+
}
|
|
4453
|
+
/** This plugin's own state directory. */
|
|
4454
|
+
function pluginDataDir(env = process.env) {
|
|
4455
|
+
return join(dshHome$1(env), PLUGIN_DATA_DIR_NAME);
|
|
4456
|
+
}
|
|
4457
|
+
/** Absolute path of the ignore list. */
|
|
4458
|
+
function ignoredIdsPath(env = process.env) {
|
|
4459
|
+
return join(pluginDataDir(env), IGNORED_FILE_NAME);
|
|
4460
|
+
}
|
|
4461
|
+
/**
|
|
4462
|
+
* Normalize whatever the file holds into a clean list.
|
|
4463
|
+
*
|
|
4464
|
+
* Tolerant on purpose: this file is hand-editable and a malformed entry must not
|
|
4465
|
+
* take the pool down. An entry without a usable id is dropped; a missing label
|
|
4466
|
+
* falls back to the id so the card always has something to print.
|
|
4467
|
+
*/
|
|
4468
|
+
function normalize(raw) {
|
|
4469
|
+
if (typeof raw !== "object" || raw === null) return [];
|
|
4470
|
+
const entries = raw.accounts;
|
|
4471
|
+
if (!Array.isArray(entries)) return [];
|
|
4472
|
+
const out = [];
|
|
4473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4474
|
+
for (const entry of entries) {
|
|
4475
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
4476
|
+
const record = entry;
|
|
4477
|
+
const id = typeof record["id"] === "string" ? record["id"].trim() : "";
|
|
4478
|
+
if (id === "" || seen.has(id)) continue;
|
|
4479
|
+
seen.add(id);
|
|
4480
|
+
const label = typeof record["label"] === "string" && record["label"].trim() !== "" ? record["label"] : id;
|
|
4481
|
+
const ignoredAt = typeof record["ignoredAt"] === "string" ? record["ignoredAt"] : "";
|
|
4482
|
+
out.push({
|
|
4483
|
+
id,
|
|
4484
|
+
label,
|
|
4485
|
+
ignoredAt
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
return out;
|
|
4489
|
+
}
|
|
4490
|
+
/**
|
|
4491
|
+
* Read the ignore list, tolerating every "no list yet" shape.
|
|
4492
|
+
*
|
|
4493
|
+
* A missing file, unreadable file, or invalid JSON all mean the same thing to
|
|
4494
|
+
* the caller — nothing is ignored — so none of them throws. The pool must be
|
|
4495
|
+
* able to start on a machine that has never ignored anything.
|
|
4496
|
+
*/
|
|
4497
|
+
async function readIgnoredAccounts(path = ignoredIdsPath()) {
|
|
4498
|
+
try {
|
|
4499
|
+
return normalize(JSON.parse(await readFile(path, "utf8")));
|
|
4500
|
+
} catch {
|
|
4501
|
+
return [];
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* Replace the ignore list, atomically.
|
|
4506
|
+
*
|
|
4507
|
+
* Written to a sibling temp file and renamed over the target so a crash (or a
|
|
4508
|
+
* concurrent reader) can never observe a half-written document — the ignore
|
|
4509
|
+
* list is the only thing standing between a dead account and the rotation, and
|
|
4510
|
+
* a truncated file reads as "nothing is ignored", which would quietly put every
|
|
4511
|
+
* discarded account back in the pool.
|
|
4512
|
+
*/
|
|
4513
|
+
async function writeIgnoredAccounts(accounts, path = ignoredIdsPath()) {
|
|
4514
|
+
const payload = JSON.stringify({
|
|
4515
|
+
version: 1,
|
|
4516
|
+
accounts
|
|
4517
|
+
}, null, 2);
|
|
4518
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4519
|
+
const temp = `${path}.tmp`;
|
|
4520
|
+
await writeFile$1(temp, `${payload}\n`, "utf8");
|
|
4521
|
+
await rename(temp, path);
|
|
4522
|
+
}
|
|
4523
|
+
/**
|
|
4524
|
+
* Add one account to the ignore list, preserving the rest.
|
|
4525
|
+
*
|
|
4526
|
+
* A read-modify-write rather than a wholesale replace: the card and the CLI can
|
|
4527
|
+
* both be open, and each request names exactly one account, so re-writing the
|
|
4528
|
+
* whole list from a stale view would drop the other side's edits.
|
|
4529
|
+
*/
|
|
4530
|
+
async function ignoreAccount(account, path = ignoredIdsPath()) {
|
|
4531
|
+
const current = await readIgnoredAccounts(path);
|
|
4532
|
+
if (current.some((entry) => entry.id === account.id)) return current;
|
|
4533
|
+
const next = [...current, {
|
|
4534
|
+
id: account.id,
|
|
4535
|
+
label: account.label ?? account.id,
|
|
4536
|
+
ignoredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4537
|
+
}];
|
|
4538
|
+
await writeIgnoredAccounts(next, path);
|
|
4539
|
+
return next;
|
|
4540
|
+
}
|
|
4541
|
+
/** Drop one account from the ignore list. Returns the resulting list. */
|
|
4542
|
+
async function unignoreAccount(accountId, path = ignoredIdsPath()) {
|
|
4543
|
+
const current = await readIgnoredAccounts(path);
|
|
4544
|
+
const next = current.filter((entry) => entry.id !== accountId);
|
|
4545
|
+
if (next.length !== current.length) await writeIgnoredAccounts(next, path);
|
|
4546
|
+
return next;
|
|
4547
|
+
}
|
|
4548
|
+
//#endregion
|
|
4261
4549
|
//#region src/index.ts
|
|
4262
4550
|
/**
|
|
4263
4551
|
* One region's model-selection schema.
|
|
@@ -4267,6 +4555,18 @@ function formatRates(status) {
|
|
|
4267
4555
|
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
4268
4556
|
* fall back to the legacy flat keys.
|
|
4269
4557
|
*/
|
|
4558
|
+
function asVolatile(schema) {
|
|
4559
|
+
const candidate = schema;
|
|
4560
|
+
return typeof candidate.volatile === "function" ? candidate.volatile() : schema;
|
|
4561
|
+
}
|
|
4562
|
+
/**
|
|
4563
|
+
* One region's model-selection schema.
|
|
4564
|
+
*
|
|
4565
|
+
* Every field is optional on purpose: an absent field keeps its documented
|
|
4566
|
+
* meaning ("all enabled" / "follow the upstream image flag" / "no cap"), and a
|
|
4567
|
+
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
4568
|
+
* fall back to the legacy flat keys.
|
|
4569
|
+
*/
|
|
4270
4570
|
const modelSelectionSchema = z.object({
|
|
4271
4571
|
enabledModelIds: z.array(z.string()).description("Model ids enabled in this region's picker (absent = all)"),
|
|
4272
4572
|
imageModelIds: z.array(z.string()).description("Model ids accepting image input in this region (absent = follow upstream)"),
|
|
@@ -4294,22 +4594,22 @@ const automationSchema = z.object({
|
|
|
4294
4594
|
exhaustCooldownMs: z.number().step(1).min(1e3).description("How long a spent account rests, in milliseconds")
|
|
4295
4595
|
});
|
|
4296
4596
|
z.object({
|
|
4297
|
-
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
4298
|
-
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
4299
|
-
distribution: z.union([
|
|
4597
|
+
authFile: asVolatile(z.string().description("WorkBuddy desktop auth file (defaults to the app own location)")),
|
|
4598
|
+
cooldownMs: asVolatile(z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds")),
|
|
4599
|
+
distribution: asVolatile(z.union([
|
|
4300
4600
|
"priority",
|
|
4301
4601
|
"round-robin",
|
|
4302
4602
|
"balanced"
|
|
4303
|
-
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)"),
|
|
4304
|
-
disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
|
|
4305
|
-
creditReserves: z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value"),
|
|
4306
|
-
enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
|
|
4307
|
-
imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
|
|
4308
|
-
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
|
|
4309
|
-
modelSelectionCn: modelSelectionSchema.description("Model selection for the domestic gateway"),
|
|
4310
|
-
modelSelectionGlobal: modelSelectionSchema.description("Model selection for the international gateway"),
|
|
4311
|
-
automation: automationSchema.description("Daily points automation (activity report, task claiming, check-in)"),
|
|
4312
|
-
automationEarnings: z.any().description("Automation earnings ledger (written by the scheduler)")
|
|
4603
|
+
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)")),
|
|
4604
|
+
disabledAccountIds: asVolatile(z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)")),
|
|
4605
|
+
creditReserves: asVolatile(z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value")),
|
|
4606
|
+
enabledModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet")),
|
|
4607
|
+
imageModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet")),
|
|
4608
|
+
contextBudgets: asVolatile(z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet")),
|
|
4609
|
+
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
4610
|
+
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
4611
|
+
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
4612
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
4313
4613
|
});
|
|
4314
4614
|
/** Assemble the runtime objects without registering anything. */
|
|
4315
4615
|
/**
|
|
@@ -4347,6 +4647,20 @@ function createCore(logger) {
|
|
|
4347
4647
|
*/
|
|
4348
4648
|
/** Directory holding imported account snapshots. */
|
|
4349
4649
|
const ACCOUNT_DIR_NAME = ".workbuddy-xdpool";
|
|
4650
|
+
/**
|
|
4651
|
+
* Assemble the runtime for a CLI command, with the ignore list applied.
|
|
4652
|
+
*
|
|
4653
|
+
* Every command goes through here rather than calling `createCore()` directly,
|
|
4654
|
+
* so a command can never accidentally act on an account the user has thrown out
|
|
4655
|
+
* — `checkin all` collecting a reward for a discarded account would be exactly
|
|
4656
|
+
* the kind of silent surprise the ignore feature exists to prevent.
|
|
4657
|
+
*/
|
|
4658
|
+
async function cliCore() {
|
|
4659
|
+
const core = createCore();
|
|
4660
|
+
const ignored = await readIgnoredAccounts();
|
|
4661
|
+
core.pool.applyIgnored(ignored.map((entry) => entry.id));
|
|
4662
|
+
return core;
|
|
4663
|
+
}
|
|
4350
4664
|
/** Snapshot files are named by the md5 prefix of their key, so any key is safe. */
|
|
4351
4665
|
function snapshotPath(key, dir) {
|
|
4352
4666
|
return join(dir, `${createHash("md5").update(key).digest("hex").slice(0, 8)}.json`);
|
|
@@ -4371,6 +4685,9 @@ function usage() {
|
|
|
4371
4685
|
" accounts List discovered accounts (add --json)",
|
|
4372
4686
|
" import <key> Snapshot the current desktop login as <key> (add --force)",
|
|
4373
4687
|
" remove <key> Delete one imported snapshot",
|
|
4688
|
+
" ignore <acct> Drop an account from the pool for good (id or label)",
|
|
4689
|
+
" unignore <acct> Put an ignored account back into the pool",
|
|
4690
|
+
" ignored List the accounts dropped from the pool (add --json)",
|
|
4374
4691
|
" login Guide for adding another account (desktop app is single-sign-in)",
|
|
4375
4692
|
" checkin [all|<acct>] Daily check-in: report status, or collect with `all` / a label",
|
|
4376
4693
|
" reset Clear all rate-limit cooldowns immediately",
|
|
@@ -4399,7 +4716,7 @@ async function commandStatus(args) {
|
|
|
4399
4716
|
const asJson = args.includes("--json");
|
|
4400
4717
|
const withCredits = args.includes("--credits");
|
|
4401
4718
|
const withRates = args.includes("--rates");
|
|
4402
|
-
const core =
|
|
4719
|
+
const core = await cliCore();
|
|
4403
4720
|
const accounts = await core.pool.scan();
|
|
4404
4721
|
const status = {
|
|
4405
4722
|
ok: accounts.length > 0,
|
|
@@ -4460,7 +4777,7 @@ async function commandStatus(args) {
|
|
|
4460
4777
|
async function commandCheckin(args) {
|
|
4461
4778
|
const asJson = args.includes("--json");
|
|
4462
4779
|
const target = args.find((arg) => !arg.startsWith("--"));
|
|
4463
|
-
const core =
|
|
4780
|
+
const core = await cliCore();
|
|
4464
4781
|
if ((await core.pool.scan()).length === 0) {
|
|
4465
4782
|
console.error("No WorkBuddy account discovered. Sign in with the WorkBuddy desktop app first.");
|
|
4466
4783
|
return 1;
|
|
@@ -4544,7 +4861,7 @@ async function commandDoctor() {
|
|
|
4544
4861
|
lines.push(` ✓ ${dir} → ${snapshots.length} snapshot(s)`);
|
|
4545
4862
|
}
|
|
4546
4863
|
lines.push("");
|
|
4547
|
-
const accounts = await
|
|
4864
|
+
const accounts = await (await cliCore()).pool.scan();
|
|
4548
4865
|
lines.push(`Accounts discovered: ${accounts.length}`);
|
|
4549
4866
|
if (accounts.length === 0) {
|
|
4550
4867
|
healthy = false;
|
|
@@ -4622,6 +4939,66 @@ async function commandRemove(args) {
|
|
|
4622
4939
|
return 1;
|
|
4623
4940
|
}
|
|
4624
4941
|
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Throw one account out of the pool for good, or take it back.
|
|
4944
|
+
*
|
|
4945
|
+
* The target may be a full account id (as `accounts --json` prints it) or any
|
|
4946
|
+
* unambiguous fragment of the label, so the user does not have to copy a hash.
|
|
4947
|
+
* The id is what gets stored — a label can change when the account is renamed,
|
|
4948
|
+
* and an ignore list keyed by a mutable label would silently stop matching.
|
|
4949
|
+
*/
|
|
4950
|
+
async function commandIgnore(args, ignored) {
|
|
4951
|
+
const verb = ignored ? "ignore" : "unignore";
|
|
4952
|
+
const target = args.filter((arg) => !arg.startsWith("--"))[0];
|
|
4953
|
+
if (target === void 0) {
|
|
4954
|
+
console.error(`usage: dsh-workbuddy-xdpool ${verb} <account-id|label>`);
|
|
4955
|
+
return 2;
|
|
4956
|
+
}
|
|
4957
|
+
const accounts = await (await cliCore()).pool.scan();
|
|
4958
|
+
const current = await readIgnoredAccounts();
|
|
4959
|
+
if (!ignored) {
|
|
4960
|
+
const match = current.find((entry) => entry.id === target) ?? current.find((entry) => entry.label === target) ?? current.find((entry) => entry.id.startsWith(target));
|
|
4961
|
+
if (match === void 0) {
|
|
4962
|
+
console.error(`No ignored account matches "${target}". Run \`ignored\` to list them.`);
|
|
4963
|
+
return 1;
|
|
4964
|
+
}
|
|
4965
|
+
await unignoreAccount(match.id);
|
|
4966
|
+
console.log(`Restored ${match.label} (${match.id}). It rejoins the pool on the next scan.`);
|
|
4967
|
+
return 0;
|
|
4968
|
+
}
|
|
4969
|
+
const matches = accounts.filter((account) => account.id === target || account.label === target || account.id.startsWith(target) || account.label.includes(target));
|
|
4970
|
+
if (matches.length === 0) {
|
|
4971
|
+
console.error(`No account matches "${target}". Run \`accounts\` to list them.`);
|
|
4972
|
+
return 1;
|
|
4973
|
+
}
|
|
4974
|
+
if (matches.length > 1) {
|
|
4975
|
+
console.error(`"${target}" matches ${matches.length} accounts; use the full id:\n` + matches.map((account) => ` ${account.id} ${account.label}`).join("\n"));
|
|
4976
|
+
return 1;
|
|
4977
|
+
}
|
|
4978
|
+
const account = matches[0];
|
|
4979
|
+
await ignoreAccount({
|
|
4980
|
+
id: account.id,
|
|
4981
|
+
label: account.label
|
|
4982
|
+
});
|
|
4983
|
+
console.log(`Ignored ${account.label} (${account.id}).\n Its credential is no longer read and it will not rejoin the pool, even if the
|
|
4984
|
+
desktop app signs it in again. Undo with: dsh-workbuddy-xdpool unignore ${account.id}`);
|
|
4985
|
+
return 0;
|
|
4986
|
+
}
|
|
4987
|
+
/** List the accounts currently thrown out of the pool. */
|
|
4988
|
+
async function commandIgnored(args) {
|
|
4989
|
+
const asJson = args.includes("--json");
|
|
4990
|
+
const ignored = await readIgnoredAccounts();
|
|
4991
|
+
if (asJson) {
|
|
4992
|
+
console.log(JSON.stringify(ignored, null, 2));
|
|
4993
|
+
return 0;
|
|
4994
|
+
}
|
|
4995
|
+
if (ignored.length === 0) {
|
|
4996
|
+
console.log("No accounts are ignored. Use `ignore <account-id|label>` to drop one.");
|
|
4997
|
+
return 0;
|
|
4998
|
+
}
|
|
4999
|
+
console.log(ignored.map((entry) => `⛔ ${entry.label} (${entry.id})`).join("\n"));
|
|
5000
|
+
return 0;
|
|
5001
|
+
}
|
|
4625
5002
|
function commandLogin() {
|
|
4626
5003
|
console.log([
|
|
4627
5004
|
"Adding another WorkBuddy account",
|
|
@@ -4641,7 +5018,7 @@ function commandLogin() {
|
|
|
4641
5018
|
return 0;
|
|
4642
5019
|
}
|
|
4643
5020
|
async function commandReset() {
|
|
4644
|
-
const core =
|
|
5021
|
+
const core = await cliCore();
|
|
4645
5022
|
await core.pool.scan();
|
|
4646
5023
|
core.pool.resetCooldowns();
|
|
4647
5024
|
console.log("Cleared all rate-limit cooldowns.");
|
|
@@ -4649,15 +5026,25 @@ async function commandReset() {
|
|
|
4649
5026
|
}
|
|
4650
5027
|
async function commandAccounts(args) {
|
|
4651
5028
|
const asJson = args.includes("--json");
|
|
4652
|
-
const accounts = await
|
|
4653
|
-
|
|
5029
|
+
const accounts = await (await cliCore()).pool.scan();
|
|
5030
|
+
const ignored = await readIgnoredAccounts();
|
|
5031
|
+
if (asJson) console.log(JSON.stringify([...accounts.map((account) => ({
|
|
4654
5032
|
id: account.id,
|
|
4655
5033
|
label: account.label,
|
|
4656
5034
|
cooling: account.cooldownUntilMs > Date.now(),
|
|
4657
5035
|
rateLimitHits: account.rateLimitHits
|
|
4658
|
-
})),
|
|
4659
|
-
|
|
4660
|
-
|
|
5036
|
+
})), ...ignored.map((entry) => ({
|
|
5037
|
+
id: entry.id,
|
|
5038
|
+
label: entry.label,
|
|
5039
|
+
ignored: true,
|
|
5040
|
+
ignoredAt: entry.ignoredAt
|
|
5041
|
+
}))], null, 2));
|
|
5042
|
+
else if (accounts.length === 0 && ignored.length === 0) console.log("No imported accounts. Run `import <key>` after signing in on the desktop app.");
|
|
5043
|
+
else {
|
|
5044
|
+
const lines = accounts.map((account) => `${account.cooldownUntilMs > Date.now() ? "⏸" : "▶"} ${account.label}`);
|
|
5045
|
+
if (ignored.length > 0) lines.push(...ignored.map((entry) => `⛔ ${entry.label} (removed; \`unignore ${entry.id}\` to restore)`));
|
|
5046
|
+
console.log(lines.join("\n"));
|
|
5047
|
+
}
|
|
4661
5048
|
return 0;
|
|
4662
5049
|
}
|
|
4663
5050
|
/** Entry point; returns the process exit code. */
|
|
@@ -4675,6 +5062,9 @@ async function main(argv) {
|
|
|
4675
5062
|
case "accounts": return commandAccounts(rest);
|
|
4676
5063
|
case "import": return commandImport(rest);
|
|
4677
5064
|
case "remove": return commandRemove(rest);
|
|
5065
|
+
case "ignore": return commandIgnore(rest, true);
|
|
5066
|
+
case "unignore": return commandIgnore(rest, false);
|
|
5067
|
+
case "ignored": return commandIgnored(rest);
|
|
4678
5068
|
case "login": return commandLogin();
|
|
4679
5069
|
case "logout": return commandRemove(["default", ...rest]);
|
|
4680
5070
|
case "reset": return commandReset();
|