dsh-connect-workbuddy 5.2.3 → 5.3.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/client.js CHANGED
@@ -530,7 +530,7 @@ window.__ModuleLoader__.load({
530
530
  headers: { "Content-Type": "application/json" },
531
531
  body: JSON.stringify({
532
532
  field,
533
- value: { [region]: value }
533
+ value: region === void 0 ? value : { [region]: value }
534
534
  })
535
535
  });
536
536
  } catch (error) {
@@ -544,6 +544,56 @@ window.__ModuleLoader__.load({
544
544
  return (await response.json().catch(() => void 0))?.value;
545
545
  }
546
546
  /**
547
+ * Read the committed CodeBuddy-CLI switch out of a settings snapshot.
548
+ *
549
+ * Only an explicit `true` is ON, matching the schema's `default(false)`: this
550
+ * switch authorizes a child process that edits files and spends CodeBuddy
551
+ * quota, so an absent value must never read as consent.
552
+ */
553
+ function cliBridgeEnabledOf(configured) {
554
+ const bridge = configured?.cliBridge;
555
+ return typeof bridge === "object" && bridge !== null && bridge.enabled === true;
556
+ }
557
+ /**
558
+ * Write the GLOBAL CodeBuddy-CLI switch, verifying it landed.
559
+ *
560
+ * Deliberately NOT a region field: one route, one budget, one switch. The two
561
+ * writers are the same as everywhere else — the plugin's Host endpoint first
562
+ * (authoritative merge, so the tuned caps beside `enabled` survive), the
563
+ * settings scope second — because this namespace's browser mirror is documented
564
+ * as stale on the affected 0.1.7 deployments, and a switch that merely *looks*
565
+ * flipped is exactly the "关闭不了" class of report.
566
+ *
567
+ * @param scope - the bound settings scope for this plugin's namespace.
568
+ * @param enabled - the desired state.
569
+ * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
570
+ */
571
+ async function writeCliBridgeEnabled(scope, enabled) {
572
+ let hostError;
573
+ try {
574
+ const authoritative = await saveViaHostEndpoint("cliBridge", void 0, { enabled });
575
+ if (authoritative !== void 0) try {
576
+ await scope.set("cliBridge", authoritative);
577
+ } catch {}
578
+ return;
579
+ } catch (error) {
580
+ hostError = error;
581
+ }
582
+ const stored = unwrapVolatileDeep(scope.getSnapshot().value?.cliBridge);
583
+ const next = {
584
+ ...typeof stored === "object" && stored !== null ? stored : {},
585
+ enabled
586
+ };
587
+ let delivered = false;
588
+ try {
589
+ delivered = await scope.set("cliBridge", next) !== false;
590
+ } catch {
591
+ delivered = false;
592
+ }
593
+ if (delivered && cliBridgeEnabledOf(scope.getSnapshot().value)) return;
594
+ throw hostError instanceof Error ? hostError : new WorkBuddySettingsWriteError("cliBridge", "neither the Host save endpoint nor the settings scope persisted the value");
595
+ }
596
+ /**
547
597
  * Write one volatile field, then confirm the value actually landed.
548
598
  *
549
599
  * ORDER MATTERS, and getting it wrong DELETES DATA. The two writers do not have
@@ -1014,6 +1064,29 @@ window.__ModuleLoader__.load({
1014
1064
  */
1015
1065
  const DAYS_AT_MS = 2 * DAY_MS$1;
1016
1066
  /**
1067
+ * The recovery form of the same duration: "约 2 小时后恢复".
1068
+ *
1069
+ * The bare duration answers "how long?", but beside a red dot on an account row
1070
+ * it left the most important half unsaid: what happens when it elapses. "约 2
1071
+ * 小时后" reads equally well as "the limit lifts" and as "it expires into
1072
+ * something else", and on a row whose whole job is to say whether the account
1073
+ * can serve, that ambiguity is the question the reader came with.
1074
+ *
1075
+ * Same numbers, same rounding, same empty-string contract as
1076
+ * {@link remainingText} — this only appends the verb. Kept as a separate
1077
+ * function rather than a parameter so the two callers that must stay verbless
1078
+ * (the card's "之后可再用" sentence, which already contains its own verb) cannot
1079
+ * accidentally grow a second one.
1080
+ */
1081
+ function recoversInText(t, untilMs, nowMs) {
1082
+ const left = untilMs - nowMs;
1083
+ if (left <= 0) return "";
1084
+ if (left < MINUTE_MS$1) return t("row.recoversImminent");
1085
+ if (left < HOUR_MS$1) return t("row.recoversMinutes", { count: Math.round(left / MINUTE_MS$1) });
1086
+ if (left < DAYS_AT_MS) return t("row.recoversHours", { count: Math.round(left / HOUR_MS$1) });
1087
+ return t("row.recoversDays", { count: Math.round(left / DAY_MS$1) });
1088
+ }
1089
+ /**
1017
1090
  * A short "约 N 分钟后 / 约 N 小时后 / 约 N 天后", or `''` once the moment passed.
1018
1091
  *
1019
1092
  * The empty string is the contract callers depend on: an elapsed cooldown means
@@ -1039,6 +1112,29 @@ window.__ModuleLoader__.load({
1039
1112
  function outcomeOk(outcome) {
1040
1113
  return outcome === "ok";
1041
1114
  }
1115
+ /**
1116
+ * The MARK form of one probe outcome: the verdict alone, for a slot that cannot
1117
+ * hold an explanation (issue #108).
1118
+ *
1119
+ * `outcomeText` is the right function wherever the reader has room — the pool
1120
+ * card's probe column, the test log — and there the extra sentence earns its
1121
+ * width. The composer panel is a different slot: one line, beside an account
1122
+ * name, in a column about 180 px wide. `row.probeChannelRejected` is 88
1123
+ * characters in Chinese and 179 in English, so it could never fit; with
1124
+ * `nowrap` + `overflow:hidden` on the row it was cut mid-sentence, which the
1125
+ * reporter read as broken rendering.
1126
+ *
1127
+ * So this function returns the SHORT label where one is declared and falls back
1128
+ * to {@link outcomeText} otherwise. The full sentence is never lost: the row's
1129
+ * `title` carries `availabilityLine`, which keeps every word and the absolute
1130
+ * instant, and the pool card still prints it in full.
1131
+ */
1132
+ function outcomeMarkText(t, outcome) {
1133
+ switch (outcome) {
1134
+ case "channel-rejected": return t("row.probeChannelRejectedShort");
1135
+ default: return outcomeText(t, outcome);
1136
+ }
1137
+ }
1042
1138
  /** A short, localizable label for one probe outcome. */
1043
1139
  function outcomeText(t, outcome) {
1044
1140
  switch (outcome) {
@@ -1123,11 +1219,11 @@ window.__ModuleLoader__.load({
1123
1219
  const probe = account.probe;
1124
1220
  if (probe !== void 0) {
1125
1221
  if (probe.retryAtMs !== void 0) {
1126
- const left = remainingText(t, probe.retryAtMs, nowMs);
1127
- return left === "" ? outcomeText(t, probe.outcome) : `${outcomeText(t, probe.outcome)} · ${left}`;
1222
+ const left = recoversInText(t, probe.retryAtMs, nowMs);
1223
+ return left === "" ? outcomeMarkText(t, probe.outcome) : `${outcomeMarkText(t, probe.outcome)} · ${left}`;
1128
1224
  }
1129
- if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}`;
1130
- return outcomeText(t, probe.outcome);
1225
+ if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${outcomeMarkText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}`;
1226
+ return outcomeMarkText(t, probe.outcome);
1131
1227
  }
1132
1228
  if (account.excludedBy !== void 0) return exclusionLabel(t, account.excludedBy);
1133
1229
  }
@@ -4154,7 +4250,7 @@ window.__ModuleLoader__.load({
4154
4250
  CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
4155
4251
  loudest object in a section made of plain 13px text. */
4156
4252
  .dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
4157
- .dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check{inline-size:15px;block-size:15px;margin:3px 0 0}
4253
+ .dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check{inline-size:15px;block-size:15px;margin:3px 0 0}
4158
4254
  .dsm-workbuddy-pref input:disabled{cursor:default}
4159
4255
  .dsm-workbuddy-pref:has(input:disabled){opacity:.6;cursor:default}
4160
4256
  /* The NAME and what it does are TWO lines, not one wrapped blob: first what the
@@ -4858,9 +4954,20 @@ window.__ModuleLoader__.load({
4858
4954
  .dsm-workbuddy-composer-panel-mark{
4859
4955
  display:flex;flex-wrap:wrap;align-items:baseline;
4860
4956
  font-size:11px;line-height:15px;
4957
+ /* WRAP, do not clip (issue #108). The switch above sets white-space:nowrap
4958
+ and overflow:hidden for the NAME, which must stay on one line and ellipsise.
4959
+ Inherited here, those two rules cut the reason mark mid-sentence with no
4960
+ ellipsis at all — the reporter saw "…不是这个账号—" and read it as broken
4961
+ rendering, because a hard cut carries no signal that more exists. The mark
4962
+ is the one part of this row whose length is NOT under the layout's control
4963
+ (it is a translated sentence, and the two locales differ by 2x), so it
4964
+ wraps instead: an extra line is a legible outcome, a severed stroke is not.
4965
+ overflow-wrap:anywhere because Chinese has no spaces to break at. */
4966
+ white-space:normal;
4967
+ overflow-wrap:anywhere;
4861
4968
  }
4862
4969
  .dsm-workbuddy-composer-panel-mark-current{color:var(--dsw-alias-state-success-primary,#22a06b)}
4863
- .dsm-workbuddy-composer-panel-mark-reason{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
4970
+ .dsm-workbuddy-composer-panel-mark-reason{color:var(--dsw-alias-state-warn-primary,#f59e0b);min-width:0}
4864
4971
  .dsm-workbuddy-composer-panel-mark-current+.dsm-workbuddy-composer-panel-mark-reason::before{
4865
4972
  content:" · ";color:var(--dsw-alias-label-dimmed,rgba(127,127,127,.6));
4866
4973
  }
@@ -5369,6 +5476,14 @@ window.__ModuleLoader__.load({
5369
5476
  */
5370
5477
  const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
5371
5478
  /**
5479
+ * The OPTIONAL CodeBuddy CLI route switch (global: one switch, one budget).
5480
+ *
5481
+ * `undefined` until the first Host answer arrives, so the box renders the
5482
+ * committed value instead of guessing — the same rule as `creditsShownOn`.
5483
+ */
5484
+ const [cliBridgeOn, setCliBridgeOn] = (0, react.useState)(void 0);
5485
+ const [cliBridgeBusy, setCliBridgeBusy] = (0, react.useState)(false);
5486
+ /**
5372
5487
  * Probe results, keyed by region then model id.
5373
5488
  *
5374
5489
  * Kept per region for the same reason drafts are: the two tabs are separate
@@ -5796,6 +5911,39 @@ window.__ModuleLoader__.load({
5796
5911
  if (mounted.current) setTogglingCredits(void 0);
5797
5912
  }
5798
5913
  };
5914
+ /**
5915
+ * Whether the CLI route is mounted.
5916
+ *
5917
+ * Host answer first, settings mirror second, for the same measured reason as
5918
+ * `imageToolOn`: this namespace's browser mirror goes stale on the affected
5919
+ * deployments, and here a stale `true` would claim a spending capability is on.
5920
+ */
5921
+ const cliBridgeSwitchedOn = () => {
5922
+ if (cliBridgeOn !== void 0) return cliBridgeOn;
5923
+ const fromHost = statusByRegion[activeRegion]?.cliBridgeEnabled;
5924
+ if (typeof fromHost === "boolean") return fromHost;
5925
+ return cliBridgeEnabledOf(settingsScope?.getSnapshot().value);
5926
+ };
5927
+ /**
5928
+ * Flip the global CLI-route switch.
5929
+ *
5930
+ * The Host rebuilds only this route on the change (`cliBridge` is volatile),
5931
+ * so no provider stack is disposed and no in-flight request is interrupted.
5932
+ * Re-reads the usage afterwards so the box shows the COMMITTED value.
5933
+ */
5934
+ const toggleCliBridge = async (enabled) => {
5935
+ if (settingsScope === void 0) return;
5936
+ setCliBridgeBusy(true);
5937
+ try {
5938
+ await writeCliBridgeEnabled(settingsScope, enabled);
5939
+ if (mounted.current) setCliBridgeOn(enabled);
5940
+ await refreshUsage(activeRegion);
5941
+ } catch (error) {
5942
+ if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
5943
+ } finally {
5944
+ if (mounted.current) setCliBridgeBusy(false);
5945
+ }
5946
+ };
5799
5947
  const toggleImageTool = async (item, enabled) => {
5800
5948
  if (settingsScope === void 0) return;
5801
5949
  setTogglingCredits(item);
@@ -6314,9 +6462,41 @@ window.__ModuleLoader__.load({
6314
6462
  })]
6315
6463
  })]
6316
6464
  }) : null;
6465
+ /**
6466
+ * The CLI-route switch.
6467
+ *
6468
+ * GLOBAL, so it is not gated on the active tab's provider being on — the route
6469
+ * pulls its credential from whichever region still has a signed-in account.
6470
+ * Its own class (not `.dsm-workbuddy-pref-check`) for the same reason the image
6471
+ * box has one: the credit-switch suite's "exactly one switch" selector must
6472
+ * keep resolving to the credit box alone.
6473
+ */
6474
+ const cliBridgePreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6475
+ className: "dsm-workbuddy-pref",
6476
+ title: t("row.cliBridgeHint"),
6477
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6478
+ type: "checkbox",
6479
+ className: "dsm-workbuddy-cli-bridge-check",
6480
+ checked: cliBridgeSwitchedOn(),
6481
+ disabled: !canWrite || saving || poolBusy || cliBridgeBusy,
6482
+ onChange: (event) => {
6483
+ toggleCliBridge(event.target.checked);
6484
+ }
6485
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6486
+ className: "dsm-workbuddy-pref-copy",
6487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6488
+ className: "dsm-workbuddy-pref-label",
6489
+ children: t("row.cliBridgeLabel")
6490
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6491
+ className: "dsm-workbuddy-pref-hint",
6492
+ children: t("row.cliBridgeHint")
6493
+ })]
6494
+ })]
6495
+ });
6317
6496
  const creditPreferences = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6318
