dsh-workbuddy-connect 0.3.2 → 0.5.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 CHANGED
@@ -1,8 +1,136 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import "@earendil-works/pi-ai";
2
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
3
4
  import { Context } from "@deepseek-ai/cordis";
4
5
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
5
6
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
7
+ //#region src/app-version.d.ts
8
+ /** Basename of the saved version under `$DSH_HOME`. */
9
+ declare const WORKBUDDY_APP_VERSION_FILENAME = ".workbuddy-ai-version.json";
10
+ /** Where the version came from, for `doctor` output. */
11
+ type WorkBuddyAppVersionSource = 'installed' | 'saved' | 'fallback';
12
+ /** Resolved version plus provenance. */
13
+ interface AppVersionInfo {
14
+ version: string;
15
+ source: WorkBuddyAppVersionSource;
16
+ /** Basename of the App bundle the version was read from, when installed. */
17
+ bundle?: string;
18
+ }
19
+ /**
20
+ * Whether a string is safe to interpolate into an HTTP header.
21
+ *
22
+ * Strict on purpose: the value reaches a header, so anything that could split
23
+ * the request (CR, LF, spaces beyond the separator) or inject a second UA
24
+ * token must never pass. The App's own version is always `N.N.N` or `N.N.N.N`.
25
+ */
26
+ declare function validAppVersion(value: unknown): value is string;
27
+ /**
28
+ * Read `CFBundleShortVersionString` out of an `Info.plist`.
29
+ *
30
+ * Parsed as XML rather than grepped, because the plist contains several
31
+ * `<string>` values and a regex would be one unrelated key away from
32
+ * returning the wrong one. A binary plist has no `<dict>` in its bytes and is
33
+ * reported as unreadable (the saved value then applies) rather than guessed at.
34
+ */
35
+ declare function readBundleVersion(plistPath: string): Promise<string | undefined>;
36
+ /**
37
+ * The installed international App's version, or `undefined` when it is not
38
+ * installed (or not readable).
39
+ *
40
+ * Windows and Linux have no verified bundle-metadata location yet, so this
41
+ * returns `undefined` there and the saved/fallback value is used instead of
42
+ * guessing a path — the same discipline the credential discovery follows.
43
+ */
44
+ declare function installedAppVersion(): Promise<{
45
+ version: string;
46
+ bundle: string;
47
+ } | undefined>;
48
+ /** Constructor dependencies; all injectable so tests never touch the real FS. */
49
+ interface ResolveAppVersionOptions {
50
+ /** Installed-version reader; defaults to {@link installedAppVersion}. */
51
+ installed?: () => Promise<{
52
+ version: string;
53
+ bundle: string;
54
+ } | undefined>;
55
+ /** Saved-version path; defaults to {@link appVersionPath}. */
56
+ path?: string;
57
+ }
58
+ /**
59
+ * Resolve the UA version: installed App first, then the last saved value, then
60
+ * the compiled-in fallback.
61
+ *
62
+ * A value read from the App is written back immediately, so an uninstalled App
63
+ * or an unreadable plist later still has the last real version to fall back
64
+ * on. The write is best-effort: failing to cache a version must never fail the
65
+ * catalog request that asked for it.
66
+ */
67
+ declare function resolveAppVersion(options?: ResolveAppVersionOptions): Promise<AppVersionInfo>;
68
+ /**
69
+ * Build the App-shaped User-Agent for catalog requests.
70
+ *
71
+ * `WorkBuddyAI/<version>` with no space is the form measured to reach the App
72
+ * document; the space form is rejected with 400/12403. Throws on an invalid
73
+ * version rather than sending a malformed header.
74
+ */
75
+ declare function appUserAgent(version: string): string;
76
+ //#endregion
77
+ //#region src/probe.d.ts
78
+ /**
79
+ * The canonical values a probe tests, in a fixed order.
80
+ *
81
+ * `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
82
+ * by policy — disabling thinking is a separate capability the upstream must
83
+ * declare through `canDisableThinking`, never something probing may infer.
84
+ */
85
+ declare const PROBE_EFFORT_CANDIDATES: readonly WorkBuddyEffort[];
86
+ /** Sentinel generator; injectable so tests get deterministic values. */
87
+ type SentinelFactory = () => string;
88
+ /** Default sentinel: unmistakably non-canonical, different on every call. */
89
+ declare function randomSentinel(): string;
90
+ /**
91
+ * One response as the probe sees it, split into the only distinctions the
92
+ * attribution rule needs.
93
+ */
94
+ interface ProbeAttempt {
95
+ /** HTTP status, or 0 for a transport failure. */
96
+ status: number;
97
+ /** True when a parseable SSE event arrived. */
98
+ streamed: boolean;
99
+ /** `extError.code` from a JSON error body, when present. */
100
+ errorCode?: string;
101
+ /** Free-form detail for logs; never shown as a capability claim. */
102
+ detail?: string;
103
+ }
104
+ /** How one attempt is performed; the caller owns credentials and HTTP. */
105
+ type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>;
106
+ /** The outcome of probing one model. */
107
+ type ProbeOutcome = {
108
+ validation: 'validating';
109
+ efforts: readonly WorkBuddyEffort[];
110
+ requests: number;
111
+ } | {
112
+ validation: 'non-validating';
113
+ efforts: readonly [];
114
+ requests: number;
115
+ } | {
116
+ validation: 'unknown';
117
+ efforts: readonly [];
118
+ requests: number;
119
+ reason: string;
120
+ };
121
+ /**
122
+ * Probe one model.
123
+ *
124
+ * `options.candidates` exists so tests can shorten the sweep; production always
125
+ * uses {@link PROBE_EFFORT_CANDIDATES}.
126
+ */
127
+ declare function probeModel(options: {
128
+ send: ProbeSender;
129
+ sentinel?: SentinelFactory;
130
+ candidates?: readonly WorkBuddyEffort[];
131
+ timeoutMs?: number;
132
+ }): Promise<ProbeOutcome>;
133
+ //#endregion
6
134
  //#region src/upstream.d.ts
7
135
  /** WorkBuddy region selected by the credential's login domain. */
8
136
  type WorkBuddyRegion = 'cn' | 'global';
@@ -13,6 +141,9 @@ interface WorkBuddyUpstreamModel {
13
141
  id: string;
14
142
  name: string;
15
143
  contextWindow: number;
144
+ maxInputTokens?: number;
145
+ supportedContextWindows?: readonly number[];
146
+ promotions?: readonly WorkBuddyPromotion[];
16
147
  maxTokens: number;
17
148
  /**
18
149
  * Upstream-declared image input capability. Missing or false upstream data
@@ -63,6 +194,17 @@ interface WorkBuddyModelBilling {
63
194
  badges?: readonly string[];
64
195
  /** Whether the model is currently free (`x0.00` credits). */
65
196
  free: boolean;
197
+ /**
198
+ * The rate cannot be stated for this model right now.
199
+ *
200
+ * Set when a row that arrived with promotions attached has no promotion in
201
+ * force: the upstream bakes the discounted value into `credits`, so the
202
+ * cached rate describes a discount that has ended. The original price is not
203
+ * recoverable from the row, so the plugin reports "unknown, refresh needed"
204
+ * rather than repeating a figure it can no longer stand behind — in
205
+ * particular it never keeps claiming the model is free.
206
+ */
207
+ rateUnknown?: boolean;
66
208
  }
67
209
  /** One billing package and its remaining credit. */
