dsh-connect-workbuddy 2.0.5 → 2.0.10

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as prepareChatBody, C as workbuddyAccountId, D as parseCreditMultiplier, E as classifyUpstreamError, O as parseReasoning, S as parseWorkBuddyAuth, T as WorkBuddyUpstreamClient, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as regionOf, k as parseUpstreamModel, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-BOrNj3pY.js";
1
+ import { A as isCredentialRejectedError, B as fetchAtRestKeyPayload, C as parseWorkBuddyAuth, D as WorkBuddyCredentialRejectedError, E as CREDENTIAL_REJECTED_CODE, F as regionOf, G as workbuddyAppExecutableCandidates, H as isEncryptedFieldWrapper, I as WORKBUDDY_APP_EXECUTABLE_ENV, L as clearAtRestKeyCache, M as parseReasoning, N as parseUpstreamModel, O as WorkBuddyUpstreamClient, P as prepareChatBody, R as deriveAtRestKey, S as legacyWorkbuddyOwnAuthPath, T as workbuddyOwnAuthPath, U as openEncryptedField, V as findWorkbuddyAppExecutable, W as readAtRestKey, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as parseCreditMultiplier, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyAccountId, x as hasEncryptedCredentialFields, y as defaultDesktopAuthDirs, z as deriveAtRestKeyId } from "./host-heartbeat-DPbL9oi2.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createProvider } from "@earendil-works/pi-ai";
@@ -7,6 +7,76 @@ import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
7
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
8
8
  import { createServer } from "node:http";
9
9
  import { Readable } from "node:stream";
10
+ //#region src/credential-recovery.ts
11
+ /** How long a verdict stays fresh by default. */
12
+ const DEFAULT_PROBE_TTL_MS = 6e4;
13
+ /**
14
+ * Cached "is this account still accepted upstream?" probe.
15
+ *
16
+ * The cache key includes the credential's own `lastRefreshAtMs`, so a fresh
17
+ * sign-in (which rewrites the file's issuance time) invalidates a stale verdict
18
+ * automatically — a rejection recorded before the user signed in again cannot
19
+ * keep labelling the account unusable afterwards, and no explicit invalidation
20
+ * is needed.
21
+ */
22
+ function createAccountUsabilityProbe(options) {
23
+ const ttlMs = options.ttlMs ?? DEFAULT_PROBE_TTL_MS;
24
+ const now = options.now ?? Date.now;
25
+ const verdicts = /* @__PURE__ */ new Map();
26
+ return async (region, account) => {
27
+ let credential;
28
+ try {
29
+ credential = await options.store(region).credentialFor(account.id);
30
+ } catch {
31
+ return false;
32
+ }
33
+ if (credential === void 0) return false;
34
+ const key = `${region}\0${account.id}\0${credential.lastRefreshAtMs ?? credential.expiresAtMs}`;
35
+ const cached = verdicts.get(key);
36
+ if (cached !== void 0 && now() - cached.at < ttlMs) return cached.usable;
37
+ let usable = false;
38
+ try {
39
+ await options.client.fetchCheckinStatus(credential);
40
+ usable = true;
41
+ } catch {
42
+ usable = false;
43
+ }
44
+ verdicts.set(key, {
45
+ at: now(),
46
+ usable
47
+ });
48
+ return usable;
49
+ };
50
+ }
51
+ /**
52
+ * Decide what to tell the user after the upstream refused the selected
53
+ * credential: switch to a verified-usable account, or sign in again.
54
+ *
55
+ * The two answers are mutually exclusive by construction. `reloginRequired` is
56
+ * set ONLY when there is no other local account to switch to — never as a
57
+ * fallback for "the probe did not verify anything", because that would repeat
58
+ * the misdirection this module exists to remove. When other accounts exist but
59
+ * none was verified, both fields stay empty and the card says so honestly.
60
+ */
61
+ async function resolveCredentialRecovery(options) {
62
+ let accounts;
63
+ try {
64
+ accounts = await options.store.accounts();
65
+ } catch {
66
+ return { reloginRequired: false };
67
+ }
68
+ const others = accounts.filter((account) => account.id !== options.rejectedAccountId);
69
+ if (others.length === 0) return { reloginRequired: true };
70
+ for (const account of others) if (await options.probe(options.region, account)) return {
71
+ usableAccount: {
72
+ accountId: account.id,
73
+ accountName: account.accountName
74
+ },
75
+ reloginRequired: false
76
+ };
77
+ return { reloginRequired: false };
78
+ }
79
+ //#endregion
10
80
  //#region src/adapter.ts
