@linxin666/dsh-ssh 0.4.3 → 0.4.5

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.js CHANGED
@@ -1016,7 +1016,7 @@ function sweepPool(engine) {
1016
1016
  * Run `fn` with a live client for `alias`, reconnecting (up to the
1017
1017
  * attempt budget) when the connection broke mid-flight.
1018
1018
  */
1019
- async function withClient(engine, alias, fn, attempts = 3) {
1019
+ async function withClient(engine, alias, fn, attempts = 3, signal) {
1020
1020
  let lastError;
1021
1021
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
1022
1022
  let record = engine.pool.get(alias);
@@ -1032,6 +1032,7 @@ async function withClient(engine, alias, fn, attempts = 3) {
1032
1032
  return result;
1033
1033
  } catch (error) {
1034
1034
  lastError = error;
1035
+ if (signal?.aborted === true) throw error;
1035
1036
  if (!record.broken) throw error;
1036
1037
  disposeRecord(engine, alias, record);
1037
1038
  } finally {
@@ -1041,16 +1042,33 @@ async function withClient(engine, alias, fn, attempts = 3) {
1041
1042
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
1042
1043
  }
1043
1044
  /** Run one command on `alias` (reusing the pooled connection). */
1044
- async function execCommand(engine, alias, command, timeoutMs) {
1045
+ async function execCommand(engine, alias, command, timeoutMs, signal) {
1045
1046
  const started = Date.now();
1046
1047
  const budget = timeoutMs !== void 0 && timeoutMs > 0 ? timeoutMs : engine.opts.defaultExecTimeoutMs;
1047
1048
  return withClient(engine, alias, async (client) => {
1048
1049
  return await new Promise((resolve, reject) => {
1050
+ let aborted = signal !== void 0 && signal.aborted;
1051
+ let kill = () => {};
1052
+ const onAbort = () => {
1053
+ aborted = true;
1054
+ kill();
1055
+ };
1056
+ if (signal !== void 0 && !signal.aborted) signal.addEventListener("abort", onAbort, { once: true });
1049
1057
  client.exec(command, (error, stream) => {
1050
1058
  if (error !== void 0) {
1059
+ signal?.removeEventListener("abort", onAbort);
1051
1060
  reject(error);
1052
1061
  return;
1053
1062
  }
1063
+ kill = () => {
1064
+ try {
1065
+ stream.signal("KILL");
1066
+ } catch {}
1067
+ try {
1068
+ stream.close();
1069
+ } catch {}
1070
+ };
1071
+ if (aborted) kill();
1054
1072
  const stdout = {
1055
1073
  text: "",
1056
1074
  truncated: false
@@ -1061,10 +1079,21 @@ async function execCommand(engine, alias, command, timeoutMs) {
1061
1079
  };
1062
1080
  let timedOut = false;
1063
1081
  let settled = false;
1082
+ /** One cancelled outcome: the remote channel was killed on caller request. */
1083
+ const cancelledResult = () => ({
1084
+ success: false,
1085
+ exitCode: null,
1086
+ timedOut: false,
1087
+ stdout: stdout.text,
1088
+ stderr: stderr.text,
1089
+ durationMs: Date.now() - started,
1090
+ error: "command cancelled by the caller"
1091
+ });
1064
1092
  const finish = () => {
1065
1093
  if (settled) return;
1066
1094
  settled = true;
1067
1095
  clearTimeout(timer);
1096
+ signal?.removeEventListener("abort", onAbort);
1068
1097
  resolve({
1069
1098
  success: false,
1070
1099
  exitCode: null,
@@ -1091,6 +1120,11 @@ async function execCommand(engine, alias, command, timeoutMs) {
1091
1120
  if (settled) return;
1092
1121
  settled = true;
1093
1122
  clearTimeout(timer);
1123
+ signal?.removeEventListener("abort", onAbort);
1124
+ if (aborted) {
1125
+ resolve(cancelledResult());
1126
+ return;
1127
+ }
1094
1128
  if (typeof code !== "number" && !timedOut) {
1095
1129
  reject(/* @__PURE__ */ new Error("ssh: connection lost mid-flight (channel closed without an exit status)"));
1096
1130
  return;
@@ -1108,6 +1142,11 @@ async function execCommand(engine, alias, command, timeoutMs) {
1108
1142
  if (settled) return;
1109
1143
  settled = true;
1110
1144
  clearTimeout(timer);
1145
+ signal?.removeEventListener("abort", onAbort);
1146
+ if (aborted) {
1147
+ resolve(cancelledResult());
1148
+ return;
1149
+ }
1111
1150
  reject(streamError);
1112
1151
  });
1113
1152
  });
@@ -1535,7 +1574,7 @@ async function cluster(engine, options) {
1535
1574
  while (queue.length > 0) {
1536
1575
  const entry = queue.shift();
1537
1576
  try {
1538
- const result = await execCommand(engine, entry.alias, options.command, options.timeoutMs);
1577
+ const result = await execCommand(engine, entry.alias, options.command, options.timeoutMs, options.signal);
1539
1578
  results.push({
1540
1579
  alias: entry.alias,
1541
1580
  ok: result.success,
@@ -1595,8 +1634,8 @@ var SshEngine = class {
1595
1634
  return entry === void 0 ? void 0 : this.store.summarize(entry);
1596
1635
  }
1597
1636
  /** Run one command on `alias` (reusing the pooled connection). */
1598
- async exec(alias, command, timeoutMs) {
1599
- return execCommand(this, alias, command, timeoutMs);
1637
+ async exec(alias, command, timeoutMs, signal) {
1638
+ return execCommand(this, alias, command, timeoutMs, signal);
1600
1639
  }
1601
1640
  /** Run one command against many hosts concurrently. */
1602
1641
  async cluster(options) {
@@ -2594,7 +2633,7 @@ function renderTunnel(tunnel) {
2594
2633
  function sshListTool(engine) {
2595
2634
  return defineTool({
2596
2635
  name: "ssh_list",
2597
- description: "List configured SSH hosts (alias, host, user, auth, environment, tags, description). Use ssh_exec etc. with the alias. Triggers: SSH, remote server, server IP/hostname, connect/login, check server/status, deploy, upload/download, jump host, tunnel, port forward.",
2636
+ description: "List configured SSH hosts (alias, host, user, auth, environment, tags, description). Use ssh_exec etc. with the alias.",
2598
2637
  parameters: { query: {
2599
2638
  type: "string",
2600
2639
  description: "Optional fuzzy match against alias, description, host, and tags."
@@ -2670,6 +2709,7 @@ function sshListTool(engine) {
2670
2709
  },
2671
2710
  render: (_args, value) => text(renderHosts(value.hosts ?? []))
2672
2711
  },
2712
+ isConcurrencySafe: () => true,
2673
2713
  async execute(args) {
2674
2714
  return { hosts: engine.list(args.query).map(toAgentHostRow) };
2675
2715
  }
@@ -2679,7 +2719,7 @@ function sshListTool(engine) {
2679
2719
  function sshExecTool(engine) {
2680
2720
  return defineTool({
2681
2721
  name: "ssh_exec",
2682
- description: "Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command. Triggers: run command on server, deploy, check server/status, service control, view logs, any remote operation.",
2722
+ description: "Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command.",
2683
2723
  parameters: {
2684
2724
  alias: {
2685
2725
  type: "string",
@@ -2730,9 +2770,9 @@ function sshExecTool(engine) {
2730
2770
  },
2731
2771
  render: (_args, value) => text(renderExec(value))
2732
2772
  },
2733
- async execute(args) {
2773
+ async execute(args, exec) {
2734
2774
  try {
2735
- return await engine.exec(args.alias, args.command, args.timeoutMs);
2775
+ return await engine.exec(args.alias, args.command, args.timeoutMs, exec.signal);
2736
2776
  } catch (error) {
2737
2777
  return {
2738
2778
  success: false,
@@ -2751,7 +2791,7 @@ function sshExecTool(engine) {
2751
2791
  function sshUploadTool(engine) {
2752
2792
  return defineTool({
2753
2793
  name: "ssh_upload",
2754
- description: "Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload. Triggers: upload file to server, deploy artifact, copy config to server.",
2794
+ description: "Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload.",
2755
2795
  parameters: {
2756
2796
  alias: {
2757
2797
  type: "string",
@@ -2806,7 +2846,7 @@ function sshUploadTool(engine) {
2806
2846
  function sshDownloadTool(engine) {
2807
2847
  return defineTool({
2808
2848
  name: "ssh_download",
2809
- description: "Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually. Triggers: download file from server, fetch remote log/artifact.",
2849
+ description: "Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually.",
2810
2850
  parameters: {
2811
2851
  alias: {
2812
2852
  type: "string",
@@ -2858,7 +2898,7 @@ function sshDownloadTool(engine) {
2858
2898
  function sshTunnelTool(engine) {
2859
2899
  return defineTool({
2860
2900
  name: "ssh_tunnel",
2861
- description: "Manage local port-forward tunnels to a configured SSH host. Start a tunnel to reach a remote internal service (database, web UI, API) through 127.0.0.1 on this machine. Triggers: tunnel, port forward, connect database, access internal service.",
2901
+ description: "Manage local port-forward tunnels to a configured SSH host. Start a tunnel to reach a remote internal service (database, web UI, API) through 127.0.0.1 on this machine.",
2862
2902
  parameters: {
2863
2903
  action: {
2864
2904
  type: "string",
@@ -3053,7 +3093,7 @@ function sshTunnelTool(engine) {
3053
3093
  function sshClusterTool(engine) {
3054
3094
  return defineTool({
3055
3095
  name: "ssh_cluster",
3056
- description: "Run one command concurrently across selected SSH hosts; at least one aliases, environment, or tags filter is required. Triggers: run on selected servers, batch operation, production servers, cluster command.",
3096
+ description: "Run one command concurrently across selected SSH hosts; at least one aliases, environment, or tags filter is required.",
3057
3097
  parameters: {
3058
3098
  command: {
3059
3099
  type: "string",
@@ -3114,9 +3154,12 @@ function sshClusterTool(engine) {
3114
3154
  },
3115
3155
  render: (_args, value) => text(renderCluster(value.results ?? []))
3116
3156
  },
3117
- async execute(args) {
3157
+ async execute(args, exec) {
3118
3158
  if (!(Array.isArray(args.aliases) && args.aliases.some((alias) => typeof alias === "string" && alias.trim() !== "") || typeof args.environment === "string" && args.environment.trim() !== "" || Array.isArray(args.tags) && args.tags.some((tag) => typeof tag === "string" && tag.trim() !== ""))) throw new Error("ssh_cluster requires aliases, environment, or tags to limit the target set");
3119
- return { results: await engine.cluster(args) };
3159
+ return { results: await engine.cluster({
3160
+ ...args,
3161
+ signal: exec.signal
3162
+ }) };
3120
3163
  }
3121
3164
  });
3122
3165
  }
@@ -3235,6 +3278,66 @@ function mountOnce(packageName, fn) {
3235
3278
  return mount;
3236
3279
  }
3237
3280
  //#endregion
3281
+ //#region src/tool-surface.ts
3282
+ /**
3283
+ * Family-shared tool-surface conventions for the packages that register
3284
+ * model-facing tools.
3285
+ *
3286
+ * One home for the prompt-section placement and the visibility gate this
3287
+ * family previously restated per package.
3288
+ *
3289
+ * Modelled on the official tool packages:
3290
+ * - `@deepseek-ai/dsh-tool-subagent` renders its section through
3291
+ * `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
3292
+ * disappears together with a restricted tool.
3293
+ * - 21 of the 22 official tool packages register their tools GLOBALLY and gate
3294
+ * them with a setting, not per agent. Only the Agent Teams tool package —
3295
+ * whose tools belong to one membership — installs into each `agent.ctx`.
3296
+ * This family follows the majority: registration stays global so the
3297
+ * registry's per-scope `restrict()` mask (which filters inherited global
3298
+ * tools and REJECTS scope-local names) can still withhold these tools.
3299
+ *
3300
+ * Structural types only: this copy lands in packages that do not all depend on
3301
+ * the same SDK service packages.
3302
+ */
3303
+ /**
3304
+ * Prompt-section order for each package's tool guidance.
3305
+ *
3306
+ * These sit in the family band between the deployment persona and the SDK's
3307
+ * `PLAN_POLICY` (500). Values are owned here rather than written per package so
3308
+ * the host plugins whose guidance shares one prompt cannot drift into an
3309
+ * accidental tie.
3310
+ */
3311
+ const PLUGIN_TOOL_SECTION_ORDERS = {
3312
+ ssh: 150,
3313
+ "task-board": 200
3314
+ };
3315
+ /**
3316
+ * Build a section provider that renders \`text\` only while at least one of
3317
+ * \`toolNames\` is visible to the rendering scope.
3318
+ *
3319
+ * The native convention: a tool that a restriction, a preset, or a missing
3320
+ * registry removed must not leave guidance behind telling the model to call it.
3321
+ * An unreadable registry renders the text unchanged — guidance that outlives a
3322
+ * momentary read failure is recoverable; guidance for a tool that is gone is
3323
+ * not.
3324
+ * @param tools - the scoped tool registry, when the deployment serves one.
3325
+ * @param toolNames - registered names the guidance describes.
3326
+ * @param text - the guidance rendered while they are reachable.
3327
+ * @returns the section text provider.
3328
+ */
3329
+ function visibleToolText(tools, toolNames, text) {
3330
+ if (tools === void 0) return () => text;
3331
+ return (context) => {
3332
+ for (const name of toolNames) try {
3333
+ if (tools.get(name, context.scope) !== void 0) return text;
3334
+ } catch {
3335
+ return text;
3336
+ }
3337
+ return "";
3338
+ };
3339
+ }
3340
+ //#endregion
3238
3341
  //#region src/index.ts
3239
3342
  /** Stable cordis plugin name. */
3240
3343
  const name = "ssh";
@@ -3275,8 +3378,17 @@ function profileConfig(ctx) {
3275
3378
  if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
3276
3379
  return config;
3277
3380
  }
3278
- /** Order of the announcement section within the tool-guidance band. */
3279
- const SECTION_ORDER = 150;
3381
+ /** Order of the announcement section within the shared tool-guidance band. */
3382
+ const SECTION_ORDER = PLUGIN_TOOL_SECTION_ORDERS.ssh;
3383
+ /** The six agent-tool names this plugin's announcement describes. */
3384
+ const SSH_TOOL_NAMES = [
3385
+ "ssh_list",
3386
+ "ssh_exec",
3387
+ "ssh_upload",
3388
+ "ssh_download",
3389
+ "ssh_tunnel",
3390
+ "ssh_cluster"
3391
+ ];
3280
3392
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
3281
3393
  const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 按 aliases/environment/tags 至少一种非空 selector 筛选后集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥、ProxyJump 跳板机(别名或 [user@]host[:port] 地址)与 OpenSSH 语义的 ProxyCommand(跳过堡垒机客户端场景,只能由用户在 GUI 配置,agent 不可写);Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
3282
3394
  /**
@@ -3332,7 +3444,7 @@ function applyImpl(ctx, config) {
3332
3444
  if (value.announceToAgent) disposeSection = ctx.systemPrompt.section({
3333
3445
  name: "plugin:dsh-ssh",
3334
3446
  order: SECTION_ORDER,
3335
- text: SSH_GUIDANCE
3447
+ text: visibleToolText(ctx.tools, SSH_TOOL_NAMES, SSH_GUIDANCE)
3336
3448
  });
3337
3449
  disposeRoutes = ctx.effect(() => {
3338
3450
  const disposers = routes.map((route) => ctx.webServer.register(route));
@@ -7,6 +7,44 @@ import { type ClusterResult, type ExecResult, type HostPayload, type ImportResul
7
7
  export declare class SshApiError extends Error {
8
8
  constructor(message: string);
9
9
  }
10
+ /** The location facts a terminal socket URL is built from. */
11
+ interface PageLocation {
12
+ /** Page scheme with its colon (for example `https:` or `dsh-app:`). */
13
+ protocol: string;
14
+ /** Page authority, empty for a scheme that carries none. */
15
+ host: string;
16
+ }
17
+ /**
18
+ * The WebSocket URL the terminal route is reached at, or undefined when this
19
+ * page cannot carry one.
20
+ *
21
+ * A web page resolves the socket against its own origin, exactly as before.
22
+ *
23
+ * A page delivered by an application on this machine (the official DSH Desktop
24
+ * shell serves its Web GUI from `dsh-app://app/`) cannot carry a socket on its
25
+ * own scheme: the protocol handler forwards HTTP to the local Host but has no
26
+ * upgrade to forward. That is exactly the case the official transport hook
27
+ * covers - the shell publishes `__DSH_TRANSPORT__.streamBaseUrl`, the loopback
28
+ * authority of the Host it owns and already forwards every other request to
29
+ * (issue #1744). The socket is therefore dialed there instead, with the `ws`/
30
+ * `wss` scheme derived from that base, which is what the shell's
31
+ * `onBeforeSendHeaders` hook expects to see before it attaches its
32
+ * authority-bound credential.
33
+ *
34
+ * The base is used only on an application-delivered page. A web page that
35
+ * somehow carries the hook (the remote channel grants `ownsHost` to a paired
36
+ * LAN page, and that page is fenced behind a pairing channel) keeps resolving
37
+ * against its own origin, so this cannot reroute gated traffic onto a host the
38
+ * page is not entitled to reach. A blank authority is treated the same as an
39
+ * application scheme with no base: there is nothing to dial, and the caller
40
+ * gets the actionable reason instead of a socket that can only fail.
41
+ *
42
+ * @param location - the page location to read.
43
+ * @param search - the query string carrying `alias` or `session`.
44
+ * @param streamBaseUrl - `__DSH_TRANSPORT__.streamBaseUrl`, when the shell published one.
45
+ * @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
46
+ */
47
+ export declare function terminalSocketUrl(location: PageLocation, search: string, streamBaseUrl?: string | undefined): string | undefined;
10
48
  /** One open terminal connection (WebSocket JSON frames). */
11
49
  export interface TerminalConnection {
12
50
  /** Fired on the ready frame (shell is up); carries the host session id and alias. */
@@ -100,3 +138,4 @@ export declare class SshApi {
100
138
  /** One terminal socket over either an alias (open) or a session id (attach). */
101
139
  private terminalSocket;
102
140
  }
141
+ export {};
@@ -89,6 +89,7 @@ export declare const zh: {
89
89
  readonly 'terminal.ready': "终端已连接({alias})";
90
90
  readonly 'terminal.exited': "终端已退出({alias})";
91
91
  readonly 'terminal.error': "终端错误:{error}";
92
+ readonly 'terminal.noWebSocket': "当前页面不支持 WebSocket(应用外壳没有可连接的本地主机地址),请改用浏览器打开的 Web 界面使用终端。";
92
93
  readonly 'terminal.auth.title': "二次身份验证 (2FA / 交互式认证)";
93
94
  readonly 'terminal.auth.submit': "提交验证码";
94
95
  readonly 'terminal.auth.cancel': "取消";
@@ -11,6 +11,8 @@ interface ClusterOptions {
11
11
  tags?: string[];
12
12
  timeoutMs?: number;
13
13
  maxWorkers?: number;
14
+ /** Caller cancellation, forwarded to every host's command. */
15
+ signal?: AbortSignal;
14
16
  }
15
17
  /** Run one command against many hosts concurrently. */
16
18
  export declare function cluster(engine: PoolEngine, options: ClusterOptions): Promise<ClusterResult[]>;
@@ -107,6 +107,6 @@ export declare function sweepPool(engine: PoolEngine): void;
107
107
  * Run `fn` with a live client for `alias`, reconnecting (up to the
108
108
  * attempt budget) when the connection broke mid-flight.
109
109
  */
110
- export declare function withClient<T>(engine: PoolEngine, alias: string, fn: (client: Client) => Promise<T>, attempts?: number): Promise<T>;
110
+ export declare function withClient<T>(engine: PoolEngine, alias: string, fn: (client: Client) => Promise<T>, attempts?: number, signal?: AbortSignal): Promise<T>;
111
111
  /** Run one command on `alias` (reusing the pooled connection). */
112
- export declare function execCommand(engine: PoolEngine, alias: string, command: string, timeoutMs?: number): Promise<ExecResult>;
112
+ export declare function execCommand(engine: PoolEngine, alias: string, command: string, timeoutMs?: number, signal?: AbortSignal): Promise<ExecResult>;
@@ -36,7 +36,7 @@ export declare class SshEngine {
36
36
  /** One host summary by alias. */
37
37
  find(alias: string): SshHostSummary | undefined;
38
38
  /** Run one command on `alias` (reusing the pooled connection). */
39
- exec(alias: string, command: string, timeoutMs?: number): Promise<ExecResult>;
39
+ exec(alias: string, command: string, timeoutMs?: number, signal?: AbortSignal): Promise<ExecResult>;
40
40
  /** Run one command against many hosts concurrently. */
41
41
  cluster(options: {
42
42
  command: string;
@@ -45,6 +45,7 @@ export declare class SshEngine {
45
45
  tags?: string[];
46
46
  timeoutMs?: number;
47
47
  maxWorkers?: number;
48
+ signal?: AbortSignal;
48
49
  }): Promise<ClusterResult[]>;
49
50
  /** Open a PTY shell session for the web terminal (standalone connection). */
50
51
  openShell(alias: string, size: {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Family-shared tool-surface conventions for the packages that register
3
+ * model-facing tools.
4
+ *
5
+ * One home for the prompt-section placement and the visibility gate this
6
+ * family previously restated per package.
7
+ *
8
+ * Modelled on the official tool packages:
9
+ * - `@deepseek-ai/dsh-tool-subagent` renders its section through
10
+ * `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
11
+ * disappears together with a restricted tool.
12
+ * - 21 of the 22 official tool packages register their tools GLOBALLY and gate
13
+ * them with a setting, not per agent. Only the Agent Teams tool package —
14
+ * whose tools belong to one membership — installs into each `agent.ctx`.
15
+ * This family follows the majority: registration stays global so the
16
+ * registry's per-scope `restrict()` mask (which filters inherited global
17
+ * tools and REJECTS scope-local names) can still withhold these tools.
18
+ *
19
+ * Structural types only: this copy lands in packages that do not all depend on
20
+ * the same SDK service packages.
21
+ */
22
+ /**
23
+ * Prompt-section order for each package's tool guidance.
24
+ *
25
+ * These sit in the family band between the deployment persona and the SDK's
26
+ * `PLAN_POLICY` (500). Values are owned here rather than written per package so
27
+ * the host plugins whose guidance shares one prompt cannot drift into an
28
+ * accidental tie.
29
+ */
30
+ export declare const PLUGIN_TOOL_SECTION_ORDERS: {
31
+ readonly ssh: 150;
32
+ readonly 'task-board': 200;
33
+ };
34
+ /** One plugin id keyed by {@link PLUGIN_TOOL_SECTION_ORDERS}. */
35
+ export type PluginToolSectionId = keyof typeof PLUGIN_TOOL_SECTION_ORDERS;
36
+ /**
37
+ * Order of an external provider extension's own guidance section, right after
38
+ * the band of the plugin it extends.
39
+ *
40
+ * An extension names itself, not the plugin it plugs into, so its order stays
41
+ * out of {@link PLUGIN_TOOL_SECTION_ORDERS}: every package's copy of this module
42
+ * then carries no provider id. The slot still has one owner and cannot collide
43
+ * with a host plugin's band.
44
+ */
45
+ export declare const EXTENSION_TOOL_SECTION_ORDER = 210;
46
+ /**
47
+ * The scoped tool lookup a section provider needs: the registry read for one
48
+ * assembly's scope. Matches `ctx.tools.get(name, scope)`.
49
+ */
50
+ export interface ScopedToolLookup {
51
+ get(name: string, scope?: unknown): unknown;
52
+ }
53
+ /** One assembly context carrying the scope the section is rendered for. */
54
+ export interface SectionScopeContext {
55
+ scope?: unknown;
56
+ }
57
+ /**
58
+ * Build a section provider that renders \`text\` only while at least one of
59
+ * \`toolNames\` is visible to the rendering scope.
60
+ *
61
+ * The native convention: a tool that a restriction, a preset, or a missing
62
+ * registry removed must not leave guidance behind telling the model to call it.
63
+ * An unreadable registry renders the text unchanged — guidance that outlives a
64
+ * momentary read failure is recoverable; guidance for a tool that is gone is
65
+ * not.
66
+ * @param tools - the scoped tool registry, when the deployment serves one.
67
+ * @param toolNames - registered names the guidance describes.
68
+ * @param text - the guidance rendered while they are reachable.
69
+ * @returns the section text provider.
70
+ */
71
+ export declare function visibleToolText(tools: ScopedToolLookup | undefined, toolNames: readonly string[], text: string): (context: SectionScopeContext) => string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-ssh",
3
3
  "description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.4.3",
4
+ "version": "0.4.5",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "engines": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dsh": {
29
29
  "engines": {
30
- "dsh": ">=0.1.7-rc.2"
30
+ "dsh": ">=0.2.0-rc.2"
31
31
  },
32
32
  "bundle": {
33
33
  "patch": "./cordis.patch.yml"
@@ -50,26 +50,26 @@
50
50
  "ws": "^8.18.0"
51
51
  },
52
52
  "peerDependencies": {
53
- "@deepseek-ai/dsh": ">=0.1.7-rc.2",
53
+ "@deepseek-ai/dsh": ">=0.2.0-rc.2",
54
54
  "react": "^18.2.0",
55
55
  "react-dom": "^18.2.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@deepseek-ai/cordis": "^4.0.4",
59
- "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
60
- "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
61
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
62
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.7-rc.2",
63
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
64
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-rc.2",
65
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
66
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
67
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
68
- "@deepseek-ai/dsh-scope": "^0.1.7-rc.2",
69
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
70
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.2",
71
- "@deepseek-ai/dsh-timeout": "^0.1.7-rc.2",
72
- "@deepseek-ai/dsh-tools": "^0.1.7-rc.2",
59
+ "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
60
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
61
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
62
+ "@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.2",
63
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
65
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
66
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
67
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
68
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.2",
69
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
70
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
71
+ "@deepseek-ai/dsh-timeout": "^0.2.0-rc.2",
72
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
73
73
  "@types/node": "^22.20.0",
74
74
  "@types/react": "~18.3.1",
75
75
  "@types/react-dom": "^18.3.5",