68
210
  interface WorkBuddyCreditAccount {
@@ -124,20 +266,177 @@ declare function regionOf(domain: string): WorkBuddyRegion;
124
266
  * compatible spelling the upstream accepts.
125
267
  */
126
268
  declare function prepareChatBody(source: string): string;
269
+ /** Provenance of one successful catalog fetch, surfaced by the status card. */
270
+ interface WorkBuddyCatalogFetch {
271
+ fetchedAtMs: number;
272
+ /** Which document answered, e.g. `workbuddy-ai:app`. */
273
+ source: string;
274
+ /** UA version used, when the request needed one. */
275
+ appVersion?: AppVersionInfo;
276
+ }
277
+ /** Constructor dependencies. */
278
+ interface WorkBuddyUpstreamClientOptions {
279
+ /** App-version resolver for international catalog requests; injectable for tests. */
280
+ resolveAppVersion?: () => Promise<AppVersionInfo>;
281
+ }
127
282
  /**
128
283
  * Upstream HTTP client. One instance serves the whole plugin; requests take
129
284
  * the credential explicitly so token refreshes apply on the next call.
285
+ *
286
+ * One instance is *per variant*: the international provider needs its own
287
+ * catalog source, UA version, and probe differences, and keeping them on the
288
+ * instance avoids passing a variant through every call signature.
130
289
  */
131
290
  declare class WorkBuddyUpstreamClient {
291
+ /**
292
+ * Resolves the App-shaped UA version for international catalog requests.
293
+ * Injectable so tests never read the real filesystem.
294
+ */
295
+ private readonly resolveAppVersion;
296
+ /** Provenance of the most recent successful catalog fetch, for the card. */
297
+ lastCatalog: WorkBuddyCatalogFetch | undefined;
298
+ constructor(options?: WorkBuddyUpstreamClientOptions);
132
299
  /** POST the chat endpoint; a successful answer is the raw SSE response. */
133
300
  chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
134
301
  /** POST the token-refresh endpoint; the caller merges the outcome. */
135
302
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
136
- /** GET the personal model catalog and keep the `cli` agent's models only. */
137
- fetchModels(credential: WorkBuddyCredential): Promise<readonly WorkBuddyUpstreamModel[]>;
303
+ /**
304
+ * GET the personal model catalog.
305
+ *
306
+ * Two upstream documents feed this, one per variant:
307
+ *
308
+ * - CN (`workbuddy`): `/console/enterprises/personal/models`, the document
309
+ * the official CLI itself consumes. Unchanged behaviour.
310
+ * - International (`workbuddy-ai`): `/v3/config`, the product document the
311
+ * App's main process fetches. The gateway splits it by User-Agent, so this
312
+ * request carries the App-shaped UA while every other request keeps the
313
+ * CLI UA it has always sent.
314
+ *
315
+ * Both are unwrapped and classified the same way — `readEnvelope` plus
316
+ * `envelopeError` — so an expired session or exhausted credit is reported as
317
+ * such rather than as a generic catalog failure.
318
+ */
319
+ fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
138
320
  /** POST the billing endpoint for the aggregated remaining credit. */
139
321
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
322
+ /**
323
+ * One probe request: a real streaming chat call carrying the effort under
324
+ * test.
325
+ *
326
+ * Shares `chatHeaders` with the normal chat path on purpose — the plan
327
+ * forbids probing through anything but the plugin's own credential handling,
328
+ * so a result describes what a real message would experience.
329
+ *
330
+ * The caller aborts as soon as a parseable event arrives; the body is never
331
+ * assembled into an answer. `reasoning_effort` is omitted entirely (rather
332
+ * than sent empty) when `effort` is undefined, so the baseline case is a
333
+ * genuinely bare request.
334
+ *
335
+ * Two international differences, both measured on 2026-09-11:
336
+ *
337
+ * - The gateway requires a leading `system` message (400/11128 otherwise), so
338
+ * one is prepended for the global region only.
339
+ * - `max_tokens: 1` is below some models' floor (the GPT-5.6 family rejects it
340
+ * with 400/11133 `integer_below_min_value`), so the international probe asks
341
+ * for a slightly larger minimum. This is a floor the plugin must clear, not
342
+ * evidence about any model's effort support: a model still refusing that
343
+ * minimum is reported as an incompatible request, never as "effort
344
+ * unsupported", and the ceiling is never raised further to force an answer.
345
+ */
346
+ probeEffort(credential: WorkBuddyCredential, model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
347
+ }
348
+ /** Parse either response shape after its envelope has been checked. */
349
+ declare function parseModelCatalog(data: Record<string, unknown>, international?: boolean): readonly WorkBuddyUpstreamModel[];
350
+ /**
351
+ * One verified promotion entry.
352
+ *
353
+ * Only the shape actually observed in the international App document is
354
+ * modelled — an enabled, time-boxed, `displayMode: "replace"` discount. An
355
+ * entry that does not match is dropped rather than guessed at: rendering a
356
+ * discount the plugin does not understand could understate what the user pays.
357
+ */
358
+ interface WorkBuddyPromotion {
359
+ /** Window start, epoch ms, parsed from the document's offset timestamp. */
360
+ start: number;
361
+ /** Window end, epoch ms. */
362
+ end: number;
363
+ /** Badge text as the upstream wrote it, e.g. `Free now`. */
364
+ label: string;
365
+ /** Multiplier applied to the model's rate; `0` replaces it outright. */
366
+ factor: number;
367
+ /** Higher wins when several promotions cover one model. */
368
+ priority: number;
369
+ }
370
+ /**
371
+ * Re-evaluate a model's promotion against the current time.
372
+ *
373
+ * Promotions are time-boxed, and the catalog they arrive in is cached for the
374
+ * life of the process. Frozen at parse time, a cached "Free now" would keep
375
+ * claiming a discount after `validUntil` had passed, and would keep showing the
376
+ * pre-discount rate as the discounted one. Re-deriving on every read means the
377
+ * badge disappears on its own and the rate reverts, with no refresh needed.
378
+ *
379
+ * Non-destructive: the model's own `credits` and `badges` are the base, and the
380
+ * promotion is layered onto a copy. A model with no live promotion is returned
381
+ * as-is, so the common case allocates nothing.
382
+ */
383
+ declare function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now?: number): WorkBuddyUpstreamModel;
384
+ /**
385
+ * Apply the international endpoint's extra chat requirement: the first message
386
+ * must be a system prompt.
387
+ *
388
+ * The international gateway rejects a body whose first message is not `system`
389
+ * with HTTP 400 code 11128 ("first message is not system prompt"). Note that
390
+ * the *same* code means something else on the CN endpoint — there it reports a
391
+ * rejected `developer` role — so the two are never branched on by code alone.
392
+ *
393
+ * The added prompt is deliberately empty of user content and prepended, never
394
+ * merged: existing messages keep their order and wording. A body that is not a
395
+ * JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
396
+ * so this is safe to run over an already-prepared-or-not body.
397
+ */
398
+ declare function prepareInternationalChatBody(source: string): string;
399
+ //#endregion
400
+ //#region src/variants.d.ts
401
+ /** One WorkBuddy product variant. */
402
+ interface WorkBuddyVariant {
403
+ /** Provider id registered with DSH, e.g. `workbuddy-ai`. */
404
+ id: string;
405
+ /** Model-group heading and card title stem, e.g. `WorkBuddy AI`. */
406
+ displayName: string;
407
+ /** Desktop app name as users know it, for diagnostics and error copy. */
408
+ appName: string;
409
+ /** Which upstream region this variant's credentials must belong to. */
410
+ region: WorkBuddyRegion;
411
+ /** Env var overriding the desktop auth-file location. */
412
+ env: string;
413
+ /** Basename of the desktop app's own auth file in the shared auth directory. */
414
+ desktopFilename: string;
415
+ /** Basename of the plugin-owned credential copy under `$DSH_HOME`. */
416
+ ownFilename: string;
417
+ /** Basename of the plugin-owned probe-record file under `$DSH_HOME`. */
418
+ probeFilename: string;
419
+ /**
420
+ * Basename of the plugin-owned saved-catalog file under `$DSH_HOME`.
421
+ *
422
+ * One per variant, like the probe records: the two endpoints disagree about
423
+ * rates, windows, and even which models exist for a shared id, so a catalog
424
+ * saved from one must never be served as the other's.
425
+ */
426
+ catalogFilename: string;
427
+ /** Same-origin status route consumed by this variant's card. */
428
+ statusPath: string;
429
+ /** Same-origin probe-control route consumed by this variant's card. */
430
+ probePath: string;
140
431
  }
432
+ /** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
433
+ declare const WORKBUDDY_VARIANTS: readonly WorkBuddyVariant[];
434
+ /** The CN variant; the plugin's long-standing default and compatibility anchor. */
435
+ declare const CN_VARIANT: WorkBuddyVariant;
436
+ /** The international variant. */
437
+ declare const AI_VARIANT: WorkBuddyVariant;
438
+ /** Look up a variant by provider id. */
439
+ declare function variantFor(id: string): WorkBuddyVariant | undefined;
141
440
  //#endregion
142
441
  //#region src/auth.d.ts
143
442
  /** Normalized WorkBuddy credential, timestamps in epoch milliseconds. */
@@ -161,9 +460,16 @@ interface WorkBuddyAuthStatus {
161
460
  nickname?: string;
162
461
  domain?: string;
163
462
  source?: 'desktop' | 'dsh';
463
+ /**
464
+ * Why no credential is usable, when the reason is diagnosable rather than
465
+ * "nobody is signed in" — a region mismatch being the case that matters.
466
+ * Present only on `signed-out`, and never a substitute for fixing the file.
467
+ */
468
+ reason?: string;
164
469
  }
165
470
  /** Constructor options; only {@link refresh} is required. */
166
471
  interface WorkBuddyStoreOptions {
472
+ variant?: WorkBuddyVariant;
167
473
  /** Explicit desktop auth-file path, overriding env and platform defaults. */
168
474
  desktopPath?: string;
169
475
  /** Explicit plugin-owned copy path, defaulting under `$DSH_HOME`. */
@@ -187,8 +493,16 @@ declare function workbuddyOwnAuthPath(): string;
187
493
  * native Linux location.
188
494
  */
189
495
  declare function defaultDesktopAuthCandidates(): string[];
496
+ /**
497
+ * The platform-default candidates for one variant, in probe order.
498
+ *
499
+ * Both apps write into the *same* shared `CodeBuddyExtension` auth directory
500
+ * and differ only in the file's basename, so the per-platform ordering above
501
+ * is reused verbatim and just the filename is swapped.
502
+ */
503
+ declare function desktopAuthCandidatesFor(variant: WorkBuddyVariant): string[];
190
504
  /** First platform-default candidate; see {@link defaultDesktopAuthCandidates}. */
191
- declare function defaultDesktopAuthPath(): string | undefined;
505
+ declare function defaultDesktopAuthPath(variant?: WorkBuddyVariant): string | undefined;
192
506
  /**
193
507
  * Parse a WorkBuddy auth document in either on-disk shape: the plugin OAuth
194
508
  * nested form `{"auth":{...},"account":{...}}` and the flat panel form.
@@ -205,6 +519,7 @@ declare function parseWorkBuddyAuth(text: string): WorkBuddyCredential | undefin
205
519
  * not take down a working session.
206
520
  */
207
521
  declare class WorkBuddyCredentialStore {
522
+ private readonly variant;
208
523
  private readonly refresh;
209
524
  private readonly refreshMarginMs;
210
525
  private readonly ownPath;
@@ -262,19 +577,165 @@ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
262
577
  * registers with a usable catalog even while the first fetch is in flight or
263
578
  * offline.
264
579
  *
265
- * The list tracks the `cli` agent's model roster exactly: the 15 models the
580
+ * The list tracks the `cli` agent's model roster exactly: the 16 models the
266
581
  * desktop CLI offers. Reasoning metadata is taken verbatim from the live
267
582
  * endpoint — each model's supported effort set and whether thinking can be
268
583
  * disabled — and the `free` flag follows the upstream `x0.00` credits marker.
269
584
  */
270
585
  declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
271
- /** Mutable catalog shared by the shim's `/v1/models` and the adapter. */
586
+ /**
587
+ * Static CLI models for the international endpoint, captured 2026-09-11 from
588
+ * the App-form `/v3/config` document (the 20 ids of its `cli` agent, in order).
589
+ *
590
+ * Same purpose and same discipline as {@link FALLBACK_WORKBUDDY_MODELS}: it
591
+ * covers the window before the first successful fetch and an offline start,
592
+ * and it is deliberately *not* a promise about the upstream's current state.
593
+ * Reasoning metadata is verbatim from that snapshot. No promo badge is baked
594
+ * in: promotions are time-boxed (`modelPromotions` carries `validFrom`/
595
+ * `validUntil`), so hard-coding a "Free now" label would keep claiming a
596
+ * discount the upstream may have already ended.
597
+ */
598
+ declare const FALLBACK_WORKBUDDY_AI_MODELS: readonly WorkBuddyModelInfo[];
599
+ /**
600
+ * Mutable catalog shared by the shim's `/v1/models` and the adapter.
601
+ *
602
+ * Visibility is separate from content. A variant whose app has no credentials
603
+ * must expose *no* models rather than a fallback roster: the DSH model picker
604
+ * drops an empty group, so an empty catalog is exactly how a provider hides
605
+ * without touching registration. Serving the fallback to a signed-out user
606
+ * instead offers models that can only fail (`store.resolve()` throws on the
607
+ * first message), which is worse than showing nothing.
608
+ *
609
+ * The flag defaults to visible so a directly-constructed catalog behaves as it
610
+ * always has; the plugin runtime applies the credential gate.
611
+ */
272
612
  declare class WorkBuddyCatalog {
273
613
  private models;
274
- /** Current entries; the fallback list until the upstream answer lands. */
614
+ private visible;
615
+ constructor(initial?: readonly WorkBuddyModelInfo[]);
616
+ /** Current entries; empty while the variant has no usable credential. */
275
617
  current(): readonly WorkBuddyModelInfo[];
276
618
  /** Replace the list; callers invalidate their adapter snapshot after this. */
277
619
  set(models: readonly WorkBuddyModelInfo[]): void;
620
+ /** Whether this variant's models are exposed at all. */
621
+ isVisible(): boolean;
622
+ /**
623
+ * Show or hide the whole catalog. Returns whether the value changed, so the
624
+ * caller can skip an invalidation that would re-render an identical list.
625
+ */
626
+ setVisible(visible: boolean): boolean;
627
+ /** Models to fall back to when the upstream fetch fails; ignores visibility. */
628
+ fallback(): readonly WorkBuddyModelInfo[];
629
+ }
630
+ //#endregion
631
+ //#region src/probe-store.d.ts
632
+ /** Basename of the probe record inside the Harness home. */
633
+ declare const WORKBUDDY_PROBE_FILENAME = ".workbuddy-probe.json";
634
+ /**
635
+ * Whether the model's effort parameter is actually validated.
636
+ *
637
+ * - `validating`: the upstream rejected an unknown sentinel value, so a
638
+ * per-level answer is meaningful.
639
+ * - `non-validating`: the upstream accepted the sentinel, so it ignores or
640
+ * loosely coerces the parameter and no per-level answer can be trusted.
641
+ * - `unknown`: baseline or sentinel failed for an unrelated reason (auth,
642
+ * rate limit, transport, ambiguous error body). Not a negative claim.
643
+ */
644
+ type WorkBuddyProbeValidation = 'validating' | 'non-validating' | 'unknown';
645
+ /** One model's recorded observation. */
646
+ interface WorkBuddyProbeRecord {
647
+ /** Fingerprint of the catalog row this observation was made against. */
648
+ fingerprint: string;
649
+ validation: WorkBuddyProbeValidation;
650
+ /** Efforts verified as accepted; only ever non-empty for `validating`. */
651
+ efforts: readonly WorkBuddyEffort[];
652
+ /** When the probe ran, epoch milliseconds. */
653
+ probedAtMs: number;
654
+ /** Plugin version that produced the record. */
655
+ pluginVersion: string;
656
+ /**
657
+ * The account this observation was made under, as `uid:enterpriseId`.
658
+ *
659
+ * An effort set is a fact about one account's entitlement as much as about
660
+ * the model: the same model id can accept different levels under a different
661
+ * subscription. Without this a record outlived the account that produced it,
662
+ * so signing out and in as someone else inherited the previous account's
663
+ * detected levels. Records written before this field existed carry no
664
+ * identity and are therefore never reused.
665
+ */
666
+ account?: string;
667
+ }
668
+ /**
669
+ * Plugin-owned probe record path inside the Harness home.
670
+ *
671
+ * One file per variant. Same-named models exist on both endpoints (the
672
+ * international catalog repeats `glm-5.3`, `glm-5.2`, `hy3`, `kimi-k2.6`), and
673
+ * {@link fingerprintModel} covers only `id`/`reasoning`/`supportsImages` —
674
+ * never the provider — so a single shared file would let one variant's
675
+ * observation answer for the other. The paths differ; the format does not.
676
+ */
677
+ declare function workbuddyProbePath(filename?: string): string;
678
+ /**
679
+ * Fingerprint the catalog fields a probe depends on.
680
+ *
681
+ * Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
682
+ * so a rename or a promo badge does not throw away a valid observation, and
683
+ * deliberately includes the whole reasoning object so any change to the
684
+ * declared shape re-probes.
685
+ */
686
+ declare function fingerprintModel(info: WorkBuddyModelInfo): string;
687
+ /** Options for {@link WorkBuddyProbeStore}. */
688
+ interface WorkBuddyProbeStoreOptions {
689
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
690
+ path?: string;
691
+ /** Observation lifetime; defaults to 14 days. */
692
+ ttlMs?: number;
693
+ /** Plugin version stamped into new records. */
694
+ pluginVersion: string;
695
+ /** Clock injection for tests. */
696
+ now?: () => number;
697
+ }
698
+ /**
699
+ * The plugin's probe records: read once, written atomically, never trusted
700
+ * across a fingerprint change or past the TTL.
701
+ */
702
+ declare class WorkBuddyProbeStore {
703
+ private readonly path;
704
+ private readonly ttlMs;
705
+ private readonly pluginVersion;
706
+ private readonly now;
707
+ private records;
708
+ constructor(options: WorkBuddyProbeStoreOptions | string);
709
+ /** Resolved state-file path, for the CLI and tests. */
710
+ filePath(): string;
711
+ private load;
712
+ /**
713
+ * The usable record for a model, or `undefined` when there is none, it is
714
+ * expired, it was taken against a different catalog row, or it belongs to a
715
+ * different account.
716
+ *
717
+ * @param account - the account in effect, as `uid:enterpriseId`. Records are
718
+ * only returned for the account that produced them.
719
+ */
720
+ get(modelId: string, fingerprint: string, account: string): WorkBuddyProbeRecord | undefined;
721
+ /**
722
+ * Store one observation. Only a decisive answer (`validating` /
723
+ * `non-validating`) replaces an existing decisive record: a transient
724
+ * `unknown` must not erase knowledge the user already paid for.
725
+ */
726
+ set(modelId: string, record: WorkBuddyProbeRecord): void;
727
+ /** Drop every record; used by the card's explicit "clear" action. */
728
+ clear(): void;
729
+ /** Every record currently held, for status display. */
730
+ all(): Readonly<Record<string, WorkBuddyProbeRecord>>;
731
+ /** Build a record stamped with this store's clock, version, and account. */
732
+ record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[], account: string): WorkBuddyProbeRecord;
733
+ /**
734
+ * Write through a temporary file and rename, so a crash mid-write cannot
735
+ * leave a half-parsed document that reads as "no records" and silently drops
736
+ * every observation.
737
+ */
738
+ private persist;
278
739
  }
279
740
  //#endregion
280
741
  //#region src/shim.d.ts
@@ -319,11 +780,18 @@ declare const WORKBUDDY_PROVIDER = "workbuddy";
319
780
  declare const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 300000;
320
781
  /** Constructor dependencies. */
321
782
  interface WorkBuddyAdapterOptions {
783
+ providerId?: string;
784
+ displayName?: string;
322
785
  shim: WorkBuddyShim;
323
786
  store: WorkBuddyCredentialStore;
324
787
  catalog: WorkBuddyCatalog;
325
788
  /** Resolve the durable attachment service at request time, when present. */
326
789
  resolveAttachments?: () => AttachmentStore | undefined;
790
+ /**
791
+ * Look up a local probe observation for a model. Consulted only for rows the
792
+ * upstream left undeclared; absent means declared-set-only behavior.
793
+ */
794
+ observe?: (modelId: string) => WorkBuddyProbeRecord | undefined;
327
795
  }
328
796
  /** What {@link createWorkBuddyAdapter} hands back. */
329
797
  interface WorkBuddyAdapter {
@@ -345,6 +813,120 @@ interface WorkBuddyAdapter {
345
813
  */
346
814
  declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
347
815
  //#endregion
816
+ //#region src/catalog-store.d.ts
817
+ /** Basename of the CN variant's saved catalog inside the Harness home. */
818
+ declare const WORKBUDDY_CATALOG_FILENAME = ".workbuddy-catalog.json";
819
+ /** One saved catalog: the account it belonged to, and the models it listed. */
820
+ interface SavedCatalog {
821
+ /** `uid:enterpriseId` the catalog was fetched for. */
822
+ account: string;
823
+ /** Which document answered, so a CN roster is never served as an AI one. */
824
+ source: string;
825
+ /** When the fetch succeeded, epoch milliseconds. */
826
+ fetchedAtMs: number;
827
+ models: readonly WorkBuddyUpstreamModel[];
828
+ /** App version used as the UA, when the variant needed one. */
829
+ appVersion?: string;
830
+ }
831
+ /** Plugin-owned saved-catalog path inside the Harness home. */
832
+ declare function workbuddyCatalogPath(filename?: string): string;
833
+ /** Options for {@link WorkBuddyCatalogStore}. */
834
+ interface WorkBuddyCatalogStoreOptions {
835
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
836
+ path?: string;
837
+ }
838
+ /**
839
+ * The last successful catalog per account, read once and written atomically.
840
+ *
841
+ * Malformed content reads as "nothing saved" rather than throwing: this file
842
+ * is an optimization for the offline and first-seconds cases, and a corrupt one
843
+ * must never be able to stop the plugin from serving models.
844
+ */
845
+ declare class WorkBuddyCatalogStore {
846
+ private readonly path;
847
+ private entries;
848
+ constructor(options?: WorkBuddyCatalogStoreOptions | string);
849
+ /** Resolved state-file path, for the CLI and tests. */
850
+ filePath(): string;
851
+ private load;
852
+ /** The saved catalog for one account, or `undefined` when there is none. */
853
+ get(account: string): SavedCatalog | undefined;
854
+ /**
855
+ * Remember a catalog for an account, replacing whatever was saved before.
856
+ *
857
+ * A failed write is swallowed: the plugin has already served these models,
858
+ * and losing the *memory* of them is not worth surfacing.
859
+ */
860
+ set(account: string, catalog: Omit<SavedCatalog, 'account'>): void;
861
+ /** Forget one account's catalog — used when that account signs out. */
862
+ delete(account: string): void;
863
+ private persist;
864
+ }
865
+ //#endregion
866
+ //#region src/probe-service.d.ts
867
+ /** What the caller learns about a completed probe. */
868
+ type WorkBuddyProbeStatus = {
869
+ state: 'ok';
870
+ validation: WorkBuddyProbeRecord['validation'];
871
+ efforts: readonly string[];
872
+ requests: number;
873
+ } | {
874
+ state: 'unavailable';
875
+ reason: string;
876
+ };
877
+ /** Options for {@link WorkBuddyProbeService}. */
878
+ interface WorkBuddyProbeServiceOptions {
879
+ store: WorkBuddyProbeStore;
880
+ catalog: WorkBuddyCatalog;
881
+ credentials: WorkBuddyCredentialStore;
882
+ client: WorkBuddyUpstreamClient;
883
+ /** Whether probing is permitted at all; consulted before every sweep. */
884
+ consent: () => boolean;
885
+ /**
886
+ * The account currently in effect, as `uid:enterpriseId`, or `undefined`
887
+ * while signed out.
888
+ *
889
+ * Records are read and written against this identity, and it is re-checked
890
+ * after the sweep finishes: an observation produced under account A must not
891
+ * be stored once account B is in effect, however long the probe took. The
892
+ * caller's `clear()` on an account switch is not sufficient on its own,
893
+ * because an in-flight probe completes *after* that clear.
894
+ */
895
+ account: () => string | undefined;
896
+ sentinel?: SentinelFactory;
897
+ /** Injectable for tests; defaults to the live upstream sender. */
898
+ send?: (modelId: string) => ProbeSender;
899
+ }
900
+ /**
901
+ * Serial probe runner. One instance is shared by the manual API and any
902
+ * future automatic trigger, so the two can never overlap.
903
+ */
904
+ declare class WorkBuddyProbeService {
905
+ private readonly options;
906
+ private queue;
907
+ private readonly pending;
908
+ private running;
909
+ constructor(options: WorkBuddyProbeServiceOptions);
910
+ /** Whether a sweep is in flight right now. */
911
+ isRunning(): boolean;
912
+ /**
913
+ * The record the adapter may use for this model, or `undefined`.
914
+ *
915
+ * Applies the plan's precedence (§5): a declared set always wins, so a model
916
+ * that declares `supportedEfforts` is never answered from an observation.
917
+ */
918
+ recordFor(modelId: string): WorkBuddyProbeRecord | undefined;
919
+ /**
920
+ * Probe one model, serially.
921
+ *
922
+ * The authenticated manual route supplies one-request consent after UI
923
+ * confirmation. Other callers must pass the configured consent gate.
924
+ * Manual consent never changes the automatic-probing configuration.
925
+ * Explicit requests bypass historical results, but share an ongoing run.
926
+ */
927
+ probe(modelId: string, manualConsent?: boolean): Promise<WorkBuddyProbeStatus>;
928
+ }
929
+ //#endregion
348
930
  //#region src/host-heartbeat.d.ts
349
931
  /**
350
932
  * Host-side heartbeat: a small JSON file written under `$DSH_HOME` once the
@@ -416,7 +998,7 @@ declare const name = "llm-workbuddy";
416
998
  /** The model registry required before the provider can register. */
417
999
  declare const inject: string[];
418
1000
  /**
419
- * Settings namespace owning the configuration card.
1001
+ * Settings namespace owning the CN card's section.
420
1002
  *
421
1003
  * DSH 0.1.2 dropped the `settingsNamespace()` branding function: a namespace is
422
1004
  * now a nominal string, validated by the type system where it is used rather
@@ -428,18 +1010,42 @@ declare const inject: string[];
428
1010
  * their namespaces as plain string literals).
429
1011
  */
430
1012
  declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
1013
+ /**
1014
+ * Settings namespace owning the international card's section.
1015
+ *
1016
+ * One namespace per card, not one shared: the settings Plugins tab dispatches a
1017
+ * card by rendering `settings.plugin.item` with `entryKey = ns` for each
1018
+ * namespace the Host serves, and skips an entry whose key names no served
1019
+ * namespace. With a single installed section, the international card registers
1020
+ * into the slot but is never rendered — the card list is built from the Host's
1021
+ * sections, not from the slot's entries. Each card therefore needs its own
1022
+ * installed section whose namespace equals the card's slot key.
1023
+ */
1024
+ declare const WORKBUDDY_AI_SETTINGS_NS: SettingsNamespace;
431
1025
  /** Plugin configuration. */
432
1026
  interface Config {
433
- /** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
1027
+ /** Explicit WorkBuddy (CN) desktop auth-file path, overriding env and platform defaults. */
434
1028
  authFile?: string;
1029
+ /** Explicit WorkBuddy AI (international) desktop auth-file path, overriding env and platform defaults. */
1030
+ authFileAI?: string;
1031
+ /**
1032
+ * Whether the user has authorized sending probe requests about reasoning
1033
+ * efforts. Off by default: a probe spends real credit, so nothing is sent
1034
+ * until the user explicitly agrees.
1035
+ */
1036
+ probeConsent?: boolean;
435
1037
  }
436
1038
  declare const Config: z<Config>;
437
1039
  /**
438
- * Start the loopback endpoint, register the `workbuddy` provider, and
439
- * refresh the model catalog from the upstream once credentials allow it.
440
- * The static fallback catalog serves from the first moment, so an offline
441
- * upstream never leaves the provider empty.
1040
+ * Start both variants: their loopback endpoints, the `workbuddy` and
1041
+ * `workbuddy-ai` providers, their configuration cards, and their
1042
+ * credential-driven catalog lifecycles.
1043
+ *
1044
+ * Each variant registers unconditionally; what varies is whether its catalog is
1045
+ * *visible*. An empty catalog is how DSH hides a model group (the host filters
1046
+ * out groups with no models), which keeps a sign-in that happens after startup
1047
+ * working without re-registering the provider.
442
1048
  */
443
1049
  declare function apply(ctx: Context, config: Config): void;
444
1050
  //#endregion
445
- export { Config, FALLBACK_WORKBUDDY_MODELS, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath };
1051
+ export { AI_VARIANT, type AppVersionInfo, CN_VARIANT, Config, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type UpstreamErrorKind, WORKBUDDY_AI_SETTINGS_NS, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, 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, appUserAgent, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readHostHeartbeat, regionOf, resolveAppVersion, validAppVersion, variantFor, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath };