dsh-model-switch 0.4.41 → 0.4.42

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
@@ -444,10 +444,22 @@ window.__ModuleLoader__.load({
444
444
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
445
445
  const status = raw;
446
446
  const count = (input) => typeof input === "number" && Number.isFinite(input) && input >= 0 ? input : 0;
447
+ const route = (input) => typeof input === "string" && input.trim() !== "" ? input.trim() : void 0;
448
+ const automode = status.automode !== null && typeof status.automode === "object" && !Array.isArray(status.automode) ? (() => {
449
+ const row = status.automode;
450
+ if (row.installed !== true) return void 0;
451
+ return {
452
+ route: route(row.route),
453
+ routeSkipped: route(row.routeSkipped),
454
+ added: count(row.added),
455
+ skipped: count(row.skipped)
456
+ };
457
+ })() : void 0;
447
458
  return {
448
459
  held: status.held === true,
449
460
  allowedCalls: count(status.allowedCalls),
450
- skippedReadOnly: count(status.skippedReadOnly)
461
+ skippedReadOnly: count(status.skippedReadOnly),
462
+ automode
451
463
  };
452
464
  }
453
465
  /**
@@ -2603,6 +2615,10 @@ window.__ModuleLoader__.load({
2603
2615
  className: ModelSwitchSettings_module_css_default.hint,
2604
2616
  children: props.t("reviewAllowlistSkipped").replace("{count}", String(reviewStatus.skippedReadOnly))
2605
2617
  }),
2618
+ reviewStatus?.automode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2619
+ className: ModelSwitchSettings_module_css_default.hint,
2620
+ children: props.t("reviewAutomodeLinked").replace("{route}", reviewStatus.automode.route ?? (reviewStatus.automode.routeSkipped === "user-set" ? props.t("reviewAutomodeRouteOwn") : props.t("reviewAutomodeRouteUnset"))).replace("{added}", String(reviewStatus.automode.added)).replace("{skipped}", String(reviewStatus.automode.skipped))
2621
+ }),
2606
2622
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2607
2623
  className: ModelSwitchSettings_module_css_default.allowList,
2608
2624
  children: allowlistIncluded.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
@@ -3049,6 +3065,9 @@ window.__ModuleLoader__.load({
3049
3065
  reviewAllowlistAvailable: "本机存在但未放行的工具:",
3050
3066
  reviewAllowlistRepo: "仓库",
3051
3067
  reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。",
3068
+ reviewAutomodeLinked: "Auto mode 已接入:审查路由 {route},直通 +{added} 个纯读工具({skipped} 条通配未同步)。",
3069
+ reviewAutomodeRouteOwn: "沿用其自身配置",
3070
+ reviewAutomodeRouteUnset: "未配置(沿用会话模型)",
3052
3071
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
3053
3072
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
3054
3073
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -3129,6 +3148,9 @@ window.__ModuleLoader__.load({
3129
3148
  reviewAllowlistAvailable: "Present on this machine but not allowed:",
3130
3149
  reviewAllowlistRepo: "repo",
3131
3150
  reviewAllowlistSkipped: "This Host process has skipped the reviewer {count} times.",
3151
+ reviewAutomodeLinked: "Auto mode linked: reviewer route {route}, passes +{added} read-only tools ({skipped} pattern entries not synced).",
3152
+ reviewAutomodeRouteOwn: "its own settings",
3153
+ reviewAutomodeRouteUnset: "unset (follows the session model)",
3132
3154
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
3133
3155
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
3134
3156
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { o as normalizeProviderGroups } from "./session-route-D1zoZgf3.js";
2
- import { h as resolveToolSource, i as autoReviewStatus, m as DEFAULT_READ_ONLY_TOOLS } from "./auto-review-BDx3LjVL.js";
2
+ import { r as resolveToolSource, t as DEFAULT_READ_ONLY_TOOLS } from "./tool-allowlist-DlzB5GlQ.js";
3
+ import { i as autoReviewStatus } from "./auto-review-QDEFUjT2.js";
3
4
  import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
5
+ import { automodeCompatView } from "./automode-compat.js";
4
6
  import { createRequire } from "node:module";
5
7
  import z from "@deepseek-ai/schemastery";
6
8
  import { Service } from "@deepseek-ai/cordis";
@@ -526,7 +528,8 @@ function installCapabilitiesRpc(ctx, registry, snapshot) {
526
528
  }), () => ({
527
529
  held: autoReviewStatus.held,
528
530
  allowedCalls: autoReviewStatus.allowedCalls,
529
- skippedReadOnly: autoReviewStatus.skippedReadOnly
531
+ skippedReadOnly: autoReviewStatus.skippedReadOnly,
532
+ automode: automodeCompatView()
530
533
  }));
531
534
  const dispose = scope.connection.fetch.register({
532
535
  path: "/api/plugin-rpc/model-switch",
@@ -0,0 +1,332 @@
1
+ /** Catalog lookup by id. */
2
+ const SOURCE_BY_ID = new Map([
3
+ {
4
+ id: "core",
5
+ kind: "core",
6
+ label: "DSH 核心",
7
+ repo: "https://github.com/deepseek-ai/deepseek-harness"
8
+ },
9
+ {
10
+ id: "dsh-ops",
11
+ kind: "plugin",
12
+ label: "插件 · dsh-ops",
13
+ repo: "https://github.com/T-Auto/dsh-ops"
14
+ },
15
+ {
16
+ id: "dsh-browser-cdp",
17
+ kind: "plugin",
18
+ label: "插件 · dsh-browser-cdp",
19
+ repo: "https://github.com/drscrewdriver/dsh-browser-cdp"
20
+ },
21
+ {
22
+ id: "dsh-auto-memory",
23
+ kind: "plugin",
24
+ label: "插件 · dsh-auto-memory",
25
+ repo: "https://github.com/AskTheWay/dsh-auto-memory"
26
+ },
27
+ {
28
+ id: "dsh-model-switch",
29
+ kind: "plugin",
30
+ label: "插件 · dsh-model-switch",
31
+ repo: "https://github.com/vandoor7-droid/dsh-model-switch"
32
+ },
33
+ {
34
+ id: "mcp:wincu",
35
+ kind: "mcp",
36
+ label: "MCP · wincu(dsh-computer-use-win)",
37
+ repo: "https://github.com/Yu-tao-Li/dsh-computer-use-win"
38
+ },
39
+ {
40
+ id: "qqbot-extension",
41
+ kind: "extension",
42
+ label: "QQ 机器人扩展(框架 @zaofan/dsh-qqbot)",
43
+ repo: "https://github.com/zaofan-make/dsh-qqbot"
44
+ }
45
+ ].map((entry) => [entry.id, entry]));
46
+ /**
47
+ * Tool names the released Host owns, calibrated against dsh 0.2.1-rc.2.
48
+ *
49
+ * A name outside the catalog would otherwise be reported as core, which is the
50
+ * one guess a reader could act on wrongly; it resolves to `unknown` instead.
51
+ */
52
+ const CORE_TOOLS = /* @__PURE__ */ new Set([
53
+ "ask_user_question",
54
+ "create_goal",
55
+ "create_skill",
56
+ "create_worktree",
57
+ "edit",
58
+ "exit_plan_mode",
59
+ "get_goal",
60
+ "glob",
61
+ "grep",
62
+ "interrupt_agent",
63
+ "job_kill",
64
+ "job_list",
65
+ "job_output",
66
+ "list_agents",
67
+ "list_mcp_resource_templates",
68
+ "list_mcp_resources",
69
+ "load_workspace_dependencies",
70
+ "present",
71
+ "pwsh",
72
+ "ralph",
73
+ "read",
74
+ "read_image",
75
+ "read_mcp_resource",
76
+ "schedule_create",
77
+ "schedule_delete",
78
+ "schedule_list",
79
+ "schedule_update",
80
+ "send_message",
81
+ "session_event_read",
82
+ "session_event_search",
83
+ "session_event_trace",
84
+ "session_search",
85
+ "session_trace",
86
+ "sidebar_open",
87
+ "skill",
88
+ "spawn_teammate",
89
+ "subagent",
90
+ "subagent_fork",
91
+ "team_task_create",
92
+ "team_task_get",
93
+ "team_task_list",
94
+ "team_task_update",
95
+ "terminal_close",
96
+ "terminal_list",
97
+ "terminal_open",
98
+ "terminal_read",
99
+ "terminal_send",
100
+ "terminal_signal",
101
+ "todo_write",
102
+ "update_goal",
103
+ "wait_agent",
104
+ "web_fetch",
105
+ "web_search",
106
+ "web_search_pro",
107
+ "workflow",
108
+ "working_directory",
109
+ "write"
110
+ ]);
111
+ /** MCP server name → catalog id, for the servers this allowlist knows. */
112
+ const MCP_SERVERS = /* @__PURE__ */ new Map([["wincu", "mcp:wincu"]]);
113
+ /** Plugin identity by tool-name prefix (checked before the core catalog). */
114
+ const PLUGIN_PREFIXES = [
115
+ ["bcdp_", "dsh-browser-cdp"],
116
+ ["browser_window_", "dsh-browser-cdp"],
117
+ ["ops_", "dsh-ops"],
118
+ ["memory_", "dsh-auto-memory"]
119
+ ];
120
+ /** Single plugin tools that carry no prefix. */
121
+ const PLUGIN_TOOLS = /* @__PURE__ */ new Map([["generate_image", "dsh-model-switch"]]);
122
+ /** `<base>__<owner>` — the extension-tool naming the QQ bot framework uses. */
123
+ const EXTENSION_TOOL = /^([A-Za-z0-9_-]+)__(.+)$/;
124
+ /**
125
+ * Default allowlist: every pure-read tool this deployment exposes plus the
126
+ * no-side-effect and autonomy entries the user explicitly released, calibrated
127
+ * on a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
128
+ *
129
+ * Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
130
+ * Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
131
+ * the bot's own name, so `sessions_list__*` keeps working when the persona is
132
+ * renamed.
133
+ *
134
+ * Everything here follows one rule: releasing it must not let a side effect escape
135
+ * the reviewer. Calls that merely dispatch, ask, present, wait or navigate qualify
136
+ * because the work they trigger still runs under the same auto preset. Delegation
137
+ * tools (`subagent`, `subagent_fork`, `spawn_teammate`) deliberately stay out: a
138
+ * child session seeds its preset from `permissionPresets.current(parent.session)`,
139
+ * and this bypass answers `'custom'` for an allowlisted call in flight — releasing
140
+ * them would hand the child no `auto` preset at all, i.e. no review.
141
+ */
142
+ const DEFAULT_READ_ONLY_TOOLS = [
143
+ "read",
144
+ "grep",
145
+ "glob",
146
+ "read_image",
147
+ "skill",
148
+ "web_search",
149
+ "web_search_pro",
150
+ "web_fetch",
151
+ "load_workspace_dependencies",
152
+ "ask_user_question",
153
+ "present",
154
+ "sidebar_open",
155
+ "exit_plan_mode",
156
+ "job_list",
157
+ "job_output",
158
+ "terminal_list",
159
+ "terminal_read",
160
+ "list_agents",
161
+ "team_task_list",
162
+ "team_task_get",
163
+ "get_goal",
164
+ "schedule_list",
165
+ "wait_agent",
166
+ "send_message",
167
+ "team_task_create",
168
+ "list_mcp_resources",
169
+ "list_mcp_resource_templates",
170
+ "read_mcp_resource",
171
+ "session_search",
172
+ "session_event_search",
173
+ "session_event_read",
174
+ "session_event_trace",
175
+ "session_trace",
176
+ "create_goal",
177
+ "schedule_create",
178
+ "terminal_open",
179
+ "ops_read_files",
180
+ "ops_discover_tools",
181
+ "bcdp_status",
182
+ "bcdp_help",
183
+ "bcdp_doctor",
184
+ "bcdp_snapshot",
185
+ "browser_window_status",
186
+ "memory_list",
187
+ "memory_read",
188
+ "mcp__wincu__windows_computer_use_accessibility_tree",
189
+ "mcp__wincu__windows_computer_use_element_info",
190
+ "mcp__wincu__windows_computer_use_find",
191
+ "mcp__wincu__windows_computer_use_health",
192
+ "mcp__wincu__windows_computer_use_list_windows",
193
+ "mcp__wincu__windows_computer_use_ocr",
194
+ "mcp__wincu__windows_computer_use_screenshot",
195
+ "mcp__wincu__windows_computer_use_snapshot",
196
+ "mcp__wincu__windows_computer_use_tools",
197
+ "mcp__wincu__windows_computer_use_wait",
198
+ "mcp__wincu__windows_computer_use_wait_for",
199
+ "sessions_list__*",
200
+ "bing_search__*"
201
+ ];
202
+ /**
203
+ * Validate one allowlist entry.
204
+ *
205
+ * `*` is a suffix wildcard: the entry must keep at least one literal leading
206
+ * character, so a stray `*` can never turn the list into "allow everything".
207
+ *
208
+ * @param entry - the raw entry as typed in settings.
209
+ * @returns `ok`, or the specific problem.
210
+ */
211
+ function validateAllowEntry(entry) {
212
+ const trimmed = entry.trim();
213
+ if (trimmed === "") return "empty";
214
+ if (/^\*+$/.test(trimmed)) return "bare-wildcard";
215
+ if (trimmed.startsWith("*")) return "leading-wildcard";
216
+ return "ok";
217
+ }
218
+ /**
219
+ * Whether a tool name matches one allowlist entry.
220
+ *
221
+ * Exact names match exactly; an entry may end in `*`, which matches any
222
+ * remaining characters (including none). Invalid entries never match.
223
+ *
224
+ * @param name - the tool name from `exec.name`.
225
+ * @param entry - one allowlist entry.
226
+ * @returns true when the call may skip review.
227
+ */
228
+ function matchesAllowPattern(name, entry) {
229
+ if (name === "" || validateAllowEntry(entry) !== "ok") return false;
230
+ const trimmed = entry.trim();
231
+ const star = trimmed.indexOf("*");
232
+ if (star === -1) return name === trimmed;
233
+ const prefix = trimmed.slice(0, star);
234
+ const suffix = trimmed.slice(star + 1);
235
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
236
+ return name.length >= prefix.length + suffix.length;
237
+ }
238
+ /**
239
+ * Whether a tool name is covered by an allowlist.
240
+ * @param name - the tool name under review.
241
+ * @param allowlist - configured entries, patterns included.
242
+ * @returns true when at least one entry matches.
243
+ */
244
+ function isAllowedByList(name, allowlist) {
245
+ return allowlist.some((entry) => matchesAllowPattern(name, entry));
246
+ }
247
+ /**
248
+ * Resolve the origin of one tool name for display.
249
+ *
250
+ * Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
251
+ * plugin table → core catalog → unknown. Only the first two are structural; the
252
+ * rest are calibrated names, so an unlisted tool is reported as unknown rather
253
+ * than guessed into a package.
254
+ *
255
+ * @param name - the tool name.
256
+ * @returns The origin, with its repository when known.
257
+ */
258
+ function resolveToolSource(name) {
259
+ const trimmed = name.trim().replace(/\*+$/, "");
260
+ const mcp = /^mcp__([A-Za-z0-9_-]+)__/.exec(trimmed);
261
+ if (mcp !== null) {
262
+ const server = mcp[1];
263
+ const entry = SOURCE_BY_ID.get(MCP_SERVERS.get(server) ?? "");
264
+ if (entry !== void 0) return {
265
+ kind: "mcp",
266
+ label: entry.label,
267
+ repo: entry.repo,
268
+ owner: server
269
+ };
270
+ return {
271
+ kind: "mcp",
272
+ label: `MCP · ${server}`,
273
+ owner: server
274
+ };
275
+ }
276
+ const extension = EXTENSION_TOOL.exec(trimmed);
277
+ if (extension !== null && !trimmed.startsWith("mcp__")) {
278
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
279
+ const base = extension[1];
280
+ const owner = extension[2];
281
+ return {
282
+ kind: "extension",
283
+ label: `扩展 · ${base}__${owner}`,
284
+ ...entry === void 0 ? {} : { repo: entry.repo },
285
+ owner
286
+ };
287
+ }
288
+ if (trimmed.endsWith("__")) {
289
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
290
+ return {
291
+ kind: "extension",
292
+ label: `扩展 · ${trimmed.slice(0, -2)}__*`,
293
+ ...entry === void 0 ? {} : { repo: entry.repo },
294
+ owner: "*"
295
+ };
296
+ }
297
+ for (const [prefix, id] of PLUGIN_PREFIXES) {
298
+ if (!trimmed.startsWith(prefix)) continue;
299
+ const entry = SOURCE_BY_ID.get(id);
300
+ if (entry !== void 0) return {
301
+ kind: "plugin",
302
+ label: entry.label,
303
+ repo: entry.repo,
304
+ owner: id
305
+ };
306
+ }
307
+ const plugin = PLUGIN_TOOLS.get(trimmed);
308
+ if (plugin !== void 0) {
309
+ const entry = SOURCE_BY_ID.get(plugin);
310
+ if (entry !== void 0) return {
311
+ kind: "plugin",
312
+ label: entry.label,
313
+ repo: entry.repo,
314
+ owner: plugin
315
+ };
316
+ }
317
+ if (CORE_TOOLS.has(trimmed)) {
318
+ const entry = SOURCE_BY_ID.get("core");
319
+ return {
320
+ kind: "core",
321
+ label: entry?.label ?? "DSH 核心",
322
+ ...entry === void 0 ? {} : { repo: entry.repo },
323
+ owner: "core"
324
+ };
325
+ }
326
+ return {
327
+ kind: "unknown",
328
+ label: "未知来源"
329
+ };
330
+ }
331
+ //#endregion
332
+ export { isAllowedByList as n, resolveToolSource as r, DEFAULT_READ_ONLY_TOOLS as t };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Adapt the third-party `@log.li/dsh-automode` row to this plugin's routes.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * `dsh-automode` owns a completely different decision pipeline: its own
7
+ * `auto-mode` permission preset, a `tools/pre-execute` gate, and a two-stage
8
+ * classifier that calls `ctx.llm.stream()` with
9
+ * `classifier.provider` / `classifier.model` (`resolveRoute`, falling back to
10
+ * the session header when both are empty). It reads the pure-read allowance from
11
+ * `config.readOnlyTools` with an exact `includes()` match
12
+ * (`src/pre-execute.ts`), so a call that hits it is allowed before the
13
+ * classifier runs — still after the plugin's own hard-deny regex band.
14
+ *
15
+ * Both knobs live in that row's own config. There is no public seam to inject a
16
+ * route or a tool list from outside, and the user asked for the adaptation to
17
+ * happen inside the plugin layer: no profile editing, no second plugin. This
18
+ * module is that adapter. It rewrites the automode row's **runtime** option
19
+ * object right before the loader mounts it and on every remount:
20
+ *
21
+ * - `classifier.provider` / `classifier.model` are filled from this plugin's
22
+ * Auto-review route **only when the row leaves them empty**; an explicit route
23
+ * in the profile always wins.
24
+ * - `readOnlyTools` is the union of the row's own list and this plugin's
25
+ * allowlist — but only while the "skip pure-read tools" switch is on. Entries
26
+ * that carry a `*` cannot transfer (the third-party check is exact-match) and
27
+ * are reported instead of silently dropped.
28
+ *
29
+ * Nothing is persisted: `entry.options` is the loader's live options object, and
30
+ * `Entry.update()` (which writes the profile layer) is deliberately never called
31
+ * here. Restarting the Host re-applies the same in-memory view.
32
+ *
33
+ * Every failure is contained. If the row shape or the loader internals change,
34
+ * this adapter logs one warning and the rest of the plugin keeps working.
35
+ *
36
+ * @module
37
+ */
38
+ import type { Context } from '@deepseek-ai/cordis';
39
+ import type { Loader } from '@deepseek-ai/cordis-plugin-loader';
40
+ import type { ModelSwitchSettings } from './host-settings.js';
41
+ /** Cordis plugin identity; matches the `cordis.patch.yml` insert row id. */
42
+ export declare const name = "model-switch-automode-compat";
43
+ /** The loader is the only seam this row needs besides this plugin's settings. */
44
+ export declare const inject: string[];
45
+ /** Third-party package this row adapts (strict package identity, never a prefix). */
46
+ export declare const AUTOMODE_PACKAGE = "@log.li/dsh-automode";
47
+ /**
48
+ * Why the route was not injected.
49
+ *
50
+ * - `user-set`: the row already names a provider and a model.
51
+ * - `no-route`: this plugin has no Auto-review route configured right now.
52
+ */
53
+ export type AutomodeRouteSkip = 'user-set' | 'no-route';
54
+ /** Result of adapting one row's config object. */
55
+ export interface AutomodeCompatApplied {
56
+ /** `provider/model` written into the row, when it was empty before. */
57
+ readonly route?: string | undefined;
58
+ /** Set when the route was left alone. */
59
+ readonly routeSkipped?: AutomodeRouteSkip | undefined;
60
+ /** Allowlist entries merged into `readOnlyTools`. */
61
+ readonly added: readonly string[];
62
+ /** Allowlist entries that cannot transfer because they are pattern-shaped. */
63
+ readonly skipped: readonly string[];
64
+ }
65
+ /** One adaptation result: the config to install plus what changed. */
66
+ export interface AutomodeCompatPlan {
67
+ /** The config object to assign to the row. */
68
+ readonly config: Record<string, unknown>;
69
+ /** Whether `config` differs from the input (false means "leave it alone"). */
70
+ readonly changed: boolean;
71
+ /** What was applied, for the status line and the log. */
72
+ readonly applied: AutomodeCompatApplied;
73
+ }
74
+ /** Live adapter state, surfaced next to the allowlist editor. */
75
+ export declare const automodeCompatStatus: {
76
+ /** A `@log.li/dsh-automode` row exists in this Host. */
77
+ installed: boolean;
78
+ /** `provider/model` this adapter wrote, when it wrote one. */
79
+ route: string | undefined;
80
+ /** Why the route was skipped, when it was. */
81
+ routeSkipped: AutomodeRouteSkip | undefined;
82
+ /** Tools merged into the row's `readOnlyTools`. */
83
+ added: string[];
84
+ /** Pattern-shaped entries that cannot transfer. */
85
+ skipped: string[];
86
+ /** How many rows were rewritten (remounts included). */
87
+ applied: number;
88
+ };
89
+ /**
90
+ * Whether one loader entry is the third-party auto-mode row.
91
+ * @param entry - anything the loader hands over (an entry, or an event payload).
92
+ * @returns true only for the exact package name.
93
+ */
94
+ export declare function isAutomodeEntry(entry: unknown): boolean;
95
+ /**
96
+ * Decide the config one automode row should run with.
97
+ *
98
+ * Pure and side-effect free so the policy is unit-testable.
99
+ *
100
+ * @param config - the row's current config (any JSON-ish value).
101
+ * @param route - this plugin's Auto-review route; empty strings mean "unset".
102
+ * @param allowlist - tool names to merge into `readOnlyTools`; pass an empty
103
+ * list while the skip switch is off.
104
+ * @param adoptedRoute - a `provider/model` this adapter wrote on an earlier
105
+ * pass; seeing it again means the row is still ours to rewrite.
106
+ * @returns the config to install, whether it changed, and what was applied.
107
+ */
108
+ export declare function planAutomodeCompat(config: unknown, route: {
109
+ readonly provider?: string;
110
+ readonly model?: string;
111
+ }, allowlist: readonly string[], adoptedRoute?: string): AutomodeCompatPlan;
112
+ /** One-line human summary of a plan, for the startup log. */
113
+ export declare function describeAutomodePlan(plan: AutomodeCompatPlan): string;
114
+ /** Shape the settings card renders for the third-party row, or undefined when absent. */
115
+ export interface AutomodeCompatView {
116
+ readonly installed: boolean;
117
+ /** `provider/model` written into the row, when one was. */
118
+ readonly route?: string | undefined;
119
+ /** Why no route was written, when none was. */
120
+ readonly routeSkipped?: AutomodeRouteSkip | undefined;
121
+ /** How many tools were merged in. */
122
+ readonly added: number;
123
+ /** How many pattern-shaped entries could not transfer. */
124
+ readonly skipped: number;
125
+ }
126
+ /**
127
+ * Snapshot for the RPC/UI layer.
128
+ * @returns the view, or undefined when this Host has no automode row.
129
+ */
130
+ export declare function automodeCompatView(): AutomodeCompatView | undefined;
131
+ /**
132
+ * Install the adapter: adapt live rows now, and every row the loader mounts later.
133
+ *
134
+ * The loader applies the profile patch layer and mounts the entry's plugin right
135
+ * after the `loader/patch-context` waterfall returns, so rewriting the entry's
136
+ * `options.config` from that waterfall is what makes the adapted values the ones
137
+ * the plugin activates with. No profile write happens here.
138
+ *
139
+ * @param ctx - this row's context.
140
+ * @param loader - the loader service, when the Host provides it.
141
+ * @param settings - reads the current plugin settings.
142
+ * @returns a disposer that detaches the listeners, plus a replay for live rows.
143
+ */
144
+ export declare function installAutomodeCompat(ctx: Context, loader: Loader | undefined, settings: () => ModelSwitchSettings): {
145
+ dispose: () => void;
146
+ replay: () => boolean;
147
+ };
148
+ /**
149
+ * Row entry point: adapt the third-party row on activation and on remount.
150
+ * @param ctx - this row's context.
151
+ */
152
+ export declare function apply(ctx: Context): void;
@@ -8,6 +8,14 @@ export interface ReviewStatus {
8
8
  readonly held: boolean;
9
9
  readonly allowedCalls: number;
10
10
  readonly skippedReadOnly: number;
11
+ /** Third-party `@log.li/dsh-automode` row, when this Host mounts one. */
12
+ readonly automode?: {
13
+ readonly installed: boolean;
14
+ readonly route?: string | undefined;
15
+ readonly routeSkipped?: string | undefined;
16
+ readonly added: number;
17
+ readonly skipped: number;
18
+ } | undefined;
11
19
  }
12
20
  export declare const CAPABILITIES_RPC_METHOD = "plugin-rpc/model-switch";
13
21
  export interface CapabilitiesSnapshot {
@@ -71,6 +71,9 @@ export declare const zh: {
71
71
  readonly reviewAllowlistAvailable: "本机存在但未放行的工具:";
72
72
  readonly reviewAllowlistRepo: "仓库";
73
73
  readonly reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。";
74
+ readonly reviewAutomodeLinked: "Auto mode 已接入:审查路由 {route},直通 +{added} 个纯读工具({skipped} 条通配未同步)。";
75
+ readonly reviewAutomodeRouteOwn: "沿用其自身配置";
76
+ readonly reviewAutomodeRouteUnset: "未配置(沿用会话模型)";
74
77
  readonly 'reason.central-subagent-routing': "Alpha.4 没有全局子代理启动路由接口。";
75
78
  readonly 'reason.packaged-preset-roots': "Alpha.4 不支持插件提供额外 preset root。";
76
79
  readonly 'reason.tool-owner-suppression': "Alpha.4 没有工具所有者或来源抑制接口。";
@@ -94,6 +94,13 @@ export interface ReviewStatusView {
94
94
  readonly held: boolean;
95
95
  readonly allowedCalls: number;
96
96
  readonly skippedReadOnly: number;
97
+ /** Third-party `@log.li/dsh-automode` row this Host adapted, when present. */
98
+ readonly automode?: {
99
+ readonly route?: string | undefined;
100
+ readonly routeSkipped?: string | undefined;
101
+ readonly added: number;
102
+ readonly skipped: number;
103
+ } | undefined;
97
104
  }
98
105
  /**
99
106
  * Decode the reviewer counters from a `tools` RPC payload.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-switch",
3
- "version": "0.4.41",
3
+ "version": "0.4.42",
4
4
  "description": "Explicit model routing plus composer/Plan picker for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,6 +58,10 @@
58
58
  "types": "./lib/types/adapter-registry.d.ts",
59
59
  "default": "./lib/adapter-registry.js"
60
60
  },
61
+ "./automode-compat": {
62
+ "types": "./lib/types/automode-compat.d.ts",
63
+ "default": "./lib/automode-compat.js"
64
+ },
61
65
  "./package.json": "./package.json"
62
66
  },
63
67
  "files": [