dsh-workbuddy-connect 0.4.0 → 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
@@ -4,6 +4,76 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
6
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
7
77
  //#region src/probe.d.ts
8
78
  /**
9
79
  * The canonical values a probe tests, in a fixed order.
@@ -71,6 +141,9 @@ interface WorkBuddyUpstreamModel {
71
141
  id: string;
72
142
  name: string;
73
143
  contextWindow: number;
144
+ maxInputTokens?: number;
145
+ supportedContextWindows?: readonly number[];
146
+ promotions?: readonly WorkBuddyPromotion[];
74
147
  maxTokens: number;
75
148
  /**
76
149
  * Upstream-declared image input capability. Missing or false upstream data
@@ -121,6 +194,17 @@ interface WorkBuddyModelBilling {
121
194
  badges?: readonly string[];
122
195
  /** Whether the model is currently free (`x0.00` credits). */
123
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;
124
208
  }
125
209
  /** One billing package and its remaining credit. */
126
210
  interface WorkBuddyCreditAccount {
@@ -182,17 +266,57 @@ declare function regionOf(domain: string): WorkBuddyRegion;
182
266
  * compatible spelling the upstream accepts.
183
267
  */
184
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
+ }
185
282
  /**
186
283
  * Upstream HTTP client. One instance serves the whole plugin; requests take
187
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.
188
289
  */
189
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);
190
299
  /** POST the chat endpoint; a successful answer is the raw SSE response. */
191
300
  chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
192
301
  /** POST the token-refresh endpoint; the caller merges the outcome. */
193
302
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
194
- /** GET the personal model catalog and keep the `cli` agent's models only. */
195
- 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[]>;
196
320
  /** POST the billing endpoint for the aggregated remaining credit. */
197
321
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
198
322
  /**
@@ -207,9 +331,112 @@ declare class WorkBuddyUpstreamClient {
207
331
  * assembled into an answer. `reasoning_effort` is omitted entirely (rather
208
332
  * than sent empty) when `effort` is undefined, so the baseline case is a
209
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.
210
345
  */
