dsh-workbuddy-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -0,0 +1,1642 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import "@earendil-works/pi-ai";
3
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
+ import { Context, Volatile } from "@deepseek-ai/cordis";
5
+ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
6
+ //#region src/shared/paths.d.ts
7
+ /**
8
+ * Why no credential is usable, as a closed enum the browser half switches on.
9
+ *
10
+ * Deliberately separate from `reason`: `reason` is free text meant for a human
11
+ * to read, so matching on it would break the moment the wording changes. This
12
+ * is the machine-readable half, and the card uses it — never a substring of
13
+ * `reason` — to decide whether the Agent assist block applies.
14
+ */
15
+ type WorkBuddySignedOutReasonCode =
16
+ /** Nobody is signed in; nothing diagnosable beyond that. */
17
+ 'no-credential' |
18
+ /** A credential for the *other* product was found in this variant's file. */
19
+ 'credential-region-mismatch' |
20
+ /** An encrypted credential exists but could not be opened (wrong key, GCM failure, helper crash). */
21
+ 'encrypted-credential-unreadable' |
22
+ /** CN/macOS: discovery ran to completion and produced no usable candidate. */
23
+ 'electron-binary-not-found' |
24
+ /** CN/macOS: discovery found more than one distinct usable app. */
25
+ 'electron-binary-ambiguous' |
26
+ /** No auto-discovery for this product/platform and no explicit path configured. */
27
+ 'electron-binary-unavailable' |
28
+ /** An explicit path (option or env) is set but missing or not executable. */
29
+ 'electron-path-invalid' |
30
+ /** Discovery could not finish: tool missing, timeout, output overflow, unreadable plist. */
31
+ 'electron-discovery-incomplete';
32
+ //#endregion
33
+ //#region src/credential/at-rest.d.ts
34
+ /** The four states a desktop auth document can be read as. */
35
+ type DesktopAuthFormat = 'absent' | 'plaintext' | 'encrypted' | 'unrecognized';
36
+ /** The spawned helper. Separated from the provider so tests can stand it in. */
37
+ type WorkBuddyKeyPayloadSource = () => Promise<string>;
38
+ /**
39
+ * Which automatic discovery, if any, this provider may run when no explicit
40
+ * binary is configured.
41
+ *
42
+ * `none` is the safe default: a provider that has not been told which product
43
+ * it serves must not reach for another product's app.
44
+ *
45
+ * The two `*-workbuddy` kinds name a **platform strategy** — both verified live
46
+ * against WorkBuddy 5.6.2, both only ever looking for the CN app — and each
47
+ * refuses when the running platform is not its own:
48
+ * `macos-workbuddy` resolves the platform default and then Spotlight;
49
+ * `windows-workbuddy` resolves the platform default and then the Uninstall
50
+ * registry key.
51
+ */
52
+ type WorkBuddyElectronDiscovery = 'none' | 'macos-workbuddy' | 'windows-workbuddy';
53
+ /** Seams the discovery flow runs through, so tests never spawn a process. */
54
+ interface WorkBuddyDiscoveryTools {
55
+ /** Candidate `.app` bundles for the CN bundle id, or a throw for an unusable tool. */
56
+ findApps: (signal: AbortSignal) => Promise<readonly string[]>;
57
+ /**
58
+ * `CFBundleIdentifier` of a bundle, or `undefined` when the tool could not
59
+ * read it — which is "we could not check this candidate", never "it does not
60
+ * match". A successful read of a *different* id returns that id, and the
61
+ * caller excludes the candidate.
62
+ */
63
+ bundleIdentifier: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
64
+ /** Display version, best effort; `undefined` when unavailable. */
65
+ bundleVersion: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
66
+ }
67
+ /** Provider options. */
68
+ interface WorkBuddyAtRestKeyProviderOptions {
69
+ /** Explicit Electron binary; overrides the platform default and env. */
70
+ electronPath?: string;
71
+ /** Helper timeout in milliseconds; default 10s. */
72
+ timeoutMs?: number;
73
+ /**
74
+ * Where the payload comes from. Defaults to spawning WorkBuddy's own
75
+ * Electron with `ELECTRON_RUN_AS_NODE=1`; tests supply a stand-in so no
76
+ * test ever touches the real binary or a real key. Supplying this replaces
77
+ * path *resolution* too, so tests about resolution use
78
+ * {@link spawnHelper} instead.
79
+ */
80
+ source?: WorkBuddyKeyPayloadSource;
81
+ /**
82
+ * Runs the helper at the resolved path. Distinct from {@link source}, which
83
+ * replaces the whole payload path: this seam keeps resolution — explicit
84
+ * config, platform default, discovery — real, so tests can exercise it
85
+ * without spawning anything.
86
+ */
87
+ spawnHelper?: (electronPath: string) => Promise<string>;
88
+ /**
89
+ * Automatic discovery budget; defaults to `'none'` (see
90
+ * {@link WorkBuddyElectronDiscovery}). Passed explicitly per variant at the
91
+ * composition root, never inferred from the environment.
92
+ */
93
+ discovery?: WorkBuddyElectronDiscovery;
94
+ /**
95
+ * Platform default binary, consulted only when `discovery` is enabled and no
96
+ * explicit path is configured. Injectable so tests can force the fallback
97
+ * branch without moving the real app; `null` means "no default here".
98
+ */
99
+ defaultElectronPath?: string | undefined;
100
+ /** Discovery subprocesses; injectable so tests never spawn. */
101
+ tools?: WorkBuddyDiscoveryTools;
102
+ /** Windows registry subprocess; injectable so tests never spawn `reg.exe`. */
103
+ windowsTools?: WorkBuddyWindowsDiscoveryTools;
104
+ /**
105
+ * Total budget for one discovery run, covering the search and every
106
+ * candidate check. Injectable so tests can exercise exhaustion without
107
+ * waiting out the production 10s.
108
+ */
109
+ discoveryBudgetMs?: number;
110
+ }
111
+ /** Seams the Windows discovery flow runs through, so tests never spawn a process. */
112
+ interface WorkBuddyWindowsDiscoveryTools {
113
+ /**
114
+ * Registered WorkBuddy install directories. An empty array means "the query ran
115
+ * and matched nothing" — a decidable absence. A query that could not run at all
116
+ * must throw {@link DiscoveryIncompleteError}, so a broken tool is never read
117
+ * as "not installed".
118
+ */
119
+ findInstallRoots: (signal: AbortSignal) => Promise<readonly string[]>;
120
+ }
121
+ /**
122
+ * In-memory protector-key resolver: one spawn per key id, single-flight, never
123
+ * persisted. The cache is keyed by the id envelopes ask for, so an envelope
124
+ * sealed under a rotated key triggers exactly one fresh resolution.
125
+ */ declare class WorkBuddyAtRestKeyProvider {
126
+ /**
127
+ * The explicit binary, when one was configured. `undefined` here means "the
128
+ * caller did not name one", which is what lets discovery run — an explicit
129
+ * path that turns out to be unusable is an error, never a reason to look for
130
+ * a different app.
131
+ */
132
+ private readonly explicitPath;
133
+ private readonly defaultPath;
134
+ private readonly discovery;
135
+ private readonly tools;
136
+ /** Explicit `tools` means the caller owns the platform question (tests, custom hosts). */
137
+ private readonly toolsAreInjected;
138
+ private readonly windowsTools;
139
+ /** Explicit `windowsTools` means the caller owns the platform question. */
140
+ private readonly windowsToolsAreInjected;
141
+ private readonly discoveryBudgetMs;
142
+ private readonly timeoutMs;
143
+ private readonly source;
144
+ private readonly spawnHelper;
145
+ /**
146
+ * The path discovery settled on, cached only on success. A failure leaves
147
+ * this unset so the next attempt tries again — the user may install or move
148
+ * the app without restarting DSH.
149
+ */
150
+ private discoveredPath;
151
+ private cache;
152
+ private inflight;
153
+ constructor(options?: WorkBuddyAtRestKeyProviderOptions);
154
+ /**
155
+ * The binary the default helper would use, for diagnostics.
156
+ *
157
+ * Reports a *discovery result* once one exists, so diagnostics describe what
158
+ * would actually run rather than the default that was bypassed. Discovery
159
+ * itself stays in {@link resolveElectronPath}: this accessor never triggers a
160
+ * search (the constructor must remain I/O-free, and callers may ask before
161
+ * any resolution has happened).
162
+ */
163
+ helperPath(): string | undefined;
164
+ /**
165
+ * A protector key matching one of the requested envelope key ids. The first
166
+ * id the cache answers wins; otherwise one spawn resolves the current key,
167
+ * which must match a request — a mismatch means the envelopes were sealed by
168
+ * a different install than the one this machine now runs, and no key we can
169
+ * reach will open them.
170
+ */
171
+ protectorKeyFor(requested: readonly string[]): Promise<Buffer>;
172
+ private ingest;
173
+ /**
174
+ * The binary to spawn, or a diagnosable error saying why there is none.
175
+ *
176
+ * Order is the contract: an explicit path is used as-is and never falls back;
177
+ * discovery runs only for a provider that was configured for it, and only
178
+ * after the platform default has been tried and found unusable.
179
+ */
180
+ private resolveElectronPath;
181
+ /**
182
+ * Resolve the CN app through Spotlight, then prove each candidate's identity
183
+ * before it can be executed.
184
+ *
185
+ * The whole flow shares one budget: a hang in one candidate must not extend
186
+ * the wait for the others, and running out of budget is reported as an
187
+ * unfinished check rather than an absent app.
188
+ */
189
+ private discoverMacosApp;
190
+ /**
191
+ * Resolve the CN app from its Windows registration.
192
+ *
193
+ * There is no second identity check as on macOS: the registration *is* the
194
+ * identity — {@link windowsInstallsFromRegistry} only accepts blocks whose
195
+ * `DisplayName` starts with `WorkBuddy` — and the executable is a fixed name
196
+ * inside the registered directory. A registered directory that no longer holds
197
+ * an executable is a decidable exclusion, the same way a Spotlight row for a
198
+ * deleted app is.
199
+ */
200
+ private discoverWindowsApp;
201
+ private spawnPayload;
202
+ private spawnAt;
203
+ }
204
+ //#endregion
205
+ //#region src/protocol/app-version.d.ts
206
+ /** Basename of the saved version under `$DSH_HOME`. */
207
+ declare const WORKBUDDY_APP_VERSION_FILENAME = ".workbuddy-ai-version.json";
208
+ /** Where the version came from, for `doctor` output. */
209
+ type WorkBuddyAppVersionSource = 'installed' | 'saved' | 'fallback';
210
+ /** Resolved version plus provenance. */
211
+ interface AppVersionInfo {
212
+ version: string;
213
+ source: WorkBuddyAppVersionSource;
214
+ /** Basename of the App bundle the version was read from, when installed. */
215
+ bundle?: string;
216
+ }
217
+ /**
218
+ * Whether a string is safe to interpolate into an HTTP header.
219
+ *
220
+ * Strict on purpose: the value reaches a header, so anything that could split
221
+ * the request (CR, LF, spaces beyond the separator) or inject a second UA
222
+ * token must never pass. The App's own version is always `N.N.N` or `N.N.N.N`.
223
+ */
224
+ declare function validAppVersion(value: unknown): value is string;
225
+ /**
226
+ * Read `CFBundleShortVersionString` out of an `Info.plist`.
227
+ *
228
+ * Parsed as XML rather than grepped, because the plist contains several
229
+ * `<string>` values and a regex would be one unrelated key away from
230
+ * returning the wrong one. A binary plist has no `<dict>` in its bytes and is
231
+ * reported as unreadable (the saved value then applies) rather than guessed at.
232
+ */
233
+ declare function readBundleVersion(plistPath: string): Promise<string | undefined>;
234
+ /**
235
+ * The installed international App's version, or `undefined` when it is not
236
+ * installed (or not readable).
237
+ *
238
+ * Windows and Linux have no verified bundle-metadata location yet, so this
239
+ * returns `undefined` there and the saved/fallback value is used instead of
240
+ * guessing a path — the same discipline the credential discovery follows.
241
+ */
242
+ declare function installedAppVersion(): Promise<{
243
+ version: string;
244
+ bundle: string;
245
+ } | undefined>;
246
+ /** Constructor dependencies; all injectable so tests never touch the real FS. */
247
+ interface ResolveAppVersionOptions {
248
+ /** Installed-version reader; defaults to {@link installedAppVersion}. */
249
+ installed?: () => Promise<{
250
+ version: string;
251
+ bundle: string;
252
+ } | undefined>;
253
+ /** Saved-version path; defaults to {@link appVersionPath}. */
254
+ path?: string;
255
+ /** Cache writer; defaults to {@link writeFileAtomic} with the plugin's modes. */
256
+ write?: (path: string, content: string) => Promise<void>;
257
+ }
258
+ /**
259
+ * Resolve the UA version: installed App first, then the last saved value, then
260
+ * the compiled-in fallback.
261
+ *
262
+ * A value read from the App is written back immediately, so an uninstalled App
263
+ * or an unreadable plist later still has the last real version to fall back
264
+ * on. The write is best-effort: failing to cache a version must never fail the
265
+ * catalog request that asked for it.
266
+ */
267
+ declare function resolveAppVersion(options?: ResolveAppVersionOptions): Promise<AppVersionInfo>;
268
+ /**
269
+ * Build the App-shaped User-Agent for catalog requests.
270
+ *
271
+ * `WorkBuddyAI/<version>` with no space is the form measured to reach the App
272
+ * document; the space form is rejected with 400/12403. Throws on an invalid
273
+ * version rather than sending a malformed header.
274
+ */
275
+ declare function appUserAgent(version: string): string;
276
+ //#endregion
277
+ //#region src/protocol/client-identity.d.ts
278
+ /**
279
+ * Compiled-in CN fallback for the `WorkBuddy/<v>` tokens.
280
+ *
281
+ * Observed on the CN desktop app installed here (research §3.1, verified
282
+ * 2026-09-11); like the international fallback it is a shape requirement,
283
+ * not a currency claim — the gateway has not been observed to branch on it.
284
+ */
285
+ declare const FALLBACK_CN_APP_VERSION = "5.5.6";
286
+ /**
287
+ * Basename of the CN saved-version cache under `$DSH_HOME`.
288
+ *
289
+ * Deliberately not the international `.workbuddy-ai-version.json`: that file
290
+ * feeds the international catalog's User-Agent, and a CN App writing its
291
+ * version into it would relabel that request. The two caches stay isolated
292
+ * the way the per-variant catalog files are.
293
+ */
294
+ declare const CN_APP_VERSION_FILENAME = ".workbuddy-app-version.json";
295
+ /** The resolved identity a chat request presents as. */
296
+ interface ChatIdentity {
297
+ /** Desktop App version; drives both `WorkBuddy/<v>` product tokens. */
298
+ clientVersion: string;
299
+ /** Bundled agent-CLI version; absent drops the `CLI/…` UA token. */
300
+ cliVersion?: string;
301
+ }
302
+ /**
303
+ * Whether a value is a CLI version that may reach a header.
304
+ *
305
+ * Tolerates a prerelease suffix (`2.137.1-rc.1`) because the bundled CLI's
306
+ * own metadata uses that spelling; anything with whitespace, CR or LF never
307
+ * passes — the value is interpolated into an HTTP header.
308
+ */
309
+ declare function validCliVersion(value: unknown): value is string;
310
+ /**
311
+ * The bundled agent CLI's real version, or `undefined` when it does not resolve.
312
+ *
313
+ * `cli/package.json` ships a `0.0.0` placeholder in `version` with the real
314
+ * version in `publishConfig.customPackage.version`; a valid non-placeholder
315
+ * `version` wins, otherwise the custom-package value applies, and unreadable
316
+ * or invalid metadata yields `undefined` (the caller drops the `CLI/…` UA
317
+ * token rather than guessing).
318
+ */
319
+ declare function readCliVersion(bundle: string): Promise<string | undefined>;
320
+ /**
321
+ * Build the chat User-Agent for one region.
322
+ *
323
+ * Throws on an invalid version rather than interpolating one into a header;
324
+ * `resolveChatIdentity` never produces such an identity, so the throw is a
325
+ * last gate against future call-site mistakes, not an expected path.
326
+ */
327
+ declare function chatUserAgent(identity: ChatIdentity, region: WorkBuddyRegion): string;
328
+ /** Constructor dependencies; every reader is injectable so tests never touch a real App or home. */
329
+ interface ResolveChatIdentityOptions {
330
+ /** Installed CN desktop-bundle reader; defaults to the macOS probe. */
331
+ installedCn?: () => Promise<{
332
+ version: string;
333
+ bundle: string;
334
+ } | undefined>;
335
+ /** International version resolver; defaults to `app-version.ts`'s chain. */
336
+ resolveIntl?: () => Promise<AppVersionInfo>;
337
+ /** CLI-version reader; defaults to reading the bundle's `cli/package.json`. */
338
+ cliVersion?: (bundle: string) => Promise<string | undefined>;
339
+ /** CN saved-cache path; defaults to `$DSH_HOME/.workbuddy-app-version.json`. */
340
+ cnSavedPath?: string;
341
+ }
342
+ /**
343
+ * Resolve the chat identity for one region: installed App → region's saved
344
+ * value → compiled-in fallback. Never throws — a missing App, an unreadable
345
+ * plist, a failed cache write, or a reader that throws outright all degrade
346
+ * to {@link fallbackChatIdentity}; resolution never blocks a message.
347
+ *
348
+ * The production path caches per region (a message must not re-read the
349
+ * install tree); any injected option bypasses the cache entirely so tests
350
+ * with different readers cannot observe each other's resolutions.
351
+ */
352
+ declare function resolveChatIdentity(region: WorkBuddyRegion, options?: ResolveChatIdentityOptions): Promise<ChatIdentity>;
353
+ /**
354
+ * The region's compiled-in fallback identity: the desktop form with the
355
+ * built-in version and no `CLI/…` segment. This is the single degraded
356
+ * shape every failure path converges on — a thrown reader, an unreadable
357
+ * bundle, or a missing cache all present this, never the legacy CLI UA.
358
+ */
359
+ declare function fallbackChatIdentity(region: WorkBuddyRegion): ChatIdentity;
360
+ //#endregion
361
+ //#region src/probe/probe.d.ts
362
+ /**
363
+ * The canonical values a probe tests, in a fixed order.
364
+ *
365
+ * `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
366
+ * by policy — disabling thinking is a separate capability the upstream must
367
+ * declare through `canDisableThinking`, never something probing may infer.
368
+ */
369
+ declare const PROBE_EFFORT_CANDIDATES: readonly WorkBuddyEffort[];
370
+ /** Sentinel generator; injectable so tests get deterministic values. */
371
+ type SentinelFactory = () => string;
372
+ /** Default sentinel: unmistakably non-canonical, different on every call. */
373
+ declare function randomSentinel(): string;
374
+ /**
375
+ * One response as the probe sees it, split into the only distinctions the
376
+ * attribution rule needs.
377
+ */
378
+ interface ProbeAttempt {
379
+ /** HTTP status, or 0 for a transport failure. */
380
+ status: number;
381
+ /** True when a parseable SSE event arrived. */
382
+ streamed: boolean;
383
+ /** `extError.code` from a JSON error body, when present. */
384
+ errorCode?: string;
385
+ /** Free-form detail for logs; never shown as a capability claim. */
386
+ detail?: string;
387
+ }
388
+ /** How one attempt is performed; the caller owns credentials and HTTP. */
389
+ type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>;
390
+ /** The outcome of probing one model. */
391
+ type ProbeOutcome = {
392
+ validation: 'validating';
393
+ efforts: readonly WorkBuddyEffort[];
394
+ requests: number;
395
+ } | {
396
+ validation: 'non-validating';
397
+ efforts: readonly [];
398
+ requests: number;
399
+ } | {
400
+ validation: 'unknown';
401
+ efforts: readonly [];
402
+ requests: number;
403
+ reason: string;
404
+ };
405
+ /**
406
+ * Probe one model.
407
+ *
408
+ * `options.candidates` exists so tests can shorten the sweep; production always
409
+ * uses {@link PROBE_EFFORT_CANDIDATES}.
410
+ */
411
+ declare function probeModel(options: {
412
+ send: ProbeSender;
413
+ sentinel?: SentinelFactory;
414
+ candidates?: readonly WorkBuddyEffort[];
415
+ timeoutMs?: number;
416
+ }): Promise<ProbeOutcome>;
417
+ //#endregion
418
+ //#region src/protocol/errors.d.ts
419
+ /**
420
+ * 上游失败的结构化分类。
421
+ *
422
+ * 判定顺序是刻意的:**结构化信号优先,HTTP 状态次之,文案匹配只做兜底**。
423
+ * 上游一旦改动提示文案,靠子串匹配的错误分类就会静默失效;而 `extError.code`
424
+ * 这类机器码与 HTTP 状态是协议的一部分,稳定得多。文案匹配保留下来只是因为
425
+ * 部分失败既不带码也不带明确状态,此时它是唯一线索 —— 命中兜底时调用方应当
426
+ * 记日志,让新出现的文案能被发现并补进码表。
427
+ *
428
+ * @module dsh-workbuddy-bridge/protocol/errors
429
+ */
430
+ /** Upstream failure classes the shim maps onto distinct HTTP answers. */
431
+ type UpstreamErrorKind$1 = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
432
+ /** 兼容旧调用点:只要类别,不要证据。 */
433
+ declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind$1;
434
+ //#endregion
435
+ //#region src/protocol/client.d.ts
436
+ /** WorkBuddy region selected by the credential's login domain. */
437
+ type WorkBuddyRegion = 'cn' | 'global';
438
+ /** Upstream failure classes the shim maps onto distinct HTTP answers. */
439
+ type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
440
+ /** One CLI-usable model as the upstream catalog describes it. */
441
+ interface WorkBuddyUpstreamModel {
442
+ id: string;
443
+ name: string;
444
+ contextWindow: number;
445
+ /** The upstream's preferred window before an optional maximum is selected. */
446
+ defaultContextWindow?: number;
447
+ maxInputTokens?: number;
448
+ supportedContextWindows?: readonly number[];
449
+ promotions?: readonly WorkBuddyPromotion[];
450
+ maxTokens: number;
451
+ /**
452
+ * Upstream-declared image input capability. Missing or false upstream data
453
+ * resolves to false, so an unknown model stays text-only: over-claiming
454
+ * admits an image the provider then rejects after the message is durable.
455
+ */
456
+ supportsImages: boolean;
457
+ /**
458
+ * Reasoning metadata the upstream catalog declares per model. The wire
459
+ * effort values (`low`, `medium`, `high`, `xhigh`, `max`) map directly onto
460
+ * pi-ai's thinking levels, and the supported set decides which levels the
461
+ * DSH model selector offers.
462
+ */
463
+ reasoning?: WorkBuddyModelReasoning;
464
+ /**
465
+ * Billing convenience metadata: the credits multiplier string the upstream
466
+ * reports (e.g. `"x0.00"` for free) and promotional badges like
467
+ * `badge:限时免费:#FF0000` or `badge:夜间折扣:#1E90FF`.
468
+ *
469
+ * The multiplier reaches the browser through the host LLM seam, which has no
470
+ * locale service, so {@link normalizeCredits} trims it to a
471
+ * language-neutral display form (`x0.79`) that reads the same in every UI
472
+ * language. The raw upstream string (which may spell `x0.79 credits`) stays
473
+ * on {@link WorkBuddyModelBilling.credits} for diagnostics.
474
+ */
475
+ billing?: WorkBuddyModelBilling;
476
+ }
477
+ /** Reasoning metadata the upstream catalog declares for one model. */
478
+ interface WorkBuddyModelReasoning {
479
+ /** Whether the model does any reasoning at all (upstream `supportsReasoning`). */
480
+ supports: boolean;
481
+ /** Whether the model can only think (upstream `onlyReasoning`). */
482
+ onlyReasoning: boolean;
483
+ /** Selectable effort values; absent means the model has no explicit set. */
484
+ supportedEfforts?: readonly WorkBuddyEffort[];
485
+ /** Default effort the upstream uses when none is chosen. */
486
+ defaultEffort?: WorkBuddyEffort;
487
+ /** Whether thinking can be switched off; false means it is always on. */
488
+ canDisableThinking: boolean;
489
+ }
490
+ /** The concrete effort spellings WorkBuddy exposes on the wire. */
491
+ type WorkBuddyEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
492
+ /** Billing convenience metadata reported for one model. */
493
+ interface WorkBuddyModelBilling {
494
+ /** Credits multiplier, e.g. `"x0.00"` (free) or `"x0.79"`. */
495
+ credits?: string;
496
+ /** Promotional tags, e.g. `"限时免费"`, `"夜间折扣"`. */
497
+ badges?: readonly string[];
498
+ /** Whether the model is currently free (`x0.00` credits). */
499
+ free: boolean;
500
+ /**
501
+ * The rate cannot be stated for this model right now.
502
+ *
503
+ * Set when a row that arrived with promotions attached has no promotion in
504
+ * force: the upstream bakes the discounted value into `credits`, so the
505
+ * cached rate describes a discount that has ended. The original price is not
506
+ * recoverable from the row, so the plugin reports "unknown, refresh needed"
507
+ * rather than repeating a figure it can no longer stand behind — in
508
+ * particular it never keeps claiming the model is free.
509
+ */
510
+ rateUnknown?: boolean;
511
+ }
512
+ /** One billing package and its remaining credit. */
513
+ interface WorkBuddyCreditAccount {
514
+ packageName: string;
515
+ remain: number;
516
+ size: number;
517
+ unlimited?: true;
518
+ }
519
+ /** Aggregated credit answer for one credential. */
520
+ interface WorkBuddyCredits {
521
+ total: number;
522
+ accounts: readonly WorkBuddyCreditAccount[];
523
+ /**
524
+ * The account's cycle quota is uncapped (`limitNum === -1` on the CN
525
+ * enterprise endpoint).
526
+ *
527
+ * A separate flag rather than a `-1`/`0` sentinel in {@link total}: the two
528
+ * mean opposite things to a reader ("no limit" vs "nothing left"), and the
529
+ * existing negative-clamp in the personal branch would turn a sentinel into
530
+ * a plausible-looking zero. Every renderer must therefore test this flag
531
+ * first and not fall back to `total` when it is set.
532
+ */
533
+ unlimited?: true;
534
+ cycleResetTime?: string;
535
+ }
536
+ /** Token refresh answer; fields the upstream omits stay absent. */
537
+ interface WorkBuddyRefreshOutcome {
538
+ accessToken: string;
539
+ refreshToken?: string;
540
+ expiresInSec?: number;
541
+ domain?: string;
542
+ }
543
+ /** Chat answer: either a live SSE response or a classified failure. */
544
+ type WorkBuddyChatResult = {
545
+ ok: true;
546
+ response: Response;
547
+ } | {
548
+ ok: false;
549
+ status: number;
550
+ kind: UpstreamErrorKind;
551
+ message: string;
552
+ };
553
+ /**
554
+ * Reduce an upstream credits string to its language-neutral display form.
555
+ *
556
+ * The host LLM seam carries this text to the browser, and the host has no
557
+ * locale service — whatever string is produced here is shown verbatim in every
558
+ * UI language. The upstream is inconsistent in a way that matters: some catalog
559
+ * rows report a bare multiplier (`x0.79`) and others append a unit word
560
+ * (`x0.79 credits`), and the unit word would pin the display to English.
561
+ * Dropping a trailing `credits` (case-insensitive, singular or plural) yields
562
+ * the one spelling that reads identically in every language.
563
+ *
564
+ * @param credits - raw upstream credits string, e.g. `"x0.79 credits"`.
565
+ * @returns the bare multiplier, or undefined when nothing displayable remains.
566
+ */
567
+ declare function normalizeCredits(credits: string | undefined): string | undefined;
568
+ /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
569
+ declare function regionOf(domain: string): WorkBuddyRegion;
570
+ /**
571
+ * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
572
+ * force `stream: true` (the upstream rejects non-streaming), flatten
573
+ * `tool_choice` (the upstream's field is a string; object forms return 400),
574
+ * and rewrite `developer` messages as `system`.
575
+ *
576
+ * The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
577
+ * `role: "developer"` (the OpenAI convention it adopted), but the WorkBuddy
578
+ * upstream rejects that role with HTTP 400 code 11128 ("Illegal API
579
+ * invocation from an unapproved channel"). Rewriting to `system` is the
580
+ * compatible spelling the upstream accepts.
581
+ */
582
+ declare function prepareChatBody(source: string): string;
583
+ /** Provenance of one successful catalog fetch, surfaced by the status card. */
584
+ interface WorkBuddyCatalogFetch {
585
+ fetchedAtMs: number;
586
+ /** Which document answered, e.g. `workbuddy-ai:app`. */
587
+ source: string;
588
+ /** UA version used, when the request needed one. */
589
+ appVersion?: AppVersionInfo;
590
+ }
591
+ /** Constructor dependencies. */
592
+ interface WorkBuddyUpstreamClientOptions {
593
+ /** App-version resolver for international catalog requests; injectable for tests. */
594
+ resolveAppVersion?: () => Promise<AppVersionInfo>;
595
+ /**
596
+ * Chat-identity resolver for chat and probe requests; injectable for tests.
597
+ * Defaults to `client-identity.ts`'s per-region chain. Refresh, catalog, and
598
+ * billing never consult it — those requests keep their long-standing headers.
599
+ */
600
+ resolveChatIdentity?: (region: WorkBuddyRegion) => Promise<ChatIdentity>;
601
+ }
602
+ /**
603
+ * Upstream HTTP client. One instance serves the whole plugin; requests take
604
+ * the credential explicitly so token refreshes apply on the next call.
605
+ *
606
+ * One instance is *per variant*: the international provider needs its own
607
+ * catalog source, UA version, and probe differences, and keeping them on the
608
+ * instance avoids passing a variant through every call signature.
609
+ */
610
+ declare class WorkBuddyUpstreamClient {
611
+ /**
612
+ * Resolves the App-shaped UA version for international catalog requests.
613
+ * Injectable so tests never read the real filesystem.
614
+ */
615
+ private readonly resolveAppVersion;
616
+ /** Chat-identity resolver; see {@link WorkBuddyUpstreamClientOptions.resolveChatIdentity}. */
617
+ private readonly resolveChatIdentity;
618
+ /** Provenance of the most recent successful catalog fetch, for the card. */
619
+ lastCatalog: WorkBuddyCatalogFetch | undefined;
620
+ constructor(options?: WorkBuddyUpstreamClientOptions);
621
+ /** POST the chat endpoint; a successful answer is the raw SSE response. */
622
+ chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
623
+ /** POST the token-refresh endpoint; the caller merges the outcome. */
624
+ refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
625
+ /**
626
+ * GET the personal model catalog.
627
+ *
628
+ * Both variants read `/v3/config`, the product document the desktop product
629
+ * itself fetches. CN used to read `/console/enterprises/personal/models`
630
+ * (the console catalog) instead, and that was why its model list drifted
631
+ * from the desktop App's selector: the console document lags the product
632
+ * one, and the product roster itself churns day to day (`auto`,
633
+ * `kimi-k3-1`, `minimax-m3` have each appeared and disappeared within a
634
+ * week).
635
+ *
636
+ * What distinguishes the two variants here is the User-Agent, not the path:
637
+ * the gateway splits `/v3/config` by client identity, and the split is
638
+ * load-bearing. A CLI-shaped UA yields the CLI's roster — the chat models
639
+ * this plugin serves — while an App-shaped UA yields the App's internal
640
+ * roster. CN keeps the CLI UA it sends for chat, so the catalog it
641
+ * advertises is exactly the one its own requests can use. The international
642
+ * variant has no CLI identity, so it keeps the App-shaped UA.
643
+ *
644
+ * Membership is the `cli` roster intersected with the usable rows (see
645
+ * {@link parseModelCatalog}); the promo badges the product document does
646
+ * not carry are merged in from a best-effort console read — see
647
+ * {@link fetchPromoBadges}.
648
+ *
649
+ * Responses are unwrapped and classified the same way — `readEnvelope` plus
650
+ * `envelopeError` — so an expired session or exhausted credit is reported as
651
+ * such rather than as a generic catalog failure.
652
+ */
653
+ fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
654
+ /**
655
+ * Read the console catalog's promotional tags, by model id.
656
+ *
657
+ * `/v3/config` carries no `badge:<label>:<color>` tags — the discount labels
658
+ * the cards render (`限时免费`, `夜间折扣`, …) live only in
659
+ * `/console/enterprises/personal/models`. Since the roster now comes from
660
+ * the product document, those tags are read from the console one in a
661
+ * second request and merged by id.
662
+ *
663
+ * Best-effort by construction: a badge is a label on a price, so failing to
664
+ * read this document must not fail a catalog refresh. Every failure —
665
+ * network, envelope, an unreadable body — returns undefined, and the models
666
+ * simply ship without badges.
667
+ */
668
+ private fetchPromoBadges;
669
+ /**
670
+ * POST the billing endpoint for the aggregated remaining credit.
671
+ *
672
+ * Two upstream shapes, chosen by account type:
673
+ *
674
+ * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks
675
+ * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single
676
+ * cycle quota. The personal endpoint serves these accounts an empty
677
+ * `Accounts` list, which the card then renders as "0 credit" — a wrong
678
+ * number rather than a visible failure (issue #31).
679
+ * - **Everyone else** keeps the personal endpoint unchanged.
680
+ *
681
+ * The region gate is load-bearing: the enterprise endpoint is unverified for
682
+ * the global region, so an international credential that happens to carry an
683
+ * `enterpriseId` must stay on the measured personal path instead of being
684
+ * moved onto an unmeasured one.
685
+ */
686
+ fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
687
+ /**
688
+ * CN enterprise credit read: a single cycle quota instead of a package list.
689
+ *
690
+ * Verified against the WorkBuddy desktop app (`app.asar`,
691
+ * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the
692
+ * body is an empty object and the account identity travels only in the
693
+ * headers. The two official call sites disagree on the field spelling
694
+ * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.
695
+ *
696
+ * A body carrying no recognisable quota field is a hard error rather than a
697
+ * zero. Rendering `0` for "we did not understand the answer" is exactly how
698
+ * issue #31 stayed invisible while users saw a plausible wrong number.
699
+ *
700
+ * The error names fields and types only: it reaches the browser, and the
701
+ * response body may describe the account's usage.
702
+ */
703
+ private fetchEnterpriseCredits;
704
+ /**
705
+ * One probe request: a real streaming chat call carrying the effort under
706
+ * test.
707
+ *
708
+ * Shares `chatHeaders` with the normal chat path on purpose — the plan
709
+ * forbids probing through anything but the plugin's own credential handling,
710
+ * so a result describes what a real message would experience.
711
+ *
712
+ * The caller aborts as soon as a parseable event arrives; the body is never
713
+ * assembled into an answer. `reasoning_effort` is omitted entirely (rather
714
+ * than sent empty) when `effort` is undefined, so the baseline case is a
715
+ * genuinely bare request.
716
+ *
717
+ * Two international differences, both measured on 2026-09-11:
718
+ *
719
+ * - The gateway requires a leading `system` message (400/11128 otherwise), so
720
+ * one is prepended for the global region only.
721
+ * - `max_tokens: 1` is below some models' floor (the GPT-5.6 family rejects it
722
+ * with 400/11133 `integer_below_min_value`), so the international probe asks
723
+ * for a slightly larger minimum. This is a floor the plugin must clear, not
724
+ * evidence about any model's effort support: a model still refusing that
725
+ * minimum is reported as an incompatible request, never as "effort
726
+ * unsupported", and the ceiling is never raised further to force an answer.
727
+ */
728
+ probeEffort(credential: WorkBuddyCredential, model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
729
+ }
730
+ /**
731
+ * Parse either response shape after its envelope has been checked.
732
+ *
733
+ * Membership is the `cli` agent's roster, intersected with the rows that are
734
+ * usable: the roster is what the client identity this plugin presents is
735
+ * allowed to chat with, and joining rather than trusting it outright drops
736
+ * both ids the roster has retired and rows the document lists but cannot
737
+ * serve (no row, `disabled: true`, or non-positive caps all drop out here —
738
+ * a published-but-unservable id must never reach the picker).
739
+ *
740
+ * @param data - the unwrapped catalog/product document.
741
+ * @param international - whether it is the international product document, whose
742
+ * rows carry window objects and promotions.
743
+ * @param promoBadges - badge tags by model id, read from the console document,
744
+ * which is the only one that carries them.
745
+ */
746
+ declare function parseModelCatalog(data: Record<string, unknown>, international?: boolean, promoBadges?: ReadonlyMap<string, readonly string[]>): readonly WorkBuddyUpstreamModel[];
747
+ /**
748
+ * One verified promotion entry.
749
+ *
750
+ * Only the shape actually observed in the international App document is
751
+ * modelled — an enabled, time-boxed, `displayMode: "replace"` discount. An
752
+ * entry that does not match is dropped rather than guessed at: rendering a
753
+ * discount the plugin does not understand could understate what the user pays.
754
+ */
755
+ interface WorkBuddyPromotion {
756
+ /** Window start, epoch ms, parsed from the document's offset timestamp. */
757
+ start: number;
758
+ /** Window end, epoch ms. */
759
+ end: number;
760
+ /** Badge text as the upstream wrote it, e.g. `Free now`. */
761
+ label: string;
762
+ /** Multiplier applied to the model's rate; `0` replaces it outright. */
763
+ factor: number;
764
+ /** Higher wins when several promotions cover one model. */
765
+ priority: number;
766
+ }
767
+ /**
768
+ * Re-evaluate a model's promotion against the current time.
769
+ *
770
+ * Promotions are time-boxed, and the catalog they arrive in is cached for the
771
+ * life of the process. Frozen at parse time, a cached "Free now" would keep
772
+ * claiming a discount after `validUntil` had passed, and would keep showing the
773
+ * pre-discount rate as the discounted one. Re-deriving on every read means the
774
+ * badge disappears on its own and the rate reverts, with no refresh needed.
775
+ *
776
+ * Non-destructive: the model's own `credits` and `badges` are the base, and the
777
+ * promotion is layered onto a copy. A model with no live promotion is returned
778
+ * as-is, so the common case allocates nothing.
779
+ */
780
+ declare function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now?: number): WorkBuddyUpstreamModel;
781
+ /**
782
+ * Apply the international endpoint's extra chat requirement: the first message
783
+ * must be a system prompt.
784
+ *
785
+ * The international gateway rejects a body whose first message is not `system`
786
+ * with HTTP 400 code 11128 ("first message is not system prompt"). Note that
787
+ * the *same* code means something else on the CN endpoint — there it reports a
788
+ * rejected `developer` role — so the two are never branched on by code alone.
789
+ *
790
+ * The added prompt is deliberately empty of user content and prepended, never
791
+ * merged: existing messages keep their order and wording. A body that is not a
792
+ * JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
793
+ * so this is safe to run over an already-prepared-or-not body.
794
+ */
795
+ declare function prepareInternationalChatBody(source: string): string;
796
+ //#endregion
797
+ //#region src/variants.d.ts
798
+ /** One WorkBuddy product variant. */
799
+ interface WorkBuddyVariant {
800
+ /** Provider id registered with DSH, e.g. `workbuddy-ai`. */
801
+ id: string;
802
+ /** Model-group heading and card title stem, e.g. `WorkBuddy AI`. */
803
+ displayName: string;
804
+ /** Desktop app name as users know it, for diagnostics and error copy. */
805
+ appName: string;
806
+ /** Which upstream region this variant's credentials must belong to. */
807
+ region: WorkBuddyRegion;
808
+ /** Env var overriding the desktop auth-file location. */
809
+ env: string;
810
+ /** Basename of the desktop app's own auth file in the shared auth directory. */
811
+ desktopFilename: string;
812
+ /** Basename of the plugin-owned credential copy under `$DSH_HOME`. */
813
+ ownFilename: string;
814
+ /** Basename of the plugin-owned probe-record file under `$DSH_HOME`. */
815
+ probeFilename: string;
816
+ /**
817
+ * Basename of the plugin-owned saved-catalog file under `$DSH_HOME`.
818
+ *
819
+ * One per variant, like the probe records: the two endpoints disagree about
820
+ * rates, windows, and even which models exist for a shared id, so a catalog
821
+ * saved from one must never be served as the other's.
822
+ */
823
+ catalogFilename: string;
824
+ /**
825
+ * Basename of the plugin-owned per-account model-visibility file under
826
+ * `$DSH_HOME`.
827
+ *
828
+ * One per variant, for the same reason as the catalogs and probe records:
829
+ * the two endpoints share model ids, so one variant's hidden list must never
830
+ * answer for the other's picker.
831
+ */
832
+ visibilityFilename: string;
833
+ /** Same-origin status route consumed by this variant's card. */
834
+ statusPath: string;
835
+ /** Same-origin probe-control route consumed by this variant's card. */
836
+ probePath: string;
837
+ }
838
+ /** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
839
+ declare const WORKBUDDY_VARIANTS: readonly WorkBuddyVariant[];
840
+ /** The CN variant; the plugin's long-standing default and compatibility anchor. */
841
+ declare const CN_VARIANT: WorkBuddyVariant;
842
+ /** The international variant. */
843
+ declare const AI_VARIANT: WorkBuddyVariant;
844
+ /** Look up a variant by provider id. */
845
+ declare function variantFor(id: string): WorkBuddyVariant | undefined;
846
+ //#endregion
847
+ //#region src/credential/store.d.ts
848
+ /** Normalized WorkBuddy credential, timestamps in epoch milliseconds. */
849
+ interface WorkBuddyCredential {
850
+ accessToken: string;
851
+ refreshToken: string;
852
+ expiresAtMs: number;
853
+ refreshExpiresAtMs?: number;
854
+ domain: string;
855
+ uid: string;
856
+ enterpriseId?: string;
857
+ nickname?: string;
858
+ /** Which storage the credential was read from; refreshes are always `dsh`. */
859
+ source: 'desktop' | 'dsh';
860
+ }
861
+ /** Read-only sign-in summary for status and doctor output. */
862
+ interface WorkBuddyAuthStatus {
863
+ state: 'signed-in' | 'signed-out';
864
+ expiresAtMs?: number;
865
+ refreshExpiresAtMs?: number;
866
+ nickname?: string;
867
+ domain?: string;
868
+ source?: 'desktop' | 'dsh';
869
+ /**
870
+ * Why no credential is usable, when the reason is diagnosable rather than
871
+ * "nobody is signed in" — a region mismatch being the case that matters.
872
+ * Present only on `signed-out`, and never a substitute for fixing the file.
873
+ */
874
+ reason?: string;
875
+ /**
876
+ * Machine-readable companion to {@link reason}, for callers that must branch
877
+ * on the cause. Never derived by matching `reason` text.
878
+ */
879
+ reasonCode?: WorkBuddySignedOutReasonCode;
880
+ }
881
+ /** Constructor options; only {@link refresh} is required. */
882
+ interface WorkBuddyStoreOptions {
883
+ variant?: WorkBuddyVariant;
884
+ /** Explicit desktop auth-file path, overriding env and platform defaults. */
885
+ desktopPath?: string;
886
+ /** Explicit plugin-owned copy path, defaulting under `$DSH_HOME`. */
887
+ ownPath?: string;
888
+ /** Performs the upstream token refresh. */
889
+ refresh: (credential: WorkBuddyCredential) => Promise<WorkBuddyRefreshOutcome>;
890
+ /** Refresh this long before actual expiry; default five minutes. */
891
+ refreshMarginMs?: number;
892
+ /**
893
+ * Resolver for WorkBuddy 5.6's at-rest protector key, needed when the
894
+ * desktop file stores encrypted token fields. Defaults to the real
895
+ * provider, which spawns the WorkBuddy Electron binary; tests stand in a
896
+ * stub. Structural so a store never depends on how the key is reached.
897
+ */
898
+ keyProvider?: Pick<WorkBuddyAtRestKeyProvider, 'protectorKeyFor' | 'helperPath'>;
899
+ }
900
+ /** Basename of the plugin-owned credential copy inside the Harness home. */
901
+ declare const WORKBUDDY_AUTH_FILENAME = ".workbuddy-auth.json";
902
+ /** Env variable that overrides the desktop auth-file location. */
903
+ declare const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
904
+ /** Plugin-owned copy path inside the Harness home. */
905
+ declare function workbuddyOwnAuthPath(): string;
906
+ /**
907
+ * Platform-default candidates for the WorkBuddy desktop app's auth file, in
908
+ * probe order. Windows probes both AppData roots: current builds write under
909
+ * `%LOCALAPPDATA%` (Local), older ones under `%APPDATA%` (Roaming). Linux
910
+ * probes both XDG bases — most distributions write under the config home,
911
+ * but UOS/deepin builds write under the data home (issue #43), and probing
912
+ * only one silently reads a signed-in app as signed out. WSL probes those
913
+ * same Windows locations through its mounted Windows profile before the
914
+ * native Linux locations.
915
+ */
916
+ declare function defaultDesktopAuthCandidates(): string[];
917
+ /**
918
+ * The platform-default candidates for one variant, in probe order.
919
+ *
920
+ * Both apps write into the *same* shared `CodeBuddyExtension` auth directory
921
+ * and differ only in the file's basename, so the per-platform ordering above
922
+ * is reused verbatim and just the filename is swapped.
923
+ */
924
+ declare function desktopAuthCandidatesFor(variant: WorkBuddyVariant): string[];
925
+ /** First platform-default candidate; see {@link defaultDesktopAuthCandidates}. */
926
+ declare function defaultDesktopAuthPath(variant?: WorkBuddyVariant): string | undefined;
927
+ /**
928
+ * Parse a WorkBuddy auth document in either on-disk shape: the plugin OAuth
929
+ * nested form `{"auth":{...},"account":{...}}` and the flat panel form.
930
+ * Returns undefined when the document carries no access token.
931
+ */
932
+ declare function parseWorkBuddyAuth(text: string): WorkBuddyCredential | undefined;
933
+ /**
934
+ * Read-only credential store with demand-driven refresh.
935
+ *
936
+ * Refresh policy: refresh only when the access token is inside the margin
937
+ * (or already expired), keep the refreshed credential in the plugin-owned
938
+ * copy, and never write the desktop app's file. A failed refresh still
939
+ * returns a not-yet-expired token so an unreachable refresh endpoint does
940
+ * not take down a working session.
941
+ */
942
+ declare class WorkBuddyCredentialStore {
943
+ private readonly variant;
944
+ private readonly refresh;
945
+ private readonly refreshMarginMs;
946
+ private readonly ownPath;
947
+ private readonly keyProvider;
948
+ private desktopPathOverride;
949
+ private inflight;
950
+ constructor(options: WorkBuddyStoreOptions);
951
+ /**
952
+ * Configuration precedence for the desktop file: the plugin's configured
953
+ * path, then the environment variable, then the platform defaults. An
954
+ * explicit path is used verbatim; the defaults are a probe order.
955
+ */
956
+ private resolveDesktopCandidates;
957
+ private resolveDesktopPath;
958
+ /**
959
+ * Repoint the desktop file; a settings change applies on the next read.
960
+ */
961
+ setDesktopPath(path: string | undefined): void;
962
+ /** The resolved desktop auth-file path, for diagnostics. */
963
+ desktopAuthPath(): string | undefined;
964
+ /** The plugin-owned copy path, for diagnostics. */
965
+ ownAuthPath(): string;
966
+ /** Read the freshest stored credential without refreshing anything. */
967
+ current(): Promise<WorkBuddyCredential | undefined>;
968
+ /**
969
+ * The credential to send upstream: {@link current}, refreshed on demand.
970
+ * Single-flight, so parallel requests share one refresh.
971
+ */
972
+ resolve(): Promise<WorkBuddyCredential>;
973
+ /** Read-only sign-in summary; never refreshes and never throws. */
974
+ status(): Promise<WorkBuddyAuthStatus>;
975
+ /** Remove the plugin-owned copy; the desktop file is untouched. */
976
+ logout(): Promise<void>;
977
+ private needsRefresh;
978
+ private refreshNow;
979
+ private saveOwn;
980
+ /**
981
+ * Read the first desktop candidate that exists. Only an absent file
982
+ * (ENOENT) falls through to the next candidate; a file that is present
983
+ * but unparsable is authoritative for its slot, so a stale older-version
984
+ * file never silently wins over a broken newer one.
985
+ *
986
+ * Since WorkBuddy 5.6 the token fields may arrive in at-rest envelopes, so
987
+ * the text is classified before the regular parser sees it. An encrypted
988
+ * document must be *opened*, never skipped; an unrecognized one must fail
989
+ * loudly. The desktop file, as long as it exists, is the identity
990
+ * authority — a document this plugin cannot read must surface as a
991
+ * diagnosis rather than be papered over by the plugin-owned copy, which
992
+ * belongs to whatever account was signed in when it was last refreshed.
993
+ * Only an absent (or empty) file lets the probe continue.
994
+ */
995
+ private readDesktop;
996
+ /** Open a 5.6 encrypted desktop document into the regular credential shape. */
997
+ private openEncryptedDesktop;
998
+ /**
999
+ * Classify the first desktop candidate that exists and carries content;
1000
+ * `absent` when none does. An empty first file is skipped so it cannot mask
1001
+ * a real document on the next candidate. Diagnostics only — it never spawns
1002
+ * the key helper and never decrypts, so doctor can describe the file
1003
+ * without attempting the unlock.
1004
+ */
1005
+ desktopAuthFormat(): Promise<DesktopAuthFormat>;
1006
+ private readOwn;
1007
+ /**
1008
+ * The first desktop candidate the probe would actually read from; `undefined`
1009
+ * when none qualifies. Semantics deliberately match the probe: empty files
1010
+ * are skipped (the probe classifies them as absent and moves on), so on an
1011
+ * XDG layout where the config-home file is empty but the data-home file
1012
+ * holds the credential, diagnostics name the *data-home* file — the one
1013
+ * authentication really uses. Like the probe it never parses or decrypts.
1014
+ */
1015
+ resolvedDesktopAuthPath(): Promise<string | undefined>;
1016
+ /** Whether any desktop-file candidate exists as a regular file; diagnostics only. */
1017
+ desktopFilePresent(): Promise<boolean>;
1018
+ }
1019
+ //#endregion
1020
+ //#region src/catalog/index.d.ts
1021
+ /** One model entry the adapter exposes. */
1022
+ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
1023
+ /**
1024
+ * Static CLI models observed on the CN endpoint (re-verified against the live
1025
+ * `/v3/config` document 2026-09-23, including the thinking-effort and billing
1026
+ * metadata). The upstream refresh replaces this list at startup; it exists so
1027
+ * the provider registers with a usable catalog even while the first fetch is
1028
+ * in flight or offline.
1029
+ *
1030
+ * The list tracks the `cli` agent's model roster exactly — the 16 models it
1031
+ * offered that day. The roster churns quickly (`auto`, `kimi-k3-1`,
1032
+ * `minimax-m3` each appeared or vanished within days, and a competing patch's
1033
+ * 2026-09-22 snapshot named three ids that were gone a day later), so this
1034
+ * table is a boot-time placeholder, never a promise: the live fetch
1035
+ * intersects the day's roster with the document's usable rows, and
1036
+ * `tests/upstream.spec.ts` pins this table to the same parse so the two
1037
+ * cannot drift apart silently. Reasoning metadata is verbatim from the live
1038
+ * document, and the `free` flag follows the normalized `x0.00` credits
1039
+ * marker.
1040
+ *
1041
+ * Deliberately NOT baked in: promotional badges. `限时免费` and friends are
1042
+ * dynamic console-side promotions with no reliable validity window, so a
1043
+ * static table would keep them alive long after the offers end. The live
1044
+ * refresh merges the day's badges best-effort from the console document (see
1045
+ * `fetchPromoBadges` in upstream.ts); until then the rows simply ship
1046
+ * without them.
1047
+ */
1048
+ declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
1049
+ /**
1050
+ * Static CLI models for the international endpoint, captured 2026-09-11 from
1051
+ * the App-form `/v3/config` document (the 20 ids of its `cli` agent, in order).
1052
+ *
1053
+ * Same purpose and same discipline as {@link FALLBACK_WORKBUDDY_MODELS}: it
1054
+ * covers the window before the first successful fetch and an offline start,
1055
+ * and it is deliberately *not* a promise about the upstream's current state.
1056
+ * Reasoning metadata is verbatim from that snapshot. No promo badge is baked
1057
+ * in: promotions are time-boxed (`modelPromotions` carries `validFrom`/
1058
+ * `validUntil`), so hard-coding a "Free now" label would keep claiming a
1059
+ * discount the upstream may have already ended.
1060
+ */
1061
+ declare const FALLBACK_WORKBUDDY_AI_MODELS: readonly WorkBuddyModelInfo[];
1062
+ /**
1063
+ * Mutable catalog shared by the shim's `/v1/models` and the adapter.
1064
+ *
1065
+ * Visibility is separate from content. A variant whose app has no credentials
1066
+ * must expose *no* models rather than a fallback roster: the DSH model picker
1067
+ * drops an empty group, so an empty catalog is exactly how a provider hides
1068
+ * without touching registration. Serving the fallback to a signed-out user
1069
+ * instead offers models that can only fail (`store.resolve()` throws on the
1070
+ * first message), which is worse than showing nothing.
1071
+ *
1072
+ * The flag defaults to visible so a directly-constructed catalog behaves as it
1073
+ * always has; the plugin runtime applies the credential gate.
1074
+ */
1075
+ declare class WorkBuddyCatalog {
1076
+ private models;
1077
+ private visible;
1078
+ private useMaximumContextWindow;
1079
+ constructor(initial?: readonly WorkBuddyModelInfo[]);
1080
+ /** Current entries; empty while the variant has no usable credential. */
1081
+ current(): readonly WorkBuddyModelInfo[];
1082
+ /** Replace the list; callers invalidate their adapter snapshot after this. */
1083
+ set(models: readonly WorkBuddyModelInfo[]): void;
1084
+ /** Whether this variant's models are exposed at all. */
1085
+ isVisible(): boolean;
1086
+ /**
1087
+ * Show or hide the whole catalog. Returns whether the value changed, so the
1088
+ * caller can skip an invalidation that would re-render an identical list.
1089
+ */
1090
+ setVisible(visible: boolean): boolean;
1091
+ /** Select the largest declared international window where the upstream offers one. */
1092
+ setUseMaximumContextWindow(useMaximum: boolean): boolean;
1093
+ /** Models to fall back to when the upstream fetch fails; ignores visibility. */
1094
+ fallback(): readonly WorkBuddyModelInfo[];
1095
+ }
1096
+ //#endregion
1097
+ //#region src/probe/store.d.ts
1098
+ /** Basename of the probe record inside the Harness home. */
1099
+ declare const WORKBUDDY_PROBE_FILENAME = ".workbuddy-probe.json";
1100
+ /**
1101
+ * Whether the model's effort parameter is actually validated.
1102
+ *
1103
+ * - `validating`: the upstream rejected an unknown sentinel value, so a
1104
+ * per-level answer is meaningful.
1105
+ * - `non-validating`: the upstream accepted the sentinel, so it ignores or
1106
+ * loosely coerces the parameter and no per-level answer can be trusted.
1107
+ * - `unknown`: baseline or sentinel failed for an unrelated reason (auth,
1108
+ * rate limit, transport, ambiguous error body). Not a negative claim.
1109
+ */
1110
+ type WorkBuddyProbeValidation = 'validating' | 'non-validating' | 'unknown';
1111
+ /** One model's recorded observation. */
1112
+ interface WorkBuddyProbeRecord {
1113
+ /** Fingerprint of the catalog row this observation was made against. */
1114
+ fingerprint: string;
1115
+ validation: WorkBuddyProbeValidation;
1116
+ /** Efforts verified as accepted; only ever non-empty for `validating`. */
1117
+ efforts: readonly WorkBuddyEffort[];
1118
+ /** When the probe ran, epoch milliseconds. */
1119
+ probedAtMs: number;
1120
+ /** Plugin version that produced the record. */
1121
+ pluginVersion: string;
1122
+ /**
1123
+ * The account this observation was made under, as `uid:enterpriseId`.
1124
+ *
1125
+ * An effort set is a fact about one account's entitlement as much as about
1126
+ * the model: the same model id can accept different levels under a different
1127
+ * subscription. Records are stored under this identity and only ever served
1128
+ * back to it, so one account never inherits another's detected levels — and
1129
+ * because the store nests by this identity, switching back finds this
1130
+ * account's own records intact rather than re-probing from scratch.
1131
+ */
1132
+ account: string;
1133
+ }
1134
+ /**
1135
+ * Plugin-owned probe record path inside the Harness home.
1136
+ *
1137
+ * One file per variant. Same-named models exist on both endpoints (the
1138
+ * international catalog repeats `glm-5.3`, `glm-5.2`, `hy3`, `kimi-k2.6`), and
1139
+ * {@link fingerprintModel} covers only `id`/`reasoning`/`supportsImages` —
1140
+ * never the provider — so a single shared file would let one variant's
1141
+ * observation answer for the other. The paths differ; the format does not.
1142
+ */
1143
+ declare function workbuddyProbePath(filename?: string): string;
1144
+ /**
1145
+ * Fingerprint the catalog fields a probe depends on.
1146
+ *
1147
+ * Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
1148
+ * so a rename or a promo badge does not throw away a valid observation, and
1149
+ * deliberately includes the whole reasoning object so any change to the
1150
+ * declared shape re-probes.
1151
+ */
1152
+ declare function fingerprintModel(info: WorkBuddyModelInfo): string;
1153
+ /** Options for {@link WorkBuddyProbeStore}. */
1154
+ interface WorkBuddyProbeStoreOptions {
1155
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
1156
+ path?: string;
1157
+ /** Observation lifetime; defaults to 14 days. */
1158
+ ttlMs?: number;
1159
+ /** Plugin version stamped into new records. */
1160
+ pluginVersion: string;
1161
+ /** Clock injection for tests. */
1162
+ now?: () => number;
1163
+ }
1164
+ /**
1165
+ * The plugin's probe records: read once, written atomically, keyed by the
1166
+ * account that produced each observation, and never trusted across a
1167
+ * fingerprint change or past the TTL.
1168
+ */
1169
+ declare class WorkBuddyProbeStore {
1170
+ private readonly path;
1171
+ private readonly ttlMs;
1172
+ private readonly pluginVersion;
1173
+ private readonly now;
1174
+ private records;
1175
+ constructor(options: WorkBuddyProbeStoreOptions | string);
1176
+ /** Resolved state-file path, for the CLI and tests. */
1177
+ filePath(): string;
1178
+ private load;
1179
+ /**
1180
+ * The usable record for one account and model, or `undefined` when there is
1181
+ * none, it is expired, it was taken against a different catalog row, or it
1182
+ * belongs to a different account.
1183
+ *
1184
+ * @param account - the account in effect, as `uid:enterpriseId`. Records are
1185
+ * only returned for the account that produced them.
1186
+ */
1187
+ get(modelId: string, fingerprint: string, account: string): WorkBuddyProbeRecord | undefined;
1188
+ /**
1189
+ * Store one observation under the account stamped on it. Only a decisive
1190
+ * answer (`validating` / `non-validating`) replaces an existing decisive
1191
+ * record *of the same account*: a transient `unknown` must not erase
1192
+ * knowledge the user already paid for.
1193
+ */
1194
+ set(modelId: string, record: WorkBuddyProbeRecord): void;
1195
+ /** Drop every record of every account; used by the card's explicit "clear" action. */
1196
+ clear(): void;
1197
+ /** Every record currently held, grouped by account, for status display. */
1198
+ all(): Readonly<Record<string, Readonly<Record<string, WorkBuddyProbeRecord>>>>;
1199
+ /** Build a record stamped with this store's clock, version, and account. */
1200
+ record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[], account: string): WorkBuddyProbeRecord;
1201
+ /**
1202
+ * Write through a temporary file and rename, so a crash mid-write cannot
1203
+ * leave a half-parsed document that reads as "no records" and silently drops
1204
+ * every observation.
1205
+ */
1206
+ private persist;
1207
+ }
1208
+ //#endregion
1209
+ //#region src/llm/shim.d.ts
1210
+ /** Minimal logger surface the plugin context already provides. */
1211
+ interface ShimLogger {
1212
+ warn(...args: unknown[]): void;
1213
+ error(...args: unknown[]): void;
1214
+ }
1215
+ /** What the plugin needs from a running shim. */
1216
+ interface WorkBuddyShim {
1217
+ /** Resolves once the listener is up; rejects if listening failed. */
1218
+ ready: Promise<void>;
1219
+ /** The shim origin, e.g. `http://127.0.0.1:39271`; valid after ready. */
1220
+ baseUrl(): string;
1221
+ /**
1222
+ * The per-process shared secret the plugin's own client must carry as
1223
+ * `Authorization: Bearer <token>`. Lives only in memory; the adapter
1224
+ * resolves this instead of the upstream access token, because the shim
1225
+ * resolves the real credential itself via the store.
1226
+ */
1227
+ token(): string;
1228
+ /** Stop serving and destroy open connections. */
1229
+ close(): Promise<void>;
1230
+ }
1231
+ /** Constructor dependencies. */
1232
+ interface WorkBuddyShimOptions {
1233
+ store: WorkBuddyCredentialStore;
1234
+ client: Pick<WorkBuddyUpstreamClient, 'chatStream'>;
1235
+ catalog: WorkBuddyCatalog;
1236
+ logger?: ShimLogger;
1237
+ }
1238
+ /**
1239
+ * Start the loopback endpoint. Requests carry any bearer; the loopback bind
1240
+ * is the boundary, and the upstream credential comes from the store alone.
1241
+ */
1242
+ declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddyShim;
1243
+ //#endregion
1244
+ //#region src/llm/adapter.d.ts
1245
+ /** Provider route this bundle owns. */
1246
+ declare const WORKBUDDY_PROVIDER = "workbuddy";
1247
+ /** Provider idle ceiling while one stream read is outstanding. */
1248
+ declare const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 300000;
1249
+ /** Constructor dependencies. */
1250
+ interface WorkBuddyAdapterOptions {
1251
+ providerId?: string;
1252
+ displayName?: string;
1253
+ shim: WorkBuddyShim;
1254
+ store: WorkBuddyCredentialStore;
1255
+ catalog: WorkBuddyCatalog;
1256
+ /** Resolve the durable attachment service at request time, when present. */
1257
+ resolveAttachments?: () => AttachmentStore | undefined;
1258
+ /**
1259
+ * Look up a local probe observation for a model. Consulted only for rows the
1260
+ * upstream left undeclared; absent means declared-set-only behavior.
1261
+ */
1262
+ observe?: (modelId: string) => WorkBuddyProbeRecord | undefined;
1263
+ /**
1264
+ * Model ids the current account has hidden from the picker, resolved per
1265
+ * read so an account switch is honored without rebuilding the adapter.
1266
+ *
1267
+ * Hiding is a *listing* concern only: `buildModels()` keeps serving the full
1268
+ * catalog because pi-ai's `resolveModel`/`prepareCall` resolve from the same
1269
+ * snapshot `listModels` reads — filtering the descriptors there would make a
1270
+ * hidden model unresolvable and break sessions already using it. The filter
1271
+ * therefore lives in this adapter's `listModels` override alone.
1272
+ */
1273
+ hidden?: () => readonly string[];
1274
+ }
1275
+ /** What {@link createWorkBuddyAdapter} hands back. */
1276
+ interface WorkBuddyAdapter {
1277
+ adapter: PiAiAdapter;
1278
+ /** Rebuild the adapter's provider snapshot; call after a catalog update. */
1279
+ invalidate: () => void;
1280
+ }
1281
+ /**
1282
+ * Assemble the adapter. The provider's `getModels` reads the live catalog,
1283
+ * and every model's `baseUrl` is re-resolved per read so the shim's
1284
+ * ephemeral port applies from the first snapshot after startup.
1285
+ *
1286
+ * The profile is constructed by hand rather than through dsh-llm-pi-ai's
1287
+ * internal `resolveProfiles()`: that helper is not part of the package's
1288
+ * public export surface (root entry, `lib/` deep imports blocked by the
1289
+ * exports map, `src/` not shipped), so hand-assembly is the only supported
1290
+ * path and every newly required field must be adopted here explicitly —
1291
+ * `modelErrors` is one such field.
1292
+ */
1293
+ declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
1294
+ //#endregion
1295
+ //#region src/catalog/store.d.ts
1296
+ /** Basename of the CN variant's saved catalog inside the Harness home. */
1297
+ declare const WORKBUDDY_CATALOG_FILENAME = ".workbuddy-catalog.json";
1298
+ /** One saved catalog: the account it belonged to, and the models it listed. */
1299
+ interface SavedCatalog {
1300
+ /** `uid:enterpriseId` the catalog was fetched for. */
1301
+ account: string;
1302
+ /** Which document answered, so a CN roster is never served as an AI one. */
1303
+ source: string;
1304
+ /** When the fetch succeeded, epoch milliseconds. */
1305
+ fetchedAtMs: number;
1306
+ models: readonly WorkBuddyUpstreamModel[];
1307
+ /** App version used as the UA, when the variant needed one. */
1308
+ appVersion?: string;
1309
+ }
1310
+ /** Plugin-owned saved-catalog path inside the Harness home. */
1311
+ declare function workbuddyCatalogPath(filename?: string): string;
1312
+ /** Options for {@link WorkBuddyCatalogStore}. */
1313
+ interface WorkBuddyCatalogStoreOptions {
1314
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
1315
+ path?: string;
1316
+ }
1317
+ /**
1318
+ * The last successful catalog per account, read once and written atomically.
1319
+ *
1320
+ * Malformed content reads as "nothing saved" rather than throwing: this file
1321
+ * is an optimization for the offline and first-seconds cases, and a corrupt one
1322
+ * must never be able to stop the plugin from serving models.
1323
+ */
1324
+ declare class WorkBuddyCatalogStore {
1325
+ private readonly path;
1326
+ private entries;
1327
+ constructor(options?: WorkBuddyCatalogStoreOptions | string);
1328
+ /** Resolved state-file path, for the CLI and tests. */
1329
+ filePath(): string;
1330
+ private load;
1331
+ /** The saved catalog for one account, or `undefined` when there is none. */
1332
+ get(account: string): SavedCatalog | undefined;
1333
+ /**
1334
+ * Remember a catalog for an account, replacing whatever was saved before.
1335
+ *
1336
+ * A failed write is swallowed: the plugin has already served these models,
1337
+ * and losing the *memory* of them is not worth surfacing.
1338
+ */
1339
+ set(account: string, catalog: Omit<SavedCatalog, 'account'>): void;
1340
+ /** Forget one account's catalog — used when that account signs out. */
1341
+ delete(account: string): void;
1342
+ private persist;
1343
+ }
1344
+ //#endregion
1345
+ //#region src/catalog/visibility.d.ts
1346
+ /**
1347
+ * Per-account model-visibility preferences: which models the signed-in account
1348
+ * has hidden from the DSH model picker (issue #36).
1349
+ *
1350
+ * A hidden-*id* list, deliberately not an enabled whitelist: a new account and a
1351
+ * model the upstream adds both start visible, and an id that temporarily
1352
+ * disappears from the catalog is kept — when the model returns it stays hidden
1353
+ * until this account says otherwise. Entries are also kept across sign-outs, so
1354
+ * returning to an account restores exactly what it left.
1355
+ *
1356
+ * One file per variant (the two endpoints share model ids but never
1357
+ * preferences), keyed by the same `uid:enterpriseId` identity the saved
1358
+ * catalogs and probe records use. Not a place for secrets: model-id strings
1359
+ * only, never a token, and never written into the desktop auth file or the
1360
+ * plugin-owned credential copy — hiding a model is a picker preference, not
1361
+ * credential state.
1362
+ *
1363
+ * Why a plugin-owned file rather than a settings section: the settings sections
1364
+ * are statically-typed schemastery objects, and `settings.yaml` is account-global
1365
+ * — a per-uid dynamic map fits neither without weakening the schema or mixing
1366
+ * one account's preferences into another's config. The saved-catalog and probe
1367
+ * stores already persist per-account data this way, so this store follows them:
1368
+ * version-tagged document, atomic write with `0o600`, and a malformed file that
1369
+ * reads as "nothing saved" rather than throwing.
1370
+ *
1371
+ * @module dsh-workbuddy-bridge/visibility-store
1372
+ */
1373
+ /** Basename of the CN variant's visibility file inside the Harness home. */
1374
+ declare const WORKBUDDY_VISIBILITY_FILENAME = ".workbuddy-model-visibility.json";
1375
+ /** Plugin-owned visibility-file path inside the Harness home. */
1376
+ declare function workbuddyVisibilityPath(filename?: string): string;
1377
+ /** Options for {@link WorkBuddyVisibilityStore}. */
1378
+ interface WorkBuddyVisibilityStoreOptions {
1379
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
1380
+ path?: string;
1381
+ }
1382
+ /**
1383
+ * The per-account hidden-model lists, read once and written atomically.
1384
+ *
1385
+ * Unlike the saved-catalog store, a failed *write* propagates: the caller
1386
+ * reports it to the user rather than answering "hidden" for a preference that
1387
+ * did not persist. Reads stay forgiving — a corrupt or unreadable file is
1388
+ * "nothing hidden", which only ever shows models the account can still pick.
1389
+ */
1390
+ declare class WorkBuddyVisibilityStore {
1391
+ private readonly path;
1392
+ private accounts;
1393
+ constructor(options?: WorkBuddyVisibilityStoreOptions | string);
1394
+ /** Resolved state-file path, for the CLI and tests. */
1395
+ filePath(): string;
1396
+ private load;
1397
+ /** The model ids one account has hidden; empty when it never hid any. */
1398
+ hidden(account: string): readonly string[];
1399
+ /**
1400
+ * Show or hide one model for one account, persisting before committing.
1401
+ *
1402
+ * Re-enabling (showing) the last hidden model removes the account's entry
1403
+ * entirely — an absent entry and an empty list mean the same thing
1404
+ * (everything visible), and the file should not accumulate empty buckets.
1405
+ * Throws when the write fails, leaving the in-memory state untouched so a
1406
+ * re-read cannot lie about what was persisted.
1407
+ */
1408
+ setVisible(account: string, model: string, visible: boolean): void;
1409
+ private persist;
1410
+ }
1411
+ //#endregion
1412
+ //#region src/probe/service.d.ts
1413
+ /** What the caller learns about a completed probe. */
1414
+ type WorkBuddyProbeStatus = {
1415
+ state: 'ok';
1416
+ validation: WorkBuddyProbeRecord['validation'];
1417
+ efforts: readonly string[];
1418
+ requests: number;
1419
+ } | {
1420
+ state: 'unavailable';
1421
+ reason: string;
1422
+ };
1423
+ /** Options for {@link WorkBuddyProbeService}. */
1424
+ interface WorkBuddyProbeServiceOptions {
1425
+ store: WorkBuddyProbeStore;
1426
+ catalog: WorkBuddyCatalog;
1427
+ credentials: WorkBuddyCredentialStore;
1428
+ client: WorkBuddyUpstreamClient;
1429
+ /** Whether probing is permitted at all; consulted before every sweep. */
1430
+ consent: () => boolean;
1431
+ /**
1432
+ * The account currently in effect, as `uid:enterpriseId`, or `undefined`
1433
+ * while signed out.
1434
+ *
1435
+ * Records are read and written against this identity, and it is re-checked
1436
+ * after the sweep finishes: an observation produced under account A must not
1437
+ * be stored once account B is in effect, however long the probe took. The
1438
+ * store's per-account keying alone cannot catch that, because an in-flight
1439
+ * probe completes *after* the switch has already happened.
1440
+ */
1441
+ account: () => string | undefined;
1442
+ sentinel?: SentinelFactory;
1443
+ /** Injectable for tests; defaults to the live upstream sender. */
1444
+ send?: (modelId: string) => ProbeSender;
1445
+ }
1446
+ /**
1447
+ * Serial probe runner. One instance is shared by the manual API and any
1448
+ * future automatic trigger, so the two can never overlap.
1449
+ */
1450
+ declare class WorkBuddyProbeService {
1451
+ private readonly options;
1452
+ private queue;
1453
+ private readonly pending;
1454
+ private running;
1455
+ constructor(options: WorkBuddyProbeServiceOptions);
1456
+ /** Whether a sweep is in flight right now. */
1457
+ isRunning(): boolean;
1458
+ /**
1459
+ * The record the adapter may use for this model, or `undefined`.
1460
+ *
1461
+ * Applies the plan's precedence (§5): a declared set always wins, so a model
1462
+ * that declares `supportedEfforts` is never answered from an observation.
1463
+ */
1464
+ recordFor(modelId: string): WorkBuddyProbeRecord | undefined;
1465
+ /**
1466
+ * Probe one model, serially.
1467
+ *
1468
+ * The authenticated manual route supplies one-request consent after UI
1469
+ * confirmation. Other callers must pass the configured consent gate.
1470
+ * Manual consent never changes the automatic-probing configuration.
1471
+ * Explicit requests bypass historical results, but share an ongoing run.
1472
+ */
1473
+ probe(modelId: string, manualConsent?: boolean): Promise<WorkBuddyProbeStatus>;
1474
+ }
1475
+ //#endregion
1476
+ //#region src/web/heartbeat.d.ts
1477
+ /**
1478
+ * Host-side heartbeat: a small JSON file written under `$DSH_HOME` once the
1479
+ * `workbuddy` provider is registered. The status CLI reads it to report
1480
+ * whether the host bundle is alive, independent of the browser card.
1481
+ *
1482
+ * The browser (client) bundle cannot write files; its health is reported
1483
+ * only through `console.error` on failure (see `src/client/index.tsx`).
1484
+ * This asymmetry is intentional: the host is the load-bearing half, and
1485
+ * a missing heartbeat unambiguously means the host never started.
1486
+ *
1487
+ * @module dsh-workbuddy-bridge/host-heartbeat
1488
+ */
1489
+ /** Basename of the host heartbeat file inside the Harness home. */
1490
+ declare const WORKBUDDY_HOST_HEARTBEAT_FILENAME = ".workbuddy-host-heartbeat.json";
1491
+ /** Current on-disk heartbeat format; readers reject others. */
1492
+ declare const HEARTBEAT_FORMAT_VERSION = 1;
1493
+ /** On-disk shape of the heartbeat. */
1494
+ interface WorkBuddyHostHeartbeat {
1495
+ version: typeof HEARTBEAT_FORMAT_VERSION;
1496
+ package: 'dsh-workbuddy-bridge';
1497
+ pluginVersion: string;
1498
+ /** Epoch milliseconds when the host registered the provider. */
1499
+ registeredAt: number;
1500
+ /** Host process PID, to distinguish a stale heartbeat after a crash. */
1501
+ pid: number;
1502
+ }
1503
+ /** Absolute path of the host heartbeat file. */
1504
+ declare function workbuddyHostHeartbeatPath(): string;
1505
+ /** Remove the heartbeat on plugin disposal so a stale file does not linger. */
1506
+ declare function clearHostHeartbeat(): Promise<void>;
1507
+ /** Read and validate the heartbeat; returns `undefined` when absent or malformed. */
1508
+ declare function readHostHeartbeat(): Promise<WorkBuddyHostHeartbeat | undefined>;
1509
+ /**
1510
+ * Absolute start time (epoch ms) of the process holding `pid`, or `undefined`
1511
+ * when it cannot be determined (no such PID, platform lacks a readable source).
1512
+ *
1513
+ * - macOS / Linux: `ps -o lstart=` prints a local-time "EEE MMM DD HH:MM:SS YYYY";
1514
+ * `Date.parse` resolves it against the local clock, which matches how
1515
+ * `registeredAt` (a `Date.now()` absolute value) is expressed.
1516
+ * - Windows: `wmic` prints a CIM_DATETIME `CreationDate` — local fields plus a
1517
+ * signed minute offset (see {@link parseWmiCreationDate}); the epoch it
1518
+ * yields is comparable to `registeredAt`.
1519
+ *
1520
+ * Failures return `undefined` so callers can fall back to plain PID liveness
1521
+ * rather than mis-report a running host as dead.
1522
+ */
1523
+ declare function processStartTimeMs(pid: number): number | undefined;
1524
+ /**
1525
+ * Whether the heartbeat's PID is still alive *and* still the same process that
1526
+ * registered it. A stale heartbeat (host crashed without clearing the file)
1527
+ * is distinguished from a live host by two checks:
1528
+ *
1529
+ * 1. `process.kill(pid, 0)` — the PID exists (signal 0 tests existence).
1530
+ * 2. The process holding that PID started at or before `registeredAt`. A host
1531
+ * that registered the heartbeat must have been started before writing it,
1532
+ * so `start <= registeredAt`; a recycled PID belongs to an unrelated process
1533
+ * started after the host died, so `start > registeredAt` correctly reads dead.
1534
+ *
1535
+ * PID-only detection is not enough: after a crash the OS may hand the same PID
1536
+ * to an unrelated process, and the un-cleared stale heartbeat would otherwise
1537
+ * produce a false "Host running". When the process start time cannot be read
1538
+ * (e.g. unsupported platform) the check degrades to plain PID liveness.
1539
+ */
1540
+ declare function isHeartbeatProcessAlive(heartbeat: WorkBuddyHostHeartbeat): boolean;
1541
+ //#endregion
1542
+ //#region src/index.d.ts
1543
+ /** Stable Cordis plugin name. */
1544
+ declare const name = "llm-workbuddy";
1545
+ /** The model registry required before the provider can register. */
1546
+ declare const inject: string[];
1547
+ /**
1548
+ * Plugin configuration.
1549
+ *
1550
+ * No field is optional: an empty string means "not set", which is also what
1551
+ * the form shows. That is forced by `.volatile()` — a volatile field always
1552
+ * carries a value, so an optional marker here would describe a different type
1553
+ * than the schema produces (and `exactOptionalPropertyTypes` rejects the
1554
+ * mismatch rather than letting it slide).
1555
+ */
1556
+ interface Config {
1557
+ /** Explicit WorkBuddy (CN) desktop auth-file path, overriding env and platform defaults; empty means "use the app's own". */
1558
+ authFile: string;
1559
+ /** Explicit WorkBuddy AI (international) desktop auth-file path; empty means "use the app's own". */
1560
+ authFileAI: string;
1561
+ /**
1562
+ * Whether the user has authorized sending probe requests about reasoning
1563
+ * efforts. Off by default: a probe spends real credit, so nothing is sent
1564
+ * until the user explicitly agrees.
1565
+ */
1566
+ probeConsent: boolean;
1567
+ /** Use the largest context window the international catalog explicitly offers. */
1568
+ useMaximumContextWindow: boolean;
1569
+ }
1570
+ /**
1571
+ * Every field is `.volatile()`, and both reasons matter:
1572
+ *
1573
+ * 1. **Only volatile fields reach the form.** The host's `volatileForm()` returns
1574
+ * `undefined` when a schema has no volatile field, which drops the whole entry
1575
+ * out of `describe()` — so `ctx.configForms.get(ENTRY_ID)` never becomes ready
1576
+ * and the plugin's configuration area stays **empty**, with no error anywhere.
1577
+ * A single field missing `.volatile()` does not fail loudly either; it just
1578
+ * silently vanishes from the form.
1579
+ * 2. **The write path is allow-listed per volatile path.** A non-volatile path is
1580
+ * rejected outright (`Config field "..." is not volatile`), so a form that did
1581
+ * show the field could never save it.
1582
+ *
1583
+ * `.default()` must come **before** `.volatile()`: the former fixes the mode, the
1584
+ * latter then yields `Volatile<T>` rather than `Volatile<T | undefined>`.
1585
+ *
1586
+ * The side effect is welcome: with every field volatile the Loader always
1587
+ * concludes "only volatile fields changed", so editing configuration never
1588
+ * remounts the plugin and `apply` runs exactly once.
1589
+ *
1590
+ * Host sources: `packages/settings/settings/src/schema.ts` (`volatileForm`) and
1591
+ * `settings/src/index.ts` (`write`).
1592
+ */
1593
+ /**
1594
+ * The reference face `apply` receives: every field is a cell whose `get()`
1595
+ * returns the current value.
1596
+ *
1597
+ * An all-volatile schema is what makes that so — the Loader commits new values
1598
+ * into the running references and remounts only the volatile parts, so this
1599
+ * plugin never remounts and `apply` runs exactly once. It also means a value
1600
+ * must never be captured into a field: read it through {@link configOf} at the
1601
+ * point of use, because the reference always holds the newest value.
1602
+ *
1603
+ * Host sources: `vendor/cosmokit/src/volatile.ts` (`Volatile<T>` exposes only
1604
+ * `get()`) and `vendor/loader/src/config/entry.ts` (`_commitVolatile`).
1605
+ */
1606
+ type ConfigRefs = { readonly [K in keyof Config]: Volatile<Config[K]>; };
1607
+ declare const Config: z<Schemastery.ObjectS<NoInfer<{
1608
+ authFile: z<string, string, "volatile-defined">;
1609
+ authFileAI: z<string, string, "volatile-defined">;
1610
+ probeConsent: z<boolean, boolean, "volatile-defined">;
1611
+ useMaximumContextWindow: z<boolean, boolean, "volatile-defined">;
1612
+ }>>, Schemastery.ObjectT<NoInfer<{
1613
+ authFile: z<string, string, "volatile-defined">;
1614
+ authFileAI: z<string, string, "volatile-defined">;
1615
+ probeConsent: z<boolean, boolean, "volatile-defined">;
1616
+ useMaximumContextWindow: z<boolean, boolean, "volatile-defined">;
1617
+ }>>, "plain">;
1618
+ /**
1619
+ * The account key model-visibility preferences are stored under: the stable
1620
+ * identity, but only when it carries a uid.
1621
+ *
1622
+ * A credential whose desktop document carried no `account.uid` normalizes to
1623
+ * an empty string; keying preferences on the resulting `":enterpriseId"` would
1624
+ * silently share one bucket between every such account. Those accounts get no
1625
+ * per-account preferences at all — everything stays visible and the control
1626
+ * route explains the refusal — which is the only honest degradation: it never
1627
+ * applies one account's hidden list to another.
1628
+ */
1629
+ declare function visibilityAccountOf(credential: Pick<WorkBuddyCredential, 'uid' | 'enterpriseId'>): string | undefined;
1630
+ /**
1631
+ * Start both variants: their loopback endpoints, the `workbuddy` and
1632
+ * `workbuddy-ai` providers, their configuration cards, and their
1633
+ * credential-driven catalog lifecycles.
1634
+ *
1635
+ * Each variant registers unconditionally; what varies is whether its catalog is
1636
+ * *visible*. An empty catalog is how DSH hides a model group (the host filters
1637
+ * out groups with no models), which keeps a sign-in that happens after startup
1638
+ * working without re-registering the provider.
1639
+ */
1640
+ declare function apply(ctx: Context, refs: ConfigRefs): void;
1641
+ //#endregion
1642
+ export { AI_VARIANT, type AppVersionInfo, CN_APP_VERSION_FILENAME, CN_VARIANT, type ChatIdentity, Config, ConfigRefs, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type ResolveChatIdentityOptions, type UpstreamErrorKind, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, type WorkBuddyAdapter, type WorkBuddyAppVersionSource, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyCatalogFetch, WorkBuddyCatalogStore, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyPromotion, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyVariant, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };