dsh-workbuddy-connect 0.7.7 → 0.7.9

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,9 +1,9 @@
1
- import { $ as validAppVersion, A as extractDisplayErrorMessage, B as randomSentinel, C as WORKBUDDY_VARIANTS, D as WORKBUDDY_UPDATE_PATH, F as prepareCnChatBody, G as readCliVersion, H as FALLBACK_CN_APP_VERSION, I as prepareInternationalChatBody, J as WORKBUDDY_APP_VERSION_FILENAME, K as resolveChatIdentity, L as regionOf, M as normalizeCredits, N as parseModelCatalog, O as WorkBuddyUpstreamClient, P as prepareChatBody, Q as resolveAppVersion, R as PROBE_EFFORT_CANDIDATES, S as CN_VARIANT, U as chatUserAgent, V as CN_APP_VERSION_FILENAME, W as fallbackChatIdentity, X as installedAppVersion, Y as appUserAgent, Z as readBundleVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as modelWithCurrentPromotion, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as validCliVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as variantFor, x as AI_VARIANT, y as workbuddyOwnAuthPath, z as probeModel } from "./host-heartbeat-FytEpUGO.js";
1
+ import { $ as validAppVersion, A as extractDisplayErrorMessage, B as randomSentinel, C as WORKBUDDY_VARIANTS, D as WORKBUDDY_UPDATE_PATH, F as prepareCnChatBody, G as readCliVersion, H as FALLBACK_CN_APP_VERSION, I as prepareInternationalChatBody, J as WORKBUDDY_APP_VERSION_FILENAME, K as resolveChatIdentity, L as regionOf, M as normalizeCredits, N as parseModelCatalog, O as WorkBuddyUpstreamClient, P as prepareChatBody, Q as resolveAppVersion, R as PROBE_EFFORT_CANDIDATES, S as CN_VARIANT, U as chatUserAgent, V as CN_APP_VERSION_FILENAME, W as fallbackChatIdentity, X as installedAppVersion, Y as appUserAgent, Z as readBundleVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as atRestKeyProviderFor, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as modelWithCurrentPromotion, k as classifyUpstreamError, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as validCliVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as variantFor, x as AI_VARIANT, y as workbuddyOwnAuthPath, z as probeModel } from "./host-heartbeat-SJaZ33tR.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
5
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
8
  import { createProvider } from "@earendil-works/pi-ai";
9
9
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -577,13 +577,6 @@ var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
577
577
  };
578
578
  //#endregion
579
579
  //#region src/loopback.ts
580
- /**
581
- * Shared loopback gates for the plugin's local HTTP surfaces: the loopback
582
- * shim and the same-origin web-status route. Both are only ever meant to be
583
- * addressed through the machine's loopback interface.
584
- *
585
- * @module dsh-workbuddy-connect/loopback
586
- */
587
580
  /** Loopback hostnames a local plugin surface may be addressed by. */
588
581
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
589
582
  "127.0.0.1",
@@ -623,6 +616,163 @@ function originIsLoopback(origin) {
623
616
  return false;
624
617
  }
625
618
  }
619
+ /**
620
+ * Ports fetch refuses to connect to, per the fetch standard's port blocking
621
+ * table (https://fetch.spec.whatwg.org/#port-blocking). Kept in sync with the
622
+ * `badPorts` table undici ships: only the entries a host's ephemeral range can
623
+ * actually reach matter, but carrying the whole list costs nothing and keeps
624
+ * the check honest on machines with a customised range.
625
+ */
626
+ const FETCH_BLOCKED_PORTS = /* @__PURE__ */ new Set([
627
+ 1,
628
+ 7,
629
+ 9,
630
+ 11,
631
+ 13,
632
+ 15,
633
+ 17,
634
+ 19,
635
+ 20,
636
+ 21,
637
+ 22,
638
+ 23,
639
+ 25,
640
+ 37,
641
+ 42,
642
+ 43,
643
+ 53,
644
+ 69,
645
+ 77,
646
+ 79,
647
+ 87,
648
+ 95,
649
+ 101,
650
+ 102,
651
+ 103,
652
+ 104,
653
+ 109,
654
+ 110,
655
+ 111,
656
+ 113,
657
+ 115,
658
+ 117,
659
+ 119,
660
+ 123,
661
+ 135,
662
+ 137,
663
+ 139,
664
+ 143,
665
+ 161,
666
+ 179,
667
+ 389,
668
+ 427,
669
+ 465,
670
+ 512,
671
+ 513,
672
+ 514,
673
+ 515,
674
+ 526,
675
+ 530,
676
+ 531,
677
+ 532,
678
+ 540,
679
+ 548,
680
+ 554,
681
+ 556,
682
+ 563,
683
+ 587,
684
+ 601,
685
+ 636,
686
+ 989,
687
+ 990,
688
+ 993,
689
+ 995,
690
+ 1719,
691
+ 1720,
692
+ 1723,
693
+ 2049,
694
+ 3659,
695
+ 4045,
696
+ 4190,
697
+ 5060,
698
+ 5061,
699
+ 6e3,
700
+ 6566,
701
+ 6665,
702
+ 6666,
703
+ 6667,
704
+ 6668,
705
+ 6669,
706
+ 6679,
707
+ 6697,
708
+ 10080
709
+ ]);
710
+ /** Whether fetch (and therefore pi-ai) refuses to connect to `port`. */
711
+ function isFetchBlockedPort(port) {
712
+ return FETCH_BLOCKED_PORTS.has(port);
713
+ }
714
+ /** Bind attempts allowed before giving up, including the first. */
715
+ const DEFAULT_MAX_ATTEMPTS = 16;
716
+ /**
717
+ * Bind `server` to an ephemeral port fetch will actually connect to, and
718
+ * resolve with the address it ended up on.
719
+ *
720
+ * `listen(0)` lets the OS hand out any free ephemeral port, and a handful of
721
+ * the ports it can hand out are on the fetch standard's bad-port list: fetch
722
+ * refuses to connect to them and fails with `TypeError: fetch failed` before a
723
+ * socket is opened. The shim is only ever reached through fetch (pi-ai calls
724
+ * it), so settling on one of those ports leaves the plugin with an endpoint
725
+ * that answers raw HTTP and rejects every real request. The port is not
726
+ * negotiable at request time, so the only fix is to bind again.
727
+ *
728
+ * A blocked port is closed and rebound; a listen error is fatal, as it was
729
+ * before, because nothing about picking another port would fix it.
730
+ */
731
+ async function bindToUsablePort(server, options) {
732
+ const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
733
+ const isPortBlocked = options.isPortBlocked ?? isFetchBlockedPort;
734
+ let lastBlockedPort = 0;
735
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
736
+ const address = await listenOnEphemeralPort(server, options.host);
737
+ if (!isPortBlocked(address.port)) return address;
738
+ lastBlockedPort = address.port;
739
+ options.onBlockedPort?.(address.port, attempt);
740
+ await closeQuietly(server);
741
+ }
742
+ throw new Error(`could not bind a fetch-reachable loopback port after ${maxAttempts} attempts (last blocked port: ${lastBlockedPort})`);
743
+ }
744
+ /** One `listen(0)` on `host`, resolving with the address the OS handed out. */
745
+ function listenOnEphemeralPort(server, host) {
746
+ return new Promise((resolve, reject) => {
747
+ const cleanup = () => {
748
+ server.off("listening", onListening);
749
+ server.off("error", onError);
750
+ };
751
+ const onListening = () => {
752
+ cleanup();
753
+ const address = server.address();
754
+ if (address === null || typeof address === "string") {
755
+ reject(/* @__PURE__ */ new Error("workbuddy shim has no listening address"));
756
+ return;
757
+ }
758
+ resolve(address);
759
+ };
760
+ const onError = (error) => {
761
+ cleanup();
762
+ reject(error);
763
+ };
764
+ server.once("listening", onListening);
765
+ server.once("error", onError);
766
+ server.listen(0, host);
767
+ });
768
+ }
769
+ /** Close between attempts; a listener that already closed is not an error here. */
770
+ function closeQuietly(server) {
771
+ return new Promise((resolve) => {
772
+ server.closeAllConnections();
773
+ server.close(() => resolve());
774
+ });
775
+ }
626
776
  //#endregion
627
777
  //#region src/shim.ts