6497
  className: "dsm-workbuddy-prefs",
6319
6498
  children: [
6499
+ cliBridgePreference,
6320
6500
  imageToolPreference,
6321
6501
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6322
6502
  className: "dsm-workbuddy-pref",
@@ -8098,6 +8278,7 @@ window.__ModuleLoader__.load({
8098
8278
  "row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
8099
8279
  "row.probePolicyRejected": "✗ WorkBuddy's server refused this request under its content policy — re-signing in won't help; try the desktop app or another region/account",
8100
8280
  "row.probeChannelRejected": "✗ The gateway refused the PROMPT itself, not this account — the opening of a system message matched its third-party-agent filter, so another account or region answers the same way",
8281
+ "row.probeChannelRejectedShort": "✗ Gateway refused the prompt",
8101
8282
  "row.probeNotFound": "✗ The upstream does not know this model",
8102
8283
  "row.probeUnavailable": "✗ The upstream could not be reached — no usable answer came back",
8103
8284
  "row.probeFailed": "✗ Test failed",
@@ -8195,6 +8376,10 @@ window.__ModuleLoader__.load({
8195
8376
  "row.remainingMinutes": "in about {count} min",
8196
8377
  "row.remainingHours": "in about {count} h",
8197
8378
  "row.remainingDays": "in about {count} d",
8379
+ "row.recoversImminent": "recovering any moment now",
8380
+ "row.recoversMinutes": "recovers in about {count} min",
8381
+ "row.recoversHours": "recovers in about {count} h",
8382
+ "row.recoversDays": "recovers in about {count} d",
8198
8383
  "row.poolProbeJustNow": "just now",
8199
8384
  "row.poolProbeMinutesAgo": "{count} min ago",
8200
8385
  "row.poolProbeHoursAgo": "{count} h ago",
@@ -8284,6 +8469,8 @@ window.__ModuleLoader__.load({
8284
8469
  "row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
8285
8470
  "row.poolRowRemoving": "Removing the account…",
8286
8471
  "row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
8472
+ "row.cliBridgeLabel": "Let the local CodeBuddy CLI do the work",
8473
+ "row.cliBridgeHint": "Adds the codebuddy_ask tool and the /codebuddy command. Each call spends CodeBuddy quota (and, with injectResult on, one DSH turn). Off by default.",
8287
8474
  "row.imageToolLabel": "Let the model draw images",
8288
8475
  "row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image, with no per-call confirmation. Off means the tool is not registered at all.",
8289
8476
  "row.showCreditsCn": "Show credits in the composer toolbar",
@@ -8457,6 +8644,7 @@ window.__ModuleLoader__.load({
8457
8644
  "row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
8458
8645
  "row.probePolicyRejected": "✗ WorkBuddy 服务端按内容策略拒绝了这次请求——重新登录无效;可在桌面端验证或切换区域/账号",
8459
8646
  "row.probeChannelRejected": "✗ 网关拦的是提示词本身,不是这个账号——某条 system 消息的开头命中了它的第三方 agent 过滤,换账号或换区域都会得到同一个答案(测试能过、聊天被拒,正是这个原因)",
8647
+ "row.probeChannelRejectedShort": "✗ 网关拦的是提示词",
8460
8648
  "row.probeNotFound": "✗ 上游不认识这个模型",
8461
8649
  "row.probeUnavailable": "✗ 连不上上游——上游没有给出可用答复",
8462
8650
  "row.probeFailed": "✗ 测试失败",
@@ -8554,6 +8742,10 @@ window.__ModuleLoader__.load({
8554
8742
  "row.remainingMinutes": "约 {count} 分钟后",
8555
8743
  "row.remainingHours": "约 {count} 小时后",
8556
8744
  "row.remainingDays": "约 {count} 天后",
8745
+ "row.recoversImminent": "即将恢复",
8746
+ "row.recoversMinutes": "约 {count} 分钟后恢复",
8747
+ "row.recoversHours": "约 {count} 小时后恢复",
8748
+ "row.recoversDays": "约 {count} 天后恢复",
8557
8749
  "row.poolProbeJustNow": "刚刚",
8558
8750
  "row.poolProbeMinutesAgo": "{count} 分钟前",
8559
8751
  "row.poolProbeHoursAgo": "{count} 小时前",
@@ -8643,6 +8835,8 @@ window.__ModuleLoader__.load({
8643
8835
  "row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
8644
8836
  "row.poolRowRemoving": "正在删除账号…",
8645
8837
  "row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
8838
+ "row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
8839
+ "row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy 配额(injectResult 开着时还会再花一次 DSH token)。默认关闭。",
8646
8840
  "row.imageToolLabel": "允许模型自主生图",
8647
8841
  "row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——约 5.71 积分/张,没有逐次确认。关闭时该工具根本不会注册。",
8648
8842
  "row.showCreditsCn": "在输入框工具栏显示积分读数",
@@ -5861,7 +5861,7 @@ function generationOf(version) {
5861
5861
  //#endregion
5862
5862
  //#region src/version.ts
5863
5863
  /** The npm package version this build was produced from. */
5864
- const WORKBUDDY_CONNECT_VERSION = "5.2.3";
5864
+ const WORKBUDDY_CONNECT_VERSION = "5.3.0";
5865
5865
  //#endregion
5866
5866
  //#region src/host-heartbeat.ts
5867
5867
  /**
package/lib/index.d.ts CHANGED
@@ -1556,6 +1556,32 @@ declare class WorkBuddyCatalog {
1556
1556
  set(models: readonly WorkBuddyModelInfo[]): void;
1557
1557
  }
1558
1558
  //#endregion
1559
+ //#region src/cli-bridge/mount.d.ts
1560
+ /**
1561
+ * The `cliBridge` settings as they arrive from the config schema.
1562
+ *
1563
+ * Every field is optional because the schema may hand over a partial object, and
1564
+ * a user who edits only one key must not have to restate the rest.
1565
+ */
1566
+ interface CliBridgeSettings {
1567
+ enabled?: boolean;
1568
+ binary?: string;
1569
+ model?: string;
1570
+ permissionMode?: string;
1571
+ effort?: string;
1572
+ maxTurns?: number;
1573
+ timeoutSecs?: number;
1574
+ regionOrder?: string;
1575
+ workingDir?: string;
1576
+ allowedDirs?: string;
1577
+ appendSystemPrompt?: string;
1578
+ allowUnattended?: boolean;
1579
+ maxReplyChars?: number;
1580
+ injectResult?: boolean;
1581
+ maxTurnsPerSessionHour?: number;
1582
+ maxTurnsPerHour?: number;
1583
+ }
1584
+ //#endregion
1559
1585
  //#region src/off-thinking.d.ts
1560
1586
  /**
1561
1587
  * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
@@ -3104,6 +3130,17 @@ type WorkBuddyWebUsage =
3104
3130
  * after a switch-off that never landed.
3105
3131
  */
3106
3132
  imageToolEnabled?: boolean;
3133
+ /**
3134
+ * Whether the OPTIONAL CodeBuddy CLI route is mounted (tool `codebuddy_ask`
3135
+ * + command `/codebuddy`).
3136
+ *
3137
+ * GLOBAL, not per region: the route has ONE switch for both regions, so the
3138
+ * same value rides on every region's answer and the card reads it off the
3139
+ * active tab. Absent reads as OFF — this switch authorizes a child process
3140
+ * that edits files and spends CodeBuddy quota, so a Host that cannot answer
3141
+ * must not be read as consent.
3142
+ */
3143
+ cliBridgeEnabled?: boolean;
3107
3144
  /** WHERE this region's readout sits (issue #62); absent means `composer`. */
3108
3145
  creditPosition?: WorkBuddyCreditPosition;
3109
3146
  message?: string;
@@ -3149,6 +3186,17 @@ type WorkBuddyWebUsage =
3149
3186
  * after a switch-off that never landed.
3150
3187
  */
3151
3188
  imageToolEnabled?: boolean;
3189
+ /**
3190
+ * Whether the OPTIONAL CodeBuddy CLI route is mounted (tool `codebuddy_ask`
3191
+ * + command `/codebuddy`).
3192
+ *
3193
+ * GLOBAL, not per region: the route has ONE switch for both regions, so the
3194
+ * same value rides on every region's answer and the card reads it off the
3195
+ * active tab. Absent reads as OFF — this switch authorizes a child process
3196
+ * that edits files and spends CodeBuddy quota, so a Host that cannot answer
3197
+ * must not be read as consent.
3198
+ */
3199
+ cliBridgeEnabled?: boolean;
3152
3200
  /** WHERE this region's readout sits (issue #62); absent means `composer`. */
3153
3201
  creditPosition?: WorkBuddyCreditPosition;
3154
3202
  source?: 'desktop' | 'dsh';
@@ -3629,6 +3677,19 @@ interface WorkBuddyStatusRouteOptions {
3629
3677
  * cannot answer must not be read as consent.
3630
3678
  */
3631
3679
  imageToolEnabled?(region: WorkBuddyRegion): boolean;
3680
+ /**
3681
+ * Whether the OPTIONAL CodeBuddy CLI route is mounted (`codebuddy_ask` tool +
3682
+ * `/codebuddy` command).
3683
+ *
3684
+ * GLOBAL rather than per region — one switch, one route — so it takes no
3685
+ * argument. Optional, and absent reads as OFF: this switch authorizes a child
3686
+ * process to edit files and spend CodeBuddy quota, so an older Host that
3687
+ * cannot answer must not be read as consent. The card reads it off the Host's
3688
+ * answer for the same measured reason as `imageToolEnabled`: the browser's
3689
+ * settings mirror is stale for this namespace on the affected 0.1.7
3690
+ * deployments, and a stale `true` here would claim a spending capability is on.
3691
+ */
3692
+ cliBridgeEnabled?(): boolean;
3632
3693
  /**
3633
3694
  * Send one real-volume request to each named model of a region and report what
3634
3695
  * came back.
@@ -4207,6 +4268,15 @@ interface Config {
4207
4268
  imageModelIds?: string[];
4208
4269
  /** @deprecated See {@link Config.lastCatalog}. */
4209
4270
  contextBudgets?: Record<string, WorkBuddyContextBudget>;
4271
+ /**
4272
+ * The optional CLI route: let the locally signed-in CodeBuddy CLI do the work
4273
+ * in a separate agent instead of proxying the gateway.
4274
+ *
4275
+ * It is OFF by default and completely independent of the provider stacks, so a
4276
+ * config that never mentions it behaves exactly as before. This is a
4277
+ * client-fingerprint workaround, not an account one — see `cli-bridge/runner`.
4278
+ */
4279
+ cliBridge?: CliBridgeSettings;
4210
4280
  }
4211
4281
  /**
4212
4282
  * Mark a schema's field as volatile on the DSH lines that support it.
@@ -4224,28 +4294,6 @@ interface Config {
4224
4294
  * moved up — which is exactly how the volatile-path defect below went unseen.
4225
4295
  */
4226
4296
  declare function asVolatile<T>(schema: z<T>): z<T>;
4227
- /**
4228
- * Deep copy of a config value with every `{get(): T}` live reference replaced
4229
- * by the value it resolves to.
4230
- *
4231
- * {@link unwrapVolatile} only peels the ONE level a caller reads, which is all
4232
- * the ordinary read paths need. Handing a config object to a settings service
4233
- * is different: the service validates and `structuredClone`s the WHOLE object,
4234
- * so a reference surviving anywhere inside it fails schema validation with a
4235
- * message that names the field but not the cause —
4236
- * `$.authFile expected string but got [object Object]`.
4237
- *
4238
- * That is exactly what happened on the 0.1.5 line once volatile marking became
4239
- * active: `installSection` received the raw config, whose volatile fields were
4240
- * live references, and every field it validated threw. The namespace never
4241
- * registered, so the card's settings silently disappeared.
4242
- *
4243
- * The implementation now lives in the node-free host<->client bridge, because
4244
- * the browser half needs the SAME unwrap before it spreads a resolved field —
4245
- * spreading a live reference is `{get: <function>}`, which drops every sibling
4246
- * region. It is imported and re-exported from here so the Host entry keeps
4247
- * exposing it as part of its public API.
4248
- */
4249
4297
  declare const Config: z<Config>;
4250
4298
  /**
4251
4299
  * One region's saved model state. A config written before the region split has