11
81
  /**
12
82
  * The WorkBuddy pi-ai providers: loopback-backed adapters registered
@@ -467,6 +537,69 @@ function regionOfStatusUrl(url) {
467
537
  return WORKBUDDY_REGIONS.includes(value) ? value : void 0;
468
538
  }
469
539
  /**
540
+ * Narrow a settings value to the `regions` map. Accepts EITHER the whole
541
+ * settings section (the Host's resolved `Config`) OR the `regions` map itself,
542
+ * and unwraps the former. This tolerance is deliberate: passing the whole
543
+ * section where the map was expected was a real shipped bug in the sibling
544
+ * project — the lookup then read `section['cn']` (absent), so the card's
545
+ * checkbox reported `true` forever and clicking it appeared to do nothing even
546
+ * though the write succeeded.
547
+ */
548
+ function regionsMapOf(value) {
549
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
550
+ const record = value;
551
+ const nested = record["regions"];
552
+ if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
553
+ return record;
554
+ }
555
+ /** One region's stored slot as a plain object; any other shape reads as empty. */
556
+ function regionSlotOf(value, region) {
557
+ const slot = regionsMapOf(value)[region];
558
+ return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
559
+ }
560
+ /**
561
+ * Whether one region's provider is switched on. Opt-out semantics: only an
562
+ * explicit `false` disables it, so a config written before this switch existed
563
+ * (and the pre-region-split flat fields, which never carry `enabled`) keep both
564
+ * providers running exactly as before. The Host reads the same rule through
565
+ * `regionStateOf`, so card and Host can never disagree about a region's state.
566
+ *
567
+ * `value` may be the whole settings section or the `regions` map (see
568
+ * {@link regionsMapOf}).
569
+ */
570
+ function regionEnabledOf(value, region) {
571
+ return regionSlotOf(value, region)["enabled"] !== false;
572
+ }
573
+ /** Build the next `regions` settings value for a signed-in tab's save. */
574
+ function nextRegionSlots(regions, region, slot) {
575
+ return {
576
+ ...typeof regions === "object" && regions !== null && !Array.isArray(regions) ? regions : {},
577
+ [region]: slot
578
+ };
579
+ }
580
+ /**
581
+ * Build the next `regions` settings value for a provider on/off toggle. ONLY
582
+ * the target region's `enabled` flag changes: every other field of that slot
583
+ * (its directory, selection, image opt-ins, context budgets) and every other
584
+ * region's slot are carried over verbatim, so switching a provider off never
585
+ * discards the user's model picks and switching it back on restores them.
586
+ *
587
+ * This is deliberately separate from {@link nextRegionSlots}: that helper
588
+ * writes a whole slot from a signed-in tab's draft, while this one must work
589
+ * for a region that is signed OUT — which is precisely the region a user wants
590
+ * to switch off (no international install, no international account).
591
+ *
592
+ * `value` may be the whole settings section or the `regions` map; the RETURN
593
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
594
+ * 'regions', ...)` needs.
595
+ */
596
+ function nextRegionEnabled(value, region, enabled) {
597
+ return nextRegionSlots(regionsMapOf(value), region, {
598
+ ...regionSlotOf(value, region),
599
+ enabled
600
+ });
601
+ }
602
+ /**
470
603
  * Project one card row into its persisted `lastCatalog` shape: the native
471
604
  * context window becomes the stored `contextWindow`, and the card-only
472
605
  * presentation fields (`nativeContextWindow`, `multimodal`) are removed BY
@@ -539,12 +672,18 @@ function toWebModel(model, budgets) {
539
672
  } }
540
673
  };
541
674
  }
542
- /** Project a store account into the card's token-free account row. */
675
+ /**
676
+ * Project a store account into the card's token-free account row.
677
+ *
678
+ * `uin` is deliberately NOT forwarded: the card has never rendered it, so
679
+ * sending it was pure exposure of an account identifier for no feature. The
680
+ * name carries whatever the desktop app recorded, and `''` means the card
681
+ * should show its own placeholder rather than an identifier.
682
+ */
543
683
  function toWebAccount(account) {
544
684
  return {
545
685
  id: account.id,
546
686
  accountName: account.accountName,
547
- ...account.uin === void 0 ? {} : { uin: account.uin },
548
687
  domain: account.domain,
549
688
  source: account.source,
550
689
  tokenExpiresAtMs: account.tokenExpiresAtMs,
@@ -552,6 +691,31 @@ function toWebAccount(account) {
552
691
  };
553
692
  }
554
693
  /**
694
+ * The probed-path list for a signed-out region, or nothing when the store
695
+ * cannot produce one.
696
+ *
697
+ * Diagnostics must never turn a page into an error: `diagnose()` re-reads the
698
+ * filesystem, and a store built without it (or one whose probe throws on an
699
+ * exotic filesystem) degrades to the plain "not signed in" hint the card showed
700
+ * before this existed. Failure reasons are `safeMessage`d because a raw
701
+ * filesystem error can embed an absolute path or a fragment of file content.
702
+ */
703
+ async function searchedPaths(store) {
704
+ if (typeof store.diagnose !== "function") return {};
705
+ try {
706
+ const { failures } = await store.diagnose();
707
+ if (failures.length === 0) return {};
708
+ return { searched: failures.map((failure) => ({
709
+ path: failure.path,
710
+ source: failure.source,
711
+ reason: failure.reason,
712
+ ...failure.message === void 0 ? {} : { message: safeMessage(failure.message) }
713
+ })) };
714
+ } catch {
715
+ return {};
716
+ }
717
+ }
718
+ /**
555
719
  * Assemble one region's card document. `region` is the tab the card is on;
556
720
  * the region-scoped store already answers with only that region's accounts,
557
721
  * so the document's model slots and account list are that region's by
@@ -561,45 +725,61 @@ function toWebAccount(account) {
561
725
  async function workBuddyWebStatus(deps, region) {
562
726
  const store = deps.store(region);
563
727
  const accounts = await store.accounts();
728
+ deps.regionUsable?.(region, accounts.length > 0);
729
+ const enabled = deps.regionEnabled(region);
564
730
  const authStatus = await store.status();
565
731
  const selectionExplicit = store.hasExplicitSelection();
566
732
  if (authStatus.state !== "signed-out" && accounts.length === 0) return {
567
733
  status: "signed-out",
568
734
  accounts: [],
569
- selectionExplicit
735
+ selectionExplicit,
736
+ enabled
570
737
  };
571
738
  let credential;
572
739
  try {
573
740
  credential = await store.resolve();
574
741
  } catch (error) {
742
+ const webAccounts = accounts.map(toWebAccount);
575
743
  return {
576
744
  status: "signed-out",
577
- accounts: accounts.map(toWebAccount),
745
+ accounts: webAccounts,
578
746
  message: safeMessage(error),
579
747
  selectionExplicit,
580
- ...await store.selectionLost() ? { selectionLost: true } : {}
748
+ enabled,
749
+ ...await store.selectionLost() ? { selectionLost: true } : {},
750
+ ...webAccounts.length === 0 ? await searchedPaths(store) : {}
581
751
  };
582
752
  }
753
+ const selected = accounts.find((account) => account.selected);
583
754
  const account = {
584
- accountId: accounts.find((account) => account.selected)?.id ?? "",
585
- accountName: credential.nickname ?? credential.uin ?? credential.uid,
586
- ...credential.uin === void 0 ? {} : { uin: credential.uin },
755
+ accountId: selected?.id ?? "",
756
+ accountName: credential.nickname ?? "",
587
757
  ...credential.domain === "" ? {} : { domain: credential.domain },
588
758
  region,
589
759
  source: credential.source,
590
760
  tokenExpiresAtMs: credential.expiresAtMs,
591
761
  selectionExplicit,
762
+ enabled,
592
763
  accounts: accounts.map(toWebAccount),
593
764
  models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
594
765
  enabledModelIds: [...deps.enabledModelIds(region)],
595
766
  imageModelIds: [...deps.imageModelIds(region)]
596
767
  };
597
768
  const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
769
+ const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
770
+ const recovery = !rejected ? void 0 : await resolveCredentialRecovery({
771
+ region,
772
+ store: deps.store(region),
773
+ ...selected === void 0 ? {} : { rejectedAccountId: selected.id },
774
+ probe: deps.accountUsable ?? (async () => false)
775
+ });
598
776
  return {
599
777
  status: "signed-in",
600
778
  ...account,
601
779
  ...creditsResult.status === "fulfilled" ? { credits: toCredits(creditsResult.value) } : { creditsError: safeMessage(creditsResult.reason) },
602
- ...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) }
780
+ ...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) },
781
+ ...!rejected ? {} : { credentialRejected: true },
782
+ ...recovery === void 0 ? {} : { recovery }
603
783
  };
604
784
  }
605
785
  /**
@@ -651,7 +831,9 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
651
831
  const region = requestRegion(req, res);
652
832
  if (region === void 0) return;
653
833
  try {
654
- json(res, 200, { accounts: (await deps.store(region).accounts()).map(toWebAccount) });
834
+ const accounts = await deps.store(region).accounts();
835
+ deps.regionUsable?.(region, accounts.length > 0);
836
+ json(res, 200, { accounts: accounts.map(toWebAccount) });
655
837
  } catch (error) {
656
838
  json(res, 500, { error: safeMessage(error) });
657
839
  }
@@ -722,6 +904,7 @@ const modelConfig = z.object({
722
904
  maxTokens: z.number().step(1).min(1)
723
905
  });
724
906
  const regionStateConfig = z.object({
907
+ enabled: z.boolean().default(true).description("Whether this region's provider is offered to DSH (opt-out; false withdraws it entirely)"),
725
908
  lastCatalog: z.array(modelConfig).default([]),
726
909
  enabledModelIds: z.array(z.string()).default([]),
727
910
  imageModelIds: z.array(z.string()).default([]),
@@ -761,6 +944,17 @@ function regionStateOf(config, region) {
761
944
  ...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
762
945
  };
763
946
  }
947
+ /**
948
+ * Whether one region's provider is switched on. Opt-out semantics: only an
949
+ * explicit `false` disables it, so every config written before this switch
950
+ * existed — including the pre-region-split flat fields, which never carry
951
+ * `enabled` — keeps both providers running exactly as before. The card reads the
952
+ * same rule through `regionEnabledOf`, so the two halves can never disagree
953
+ * about a region's state.
954
+ */
955
+ function regionEnabled(config, region) {
956
+ return regionStateOf(config, region).enabled !== false;
957
+ }
764
958
  /** Every region, in card tab order. */
765
959
  const REGION_KEYS = ["cn", "global"];
766
960
  /**
@@ -844,6 +1038,10 @@ function apply(ctx, config) {
844
1038
  })
845
1039
  };
846
1040
  }
1041
+ const accountUsabilityProbe = createAccountUsabilityProbe({
1042
+ store: (region) => stacks[region].store,
1043
+ client
1044
+ });
847
1045
  const withImageSelection = (models, images) => models.map((model) => ({
848
1046
  ...model,
849
1047
  ...images.has(model.id) ? { multimodal: true } : { multimodal: false }
@@ -879,15 +1077,81 @@ function apply(ctx, config) {
879
1077
  stacks[region].catalog.set(configuredModels(value, region));
880
1078
  }
881
1079
  invalidateCatalog();
1080
+ syncRegionRegistration(value);
1081
+ refreshRegionUsability();
1082
+ };
1083
+ /**
1084
+ * Tell each catalog whether its region has ANY local sign-in, so a region the
1085
+ * user has no account for advertises nothing instead of a roster that can
1086
+ * only 401 (issue #12).
1087
+ *
1088
+ * `accounts()` is the right source rather than the SELECTED credential: an
1089
+ * orphaned saved id must not blank a region that still has other sign-ins to
1090
+ * fall back on, and a region the user deliberately cleared must come back to
1091
+ * life the moment its first account appears.
1092
+ *
1093
+ * Deliberately fire-and-forget and idempotent — it runs on every settings
1094
+ * change, and `setRegionUsable` reports whether anything moved so a
1095
+ * no-change pass costs nothing beyond the scan. A failed scan leaves the
1096
+ * previous answer alone (the catalog starts permissive), so a transient
1097
+ * filesystem error never blanks a working region.
1098
+ */
1099
+ const refreshRegionUsability = async () => {
1100
+ for (const region of REGION_KEYS) {
1101
+ let accounts;
1102
+ try {
1103
+ accounts = await stacks[region].store.accounts();
1104
+ } catch {
1105
+ continue;
1106
+ }
1107
+ if (stacks[region].catalog.setRegionUsable(accounts.length > 0)) invalidateCatalog();
1108
+ }
1109
+ };
1110
+ /**
1111
+ * Live registration handles per region, filled once the shim is listening.
1112
+ * A disabled region holds ZERO routes while staying registered: DSH allows
1113
+ * `replace([])` for exactly this case ("a settings section that emptied holds
1114
+ * zero routes while staying registered"), which is what makes the on/off
1115
+ * switch reversible without a restart. Withdrawing the adapter route is what
1116
+ * actually removes the region's models from DSH's model picker — hiding the
1117
+ * card tab alone would leave every model selectable.
1118
+ */
1119
+ const registration = {
1120
+ cn: {},
1121
+ global: {}
1122
+ };
1123
+ /**
1124
+ * Publish each region's on/off state to the harness (issue #11-style region
1125
+ * switch). Both swaps are single synchronous sections, so no request can
1126
+ * observe a half-applied state, and `replace` announces itself through
1127
+ * `llm/adapters-updated`, which is what makes third-party consumers drop the
1128
+ * region too. No-op until the shim has registered; `applySelection` runs again
1129
+ * on every card write (and once more after registration completes), so a
1130
+ * toggle lands immediately.
1131
+ */
1132
+ const syncRegionRegistration = (value) => {
1133
+ for (const region of REGION_KEYS) registration[region].adapter?.replace(regionEnabled(value, region) ? [WORKBUDDY_PROVIDERS[region]] : []);
1134
+ registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
1135
+ provider: WORKBUDDY_PROVIDERS[region],
1136
+ displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
1137
+ settingsNs: WORKBUDDY_SETTINGS_NS,
1138
+ settingsPath: [],
1139
+ declared: false
1140
+ })));
882
1141
  };
883
1142
  ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
884
1143
  store: (region) => stacks[region].store,
885
1144
  client,
1145
+ accountUsable: accountUsabilityProbe,
886
1146
  displayModels: (region) => displayModels(current(), region),
887
1147
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
888
1148
  imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
889
1149
  contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
890
- discoverModels
1150
+ discoverModels,
1151
+ regionEnabled: (region) => regionEnabled(current(), region),
1152
+ regionUsable(region, usable) {
1153
+ if (stacks[region].catalog.setRegionUsable(usable)) invalidateCatalog();
1154
+ }
891
1155
  }));
892
1156
  ctx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
893
1157
  setSource(source) {
@@ -931,9 +1195,9 @@ function apply(ctx, config) {
931
1195
  let releaseAdapterGlobal;
932
1196
  let releaseDirectory;
933
1197
  try {
934
- releaseAdapterCn = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
935
- releaseAdapterGlobal = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
936
- releaseDirectory = ctx.llm.registerConfigurableProviders([{
1198
+ releaseAdapterCn = registration.cn.adapter = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
1199
+ releaseAdapterGlobal = registration.global.adapter = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
1200
+ releaseDirectory = registration.cn.directory = ctx.llm.registerConfigurableProviders([{
937
1201
  provider: WORKBUDDY_PROVIDER,
938
1202
  displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.cn,
939
1203
  settingsNs: WORKBUDDY_SETTINGS_NS,
@@ -964,9 +1228,11 @@ function apply(ctx, config) {
964
1228
  releaseAdapterGlobal?.();
965
1229
  releaseDirectory?.();
966
1230
  }
1231
+ syncRegionRegistration(current());
967
1232
  ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
968
1233
  const region = regionOfProvider(request.provider ?? "");
969
1234
  if (region === void 0) return [];
1235
+ if (!regionEnabled(current(), region)) return [];
970
1236
  const discovered = await discoverModels(region, signal);
971
1237
  const state = regionStateOf(current(), region);
972
1238
  return withImageSelection(deriveCatalog(discovered, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
@@ -983,22 +1249,25 @@ function apply(ctx, config) {
983
1249
  return;
984
1250
  }
985
1251
  if (stopped) return;
986
- for (const region of REGION_KEYS) (async () => {
987
- try {
988
- const credential = await stacks[region].store.resolve();
989
- if (stopped) return;
990
- const models = await client.fetchModels(credential);
991
- if (stopped) return;
992
- const state = regionStateOf(current(), region);
993
- stacks[region].catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
994
- adapters[region].invalidate();
995
- } catch (error) {
996
- ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
997
- }
998
- })();
1252
+ for (const region of REGION_KEYS) {
1253
+ if (!regionEnabled(current(), region)) continue;
1254
+ (async () => {
1255
+ try {
1256
+ const credential = await stacks[region].store.resolve();
1257
+ if (stopped) return;
1258
+ const models = await client.fetchModels(credential);
1259
+ if (stopped) return;
1260
+ const state = regionStateOf(current(), region);
1261
+ stacks[region].catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
1262
+ adapters[region].invalidate();
1263
+ } catch (error) {
1264
+ ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
1265
+ }
1266
+ })();
1267
+ }
999
1268
  }).catch((error) => {
1000
1269
  ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; providers not registered", error);
1001
1270
  });
1002
1271
  }
1003
1272
  //#endregion
1004
- export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionCleared, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1273
+ export { CREDENTIAL_REJECTED_CODE, Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
5
- "version": "2.0.5",
5
+ "version": "2.0.10",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",