628
778
  /**
@@ -713,11 +863,12 @@ function createWorkBuddyShim(options) {
713
863
  const server = createServer((req, res) => {
714
864
  handle(req, res);
715
865
  });
716
- const ready = new Promise((resolve, reject) => {
717
- server.once("listening", () => resolve());
718
- server.once("error", reject);
719
- });
720
- server.listen(0, "127.0.0.1");
866
+ const ready = bindToUsablePort(server, {
867
+ host: "127.0.0.1",
868
+ onBlockedPort: (port, attempt) => {
869
+ logger?.warn(`dsh-workbuddy-connect: port ${port} is blocked for fetch; rebinding (attempt ${attempt})`);
870
+ }
871
+ }).then(() => void 0);
721
872
  const baseUrl = () => {
722
873
  const address = server.address();
723
874
  if (address === null || typeof address === "string") throw new Error("workbuddy shim has no listening address");
@@ -1282,6 +1433,8 @@ async function workBuddyWebStatus(deps) {
1282
1433
  ...model.billing?.badges !== void 0 && model.billing.badges.length > 0 ? { badges: model.billing.badges } : {},
1283
1434
  ...rate === void 0 ? {} : { credits: rate },
1284
1435
  ...model.billing?.rateUnknown === true ? { rateUnknown: true } : {},
1436
+ ...(model.reasoning?.supportedEfforts?.length ?? 0) > 0 ? { declaredEfforts: model.reasoning.supportedEfforts } : {},
1437
+ ...model.reasoning?.defaultEffort !== void 0 ? { defaultEffort: model.reasoning.defaultEffort } : {},
1285
1438
  ...typeof model.contextWindow === "number" && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {},
1286
1439
  ...typeof defaultContextWindow === "number" && defaultContextWindow > 0 && defaultContextWindow < model.contextWindow ? { defaultContextWindow } : {},
1287
1440
  ...maxContextWindow === void 0 || maxContextWindow <= defaultContextWindow ? {} : { maxContextWindow },
@@ -1860,6 +2013,38 @@ function legacySettingsOf(settings) {
1860
2013
  if (typeof candidate.update !== "function") return void 0;
1861
2014
  return settings;
1862
2015
  }
2016
+ /**
2017
+ * Narrow one host settings service to the modern DSH 0.2.x SettingsForms API, or
2018
+ * `undefined` when this host's generation does not carry it.
2019
+ */
2020
+ function modernSettingsOf(settings) {
2021
+ if (typeof settings !== "object" || settings === null) return void 0;
2022
+ const candidate = settings;
2023
+ if (typeof candidate.installSection === "function") return void 0;
2024
+ if (typeof candidate.describe !== "function") return void 0;
2025
+ if (typeof candidate.update !== "function") return void 0;
2026
+ return settings;
2027
+ }
2028
+ /**
2029
+ * Resolve this plugin's own profile entry id from SettingsForms descriptors.
2030
+ *
2031
+ * The real `describe()` (dsh-settings 0.2.1-rc.2, `SettingsForms.describe`)
2032
+ * hands out *serialized* form schemas (`volatileForm(schema).toJSON()`) and
2033
+ * no fiber reference, so neither a `desc.schema === Config` identity check
2034
+ * nor a fiber comparison can ever succeed — identity has to come from the
2035
+ * entry id the loader attached to this plugin's own fiber
2036
+ * (`ctx.fiber.entry.options.id`; the tree-qualified `entry.id` includes
2037
+ * parent prefixes that SettingsForms does not use). The package-name literal is the only fallback, for hosts where the
2038
+ * plugin was loaded programmatically and carries no entry: entry ids are
2039
+ * unique per composition, so the literal can only name this plugin. Neither
2040
+ * route matching an entry that is served answers `undefined` — never a
2041
+ * guess, because a wrong guess silently writes another plugin's
2042
+ * configuration.
2043
+ */
2044
+ function resolveModernSettingsEntryId(descriptors, ownEntryId) {
2045
+ if (ownEntryId !== void 0) return descriptors.find((row) => row.ns === ownEntryId)?.ns;
2046
+ return descriptors.find((row) => row.ns === "dsh-workbuddy-connect")?.ns;
2047
+ }
1863
2048
  //#endregion
1864
2049
  //#region src/index.ts
1865
2050
  /** Stable Cordis plugin name. */
@@ -1938,7 +2123,7 @@ const AUTH_FILE_FIELD = z.string().description("WorkBuddy desktop auth file (def
1938
2123
  const AUTH_FILE_AI_FIELD = z.string().description("WorkBuddy AI desktop auth file (defaults to the app's own location)");
1939
2124
  /** Probe authorization (shared by the plugin schema and the CN section). */
1940
2125
  const PROBE_CONSENT_FIELD = z.boolean().default(false).description("Authorize reasoning-effort probes (each probe sends real requests that may consume credit)");
1941
- const MAXIMUM_CONTEXT_WINDOW_FIELD = z.boolean().default(true).description("Use the largest context window declared by WorkBuddy AI when alternatives are available (on by default)");
2126
+ const MAXIMUM_CONTEXT_WINDOW_FIELD = z.boolean().default(true).volatile().description("Use the largest context window declared by WorkBuddy AI when alternatives are available (on by default)");
1942
2127
  const Config = z.object({
1943
2128
  authFile: AUTH_FILE_FIELD,
1944
2129
  authFileAI: AUTH_FILE_AI_FIELD,
@@ -1946,6 +2131,17 @@ const Config = z.object({
1946
2131
  useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1947
2132
  });
1948
2133
  /**
2134
+ * Read the context-window preference off a resolved Config. Fields marked
2135
+ * `.volatile()` materialize as cosmokit volatile containers (`{ get() }`) so
2136
+ * the host can live-update them without remounting the plugin; plain hosts
2137
+ * and values set through this plugin are plain booleans. Both spellings
2138
+ * answer here.
2139
+ */
2140
+ function plainConfigBoolean(value) {
2141
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return plainConfigBoolean(value.get());
2142
+ return value === void 0 ? void 0 : value === true;
2143
+ }
2144
+ /**
1949
2145
  * The CN side's settings section: only the fields that side edits.
1950
2146
  *
1951
2147
  * The section's namespace is what serves these fields to `settings.yaml` and
@@ -2007,7 +2203,7 @@ function createVariantRuntime(config, variant, current, identityOf, accountOf, k
2007
2203
  });
2008
2204
  const fallback = fallbackFor(variant);
2009
2205
  const catalog = new WorkBuddyCatalog(fallback);
2010
- if (variant.id !== CN_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
2206
+ if (variant.id !== CN_VARIANT.id) catalog.setUseMaximumContextWindow(plainConfigBoolean(config.useMaximumContextWindow) === true);
2011
2207
  catalog.setVisible(false);
2012
2208
  const probeStore = new WorkBuddyProbeStore({
2013
2209
  pluginVersion: WORKBUDDY_CONNECT_VERSION,
@@ -2194,6 +2390,8 @@ function apply(ctx, config) {
2194
2390
  * checkbox that could not be saved.
2195
2391
  */
2196
2392
  let legacySettingsAvailable = false;
2393
+ /** Whether modern DSH 0.2.x SettingsForms service is available to persist settings. */
2394
+ let settingsAvailable = false;
2197
2395
  /**
2198
2396
  * Point a variant at an account identity, invalidating whatever the previous
2199
2397
  * one left behind.
@@ -2273,7 +2471,7 @@ function apply(ctx, config) {
2273
2471
  disabled: runtime.visibilityStore.disabled(account)
2274
2472
  };
2275
2473
  },
2276
- ...runtime.variant.id === CN_VARIANT.id ? {} : { useMaximumContextWindow: () => legacySettingsAvailable ? current().useMaximumContextWindow === true : void 0 }
2474
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { useMaximumContextWindow: () => legacySettingsAvailable || settingsAvailable ? plainConfigBoolean(current().useMaximumContextWindow) === true : void 0 }
2277
2475
  });
2278
2476
  registerWorkBuddyProbeRoute(webCtx, {
2279
2477
  path: runtime.variant.probePath,
@@ -2348,50 +2546,191 @@ function apply(ctx, config) {
2348
2546
  });
2349
2547
  ctx.inject(["settings"], (settingsCtx) => {
2350
2548
  const legacy = legacySettingsOf(settingsCtx.settings);
2351
- if (legacy === void 0) {
2352
- ctx.logger.warn("dsh-workbuddy-connect: host settings service has no installSection API; per-variant settings and the maximum-context preference are unavailable");
2549
+ if (legacy !== void 0) {
2550
+ legacySettingsAvailable = true;
2551
+ /** Section sources; each falls back to its own slice when its side unloads. */
2552
+ const sources = {
2553
+ cn: () => config,
2554
+ ai: () => config
2555
+ };
2556
+ /** Merge both sections into the whole config the rest of the plugin reads. */
2557
+ const merged = () => {
2558
+ const aiMaximum = plainConfigBoolean(sources.ai().useMaximumContextWindow);
2559
+ return {
2560
+ ...sources.cn().authFile === void 0 ? {} : { authFile: sources.cn().authFile },
2561
+ ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
2562
+ ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI },
2563
+ ...aiMaximum === void 0 ? {} : { useMaximumContextWindow: aiMaximum }
2564
+ };
2565
+ };
2566
+ const applyMaximumContextWindow = (next) => {
2567
+ const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
2568
+ if (runtime?.catalog.setUseMaximumContextWindow(next.useMaximumContextWindow === true)) runtime.invalidate();
2569
+ };
2570
+ const repointStores = () => {
2571
+ const next = merged();
2572
+ applyMaximumContextWindow(next);
2573
+ for (const runtime of runtimes) runtime.store.setDesktopPath(configuredAuthFile(next, runtime.variant));
2574
+ };
2575
+ legacy.installSection(ctx, WORKBUDDY_SETTINGS_NS, CN_SECTION, config, {
2576
+ setSource(source) {
2577
+ sources.cn = source;
2578
+ current = merged;
2579
+ },
2580
+ onChange: repointStores
2581
+ });
2582
+ legacy.installSection(ctx, WORKBUDDY_AI_SETTINGS_NS, AI_SECTION, config, {
2583
+ setSource(source) {
2584
+ sources.ai = source;
2585
+ current = merged;
2586
+ },
2587
+ onChange: repointStores
2588
+ });
2589
+ setMaximumContextWindow = async (enabled) => {
2590
+ await legacy.update(WORKBUDDY_AI_SETTINGS_NS, { useMaximumContextWindow: enabled });
2591
+ return { state: "updated" };
2592
+ };
2353
2593
  return;
2354
2594
  }
2355
- legacySettingsAvailable = true;
2356
- /** Section sources; each falls back to its own slice when its side unloads. */
2357
- const sources = {
2358
- cn: () => config,
2359
- ai: () => config
2360
- };
2361
- /** Merge both sections into the whole config the rest of the plugin reads. */
2362
- const merged = () => ({
2363
- ...sources.cn().authFile === void 0 ? {} : { authFile: sources.cn().authFile },
2364
- ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
2365
- ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI },
2366
- ...sources.ai().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.ai().useMaximumContextWindow }
2367
- });
2368
- const applyMaximumContextWindow = (next) => {
2369
- const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
2370
- if (runtime?.catalog.setUseMaximumContextWindow(next.useMaximumContextWindow === true)) runtime.invalidate();
2371
- };
2372
- const repointStores = () => {
2373
- const next = merged();
2374
- applyMaximumContextWindow(next);
2375
- for (const runtime of runtimes) runtime.store.setDesktopPath(configuredAuthFile(next, runtime.variant));
2376
- };
2377
- legacy.installSection(ctx, WORKBUDDY_SETTINGS_NS, CN_SECTION, config, {
2378
- setSource(source) {
2379
- sources.cn = source;
2380
- current = merged;
2381
- },
2382
- onChange: repointStores
2383
- });
2384
- legacy.installSection(ctx, WORKBUDDY_AI_SETTINGS_NS, AI_SECTION, config, {
2385
- setSource(source) {
2386
- sources.ai = source;
2387
- current = merged;
2388
- },
2389
- onChange: repointStores
2390
- });
2391
- setMaximumContextWindow = async (enabled) => {
2392
- await legacy.update(WORKBUDDY_AI_SETTINGS_NS, { useMaximumContextWindow: enabled });
2393
- return { state: "updated" };
2394
- };
2595
+ const modern = modernSettingsOf(settingsCtx.settings);
2596
+ if (modern !== void 0) {
2597
+ /**
2598
+ * The live preference as the host's own Config reports it.
2599
+ *
2600
+ * The forms layer is the single source of truth on a 0.2.x host: a write
2601
+ * lands in the profile document and comes back through `describe()`, so
2602
+ * nothing has to be mirrored in memory here (and a mirror is exactly how
2603
+ * runtime and saved state drift apart). The persisted default already
2604
+ * applies — the preference field defaults to `true` and the composition
2605
+ * handed this plugin its resolved `config`.
2606
+ */
2607
+ let liveConfig = {
2608
+ ...config,
2609
+ useMaximumContextWindow: plainConfigBoolean(config.useMaximumContextWindow) ?? true
2610
+ };
2611
+ /** Whether {@link liveConfig} came from a settings descriptor (see below). */
2612
+ let liveConfigBound = false;
2613
+ /**
2614
+ * Read this plugin's own entry out of the host's forms.
2615
+ *
2616
+ * The namespace on a 0.2.x host is the *profile entry id* — not the
2617
+ * `SettingsNamespace` the 0.1.x sections were installed under — so it is
2618
+ * whatever the user named this plugin in the profile composition.
2619
+ *
2620
+ * Identity cannot come from the descriptors themselves: `describe()`
2621
+ * hands out *serialized* forms (`volatileForm(schema).toJSON()`), never
2622
+ * the live Config object, and its rows carry no fiber — so
2623
+ * `desc.schema === Config` and fiber comparisons are always false
2624
+ * against the real service. The loader instead keys entries by id and
2625
+ * attaches them to the fibers it runs (`ctx.fiber.entry.options.id`, unlike the tree-qualified `entry.id`), which is
2626
+ * what {@link resolveModernSettingsEntryId} reads; the package-name
2627
+ * literal stays as the only fallback (entry ids are unique per
2628
+ * composition, so it can only name this plugin). Anything unmatched
2629
+ * answers `undefined` — a wrong guess would silently write another
2630
+ * plugin's configuration.
2631
+ */
2632
+ const settingsEntryId = () => {
2633
+ try {
2634
+ const entry = ctx.fiber?.entry;
2635
+ return resolveModernSettingsEntryId(modern.describe(), entry?.options?.id ?? entry?.id);
2636
+ } catch (error) {
2637
+ ctx.logger.warn("dsh-workbuddy-connect: reading this plugin's settings entry failed: %s", error);
2638
+ return;
2639
+ }
2640
+ };
2641
+ /**
2642
+ * Apply whatever the settings document now says.
2643
+ *
2644
+ * The value read is the descriptor's `value` — the projection of the live
2645
+ * fiber config, i.e. what the loader actually resolved — rather than
2646
+ * `user`, which names only the profile's own section and would miss an
2647
+ * inherited or default value that is nonetheless in effect.
2648
+ */
2649
+ const applyModernConfig = () => {
2650
+ if (!liveConfigBound) return;
2651
+ const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
2652
+ if (runtime?.catalog.setUseMaximumContextWindow(plainConfigBoolean(liveConfig.useMaximumContextWindow) === true)) runtime.invalidate();
2653
+ };
2654
+ setMaximumContextWindow = async (enabled) => {
2655
+ const ns = settingsEntryId();
2656
+ if (ns === void 0) return {
2657
+ state: "failed",
2658
+ reason: "no configurable settings entry for this plugin"
2659
+ };
2660
+ try {
2661
+ await modern.update(ns, { useMaximumContextWindow: enabled });
2662
+ if ((modern.describe().find((entry) => entry.ns === ns)?.value)?.useMaximumContextWindow !== enabled) return {
2663
+ state: "failed",
2664
+ reason: "the settings document did not accept the context-window preference"
2665
+ };
2666
+ } catch (error) {
2667
+ return {
2668
+ state: "failed",
2669
+ reason: error instanceof Error ? error.message.slice(0, 300) : String(error)
2670
+ };
2671
+ }
2672
+ liveConfig = {
2673
+ ...liveConfig,
2674
+ useMaximumContextWindow: enabled
2675
+ };
2676
+ current = () => liveConfig;
2677
+ applyModernConfig();
2678
+ return { state: "updated" };
2679
+ };
2680
+ let retry;
2681
+ let retryIndex = 0;
2682
+ let bindingStopped = false;
2683
+ const retryDelays = [
2684
+ 250,
2685
+ 500,
2686
+ 1e3,
2687
+ 2e3,
2688
+ 4e3,
2689
+ 8e3
2690
+ ];
2691
+ const clearRetry = () => {
2692
+ clearTimeout(retry);
2693
+ retry = void 0;
2694
+ };
2695
+ const bindModernConfig = () => {
2696
+ if (bindingStopped) return;
2697
+ try {
2698
+ const ns = settingsEntryId();
2699
+ const entry = ns === void 0 ? void 0 : modern.describe().find((desc) => desc.ns === ns);
2700
+ settingsAvailable = (entry?.schema === void 0 ? void 0 : new z(entry.schema))?.dict?.useMaximumContextWindow?.type === "boolean";
2701
+ if (settingsAvailable) {
2702
+ const stored = (entry?.value)?.useMaximumContextWindow;
2703
+ liveConfig = {
2704
+ ...liveConfig,
2705
+ useMaximumContextWindow: plainConfigBoolean(stored) ?? plainConfigBoolean(liveConfig.useMaximumContextWindow) ?? true
2706
+ };
2707
+ liveConfigBound = true;
2708
+ current = () => liveConfig;
2709
+ applyModernConfig();
2710
+ clearRetry();
2711
+ return;
2712
+ }
2713
+ } catch (error) {
2714
+ settingsAvailable = false;
2715
+ ctx.logger.warn("dsh-workbuddy-connect: reading the stored context-window preference failed: %s", error);
2716
+ }
2717
+ if (retry === void 0 && retryIndex < retryDelays.length) retry = setTimeout(() => {
2718
+ retry = void 0;
2719
+ bindModernConfig();
2720
+ }, retryDelays[retryIndex++]);
2721
+ };
2722
+ settingsCtx.effect(() => () => {
2723
+ bindingStopped = true;
2724
+ clearRetry();
2725
+ settingsAvailable = false;
2726
+ });
2727
+ settingsCtx.effect(() => ctx.on("settings/document-updated", () => {
2728
+ bindModernConfig();
2729
+ }));
2730
+ bindModernConfig();
2731
+ return;
2732
+ }
2733
+ ctx.logger.warn("dsh-workbuddy-connect: host settings service has no installSection or SettingsForms API; per-variant settings and the maximum-context preference are unavailable");
2395
2734
  });