211
346
  probeEffort(credential: WorkBuddyCredential, model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
212
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;
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;
213
440
  //#endregion
214
441
  //#region src/auth.d.ts
215
442
  /** Normalized WorkBuddy credential, timestamps in epoch milliseconds. */
@@ -233,9 +460,16 @@ interface WorkBuddyAuthStatus {
233
460
  nickname?: string;
234
461
  domain?: string;
235
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;
236
469
  }
237
470
  /** Constructor options; only {@link refresh} is required. */
238
471
  interface WorkBuddyStoreOptions {
472
+ variant?: WorkBuddyVariant;
239
473
  /** Explicit desktop auth-file path, overriding env and platform defaults. */
240
474
  desktopPath?: string;
241
475
  /** Explicit plugin-owned copy path, defaulting under `$DSH_HOME`. */
@@ -259,8 +493,16 @@ declare function workbuddyOwnAuthPath(): string;
259
493
  * native Linux location.
260
494
  */
261
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[];
262
504
  /** First platform-default candidate; see {@link defaultDesktopAuthCandidates}. */
263
- declare function defaultDesktopAuthPath(): string | undefined;
505
+ declare function defaultDesktopAuthPath(variant?: WorkBuddyVariant): string | undefined;
264
506
  /**
265
507
  * Parse a WorkBuddy auth document in either on-disk shape: the plugin OAuth
266
508
  * nested form `{"auth":{...},"account":{...}}` and the flat panel form.
@@ -277,6 +519,7 @@ declare function parseWorkBuddyAuth(text: string): WorkBuddyCredential | undefin
277
519
  * not take down a working session.
278
520
  */
279
521
  declare class WorkBuddyCredentialStore {
522
+ private readonly variant;
280
523
  private readonly refresh;
281
524
  private readonly refreshMarginMs;
282
525
  private readonly ownPath;
@@ -334,19 +577,55 @@ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
334
577
  * registers with a usable catalog even while the first fetch is in flight or
335
578
  * offline.
336
579
  *
337
- * 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
338
581
  * desktop CLI offers. Reasoning metadata is taken verbatim from the live
339
582
  * endpoint — each model's supported effort set and whether thinking can be
340
583
  * disabled — and the `free` flag follows the upstream `x0.00` credits marker.
341
584
  */
342
585
  declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
343
- /** 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
+ */
344
612
  declare class WorkBuddyCatalog {
345
613
  private models;
346
- /** 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. */
347
617
  current(): readonly WorkBuddyModelInfo[];
348
618
  /** Replace the list; callers invalidate their adapter snapshot after this. */
349
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[];
350
629
  }
351
630
  //#endregion
352
631
  //#region src/probe-store.d.ts
@@ -374,9 +653,28 @@ interface WorkBuddyProbeRecord {
374
653
  probedAtMs: number;
375
654
  /** Plugin version that produced the record. */
376
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;
377
667
  }
378
- /** Plugin-owned probe record path inside the Harness home. */
379
- declare function workbuddyProbePath(): string;
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;
380
678
  /**
381
679
  * Fingerprint the catalog fields a probe depends on.
382
680
  *
@@ -413,9 +711,13 @@ declare class WorkBuddyProbeStore {
413
711
  private load;
414
712
  /**
415
713
  * The usable record for a model, or `undefined` when there is none, it is
416
- * expired, or it was taken against a different catalog row.
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.
417
719
  */
418
- get(modelId: string, fingerprint: string): WorkBuddyProbeRecord | undefined;
720
+ get(modelId: string, fingerprint: string, account: string): WorkBuddyProbeRecord | undefined;
419
721
  /**
420
722
  * Store one observation. Only a decisive answer (`validating` /
421
723
  * `non-validating`) replaces an existing decisive record: a transient
@@ -426,8 +728,8 @@ declare class WorkBuddyProbeStore {
426
728
  clear(): void;
427
729
  /** Every record currently held, for status display. */
428
730
  all(): Readonly<Record<string, WorkBuddyProbeRecord>>;
429
- /** Build a record stamped with this store's clock and version. */
430
- record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[]): 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;
431
733
  /**
432
734
  * Write through a temporary file and rename, so a crash mid-write cannot
433
735
  * leave a half-parsed document that reads as "no records" and silently drops
@@ -478,6 +780,8 @@ declare const WORKBUDDY_PROVIDER = "workbuddy";
478
780
  declare const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 300000;
479
781
  /** Constructor dependencies. */
480
782
  interface WorkBuddyAdapterOptions {
783
+ providerId?: string;
784
+ displayName?: string;
481
785
  shim: WorkBuddyShim;
482
786
  store: WorkBuddyCredentialStore;
483
787
  catalog: WorkBuddyCatalog;
@@ -509,6 +813,56 @@ interface WorkBuddyAdapter {
509
813
  */
510
814
  declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
511
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
512
866
  //#region src/probe-service.d.ts
513
867
  /** What the caller learns about a completed probe. */
514
868
  type WorkBuddyProbeStatus = {
@@ -528,6 +882,17 @@ interface WorkBuddyProbeServiceOptions {
528
882
  client: WorkBuddyUpstreamClient;
529
883
  /** Whether probing is permitted at all; consulted before every sweep. */
530
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;
531
896
  sentinel?: SentinelFactory;
532
897
  /** Injectable for tests; defaults to the live upstream sender. */
533
898
  send?: (modelId: string) => ProbeSender;
@@ -633,7 +998,7 @@ declare const name = "llm-workbuddy";
633
998
  /** The model registry required before the provider can register. */
634
999
  declare const inject: string[];
635
1000
  /**
636
- * Settings namespace owning the configuration card.
1001
+ * Settings namespace owning the CN card's section.
637
1002
  *
638
1003
  * DSH 0.1.2 dropped the `settingsNamespace()` branding function: a namespace is
639
1004
  * now a nominal string, validated by the type system where it is used rather
@@ -645,10 +1010,24 @@ declare const inject: string[];
645
1010
  * their namespaces as plain string literals).
646
1011
  */
647
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;
648
1025
  /** Plugin configuration. */
649
1026
  interface Config {
650
- /** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
1027
+ /** Explicit WorkBuddy (CN) desktop auth-file path, overriding env and platform defaults. */
651
1028
  authFile?: string;
1029
+ /** Explicit WorkBuddy AI (international) desktop auth-file path, overriding env and platform defaults. */
1030
+ authFileAI?: string;
652
1031
  /**
653
1032
  * Whether the user has authorized sending probe requests about reasoning
654
1033
  * efforts. Off by default: a probe spends real credit, so nothing is sent
@@ -658,11 +1037,15 @@ interface Config {
658
1037
  }
659
1038
  declare const Config: z<Config>;
660
1039
  /**
661
- * Start the loopback endpoint, register the `workbuddy` provider, and
662
- * refresh the model catalog from the upstream once credentials allow it.
663
- * The static fallback catalog serves from the first moment, so an offline
664
- * 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.
665
1048
  */
666
1049
  declare function apply(ctx: Context, config: Config): void;
667
1050
  //#endregion
668
- export { Config, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_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 WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, fingerprintModel, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, probeModel, processStartTimeMs, randomSentinel, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath };
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 };