2396
2735
  ctx.effect(() => () => {
2397
2736
  stopped = true;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-connect",
3
3
  "displayName": "DSH WorkBuddy Connect",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.7.7",
5
+ "version": "0.7.9",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",
@@ -41,7 +41,8 @@
41
41
  "cordis.patch.yml",
42
42
  "README.md",
43
43
  "README.en.md",
44
- "LICENSE"
44
+ "LICENSE",
45
+ "THIRD_PARTY_NOTICES.txt"
45
46
  ],
46
47
  "license": "MIT",
47
48
  "repository": {
@@ -71,36 +72,35 @@
71
72
  },
72
73
  "peerDependencies": {
73
74
  "@deepseek-ai/cordis": "^4.0.5",
74
- "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.1",
75
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.1",
76
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.1",
77
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.1",
78
- "@deepseek-ai/dsh-llm": "0.2.1-rc.1",
79
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.1",
80
- "@deepseek-ai/dsh-settings": "0.2.1-rc.1",
75
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
76
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
77
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
78
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
79
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.2",
80
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
81
81
  "@deepseek-ai/schemastery": "^3.18.5",
82
82
  "react": "^18.2.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@deepseek-ai/cordis": "^4.0.5",
86
- "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.1",
87
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.1",
88
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.1",
89
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.1",
90
- "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.1",
91
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.1",
92
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.1",
93
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.1",
94
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.1",
95
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.1",
96
- "@deepseek-ai/dsh-authorization": "0.2.1-rc.1",
97
- "@deepseek-ai/dsh-fs": "0.2.1-rc.1",
98
- "@deepseek-ai/dsh-launch-environment": "0.2.1-rc.1",
99
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.1",
100
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.1",
101
- "@deepseek-ai/dsh-llm": "0.2.1-rc.1",
102
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.1",
103
- "@deepseek-ai/dsh-settings": "0.2.1-rc.1",
86
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.2",
87
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
88
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.2",
91
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
92
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.2",
93
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
94
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
95
+ "@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
96
+ "@deepseek-ai/dsh-authorization": "0.2.1-rc.2",
97
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
98
+ "@deepseek-ai/dsh-launch-environment": "0.2.1-rc.2",
99
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
100
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
101
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
102
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.2",
103
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
104
104
  "@deepseek-ai/schemastery": "^3.18.5",
105
105
  "@earendil-works/pi-ai": "1.3.0",
106
106
  "@types/node": "^22.20.0",