dsh-ssh-manage 0.3.7 → 0.3.8

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/README.en.md CHANGED
@@ -23,6 +23,7 @@
23
23
  `dsh-ssh-manage` is a Web plugin for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness). It turns the hosts in `~/.ssh/config` into manageable objects in the DSH sidebar:
24
24
 
25
25
  - **Host management** — scan the config, groups, tags, favorites, notes, latency and host inspection; SSH aliases are globally unique, so use the repeatable Display name for same names across groups;
26
+ - **DSH Agent integration** — the host catalog is injected into the DSH system prompt so the Agent can use `ssh_manager` directly for exec, batch commands, inspection and SFTP on recorded servers;
26
27
  - **Interactive terminal** — ssh2 allocates a PTY, xterm.js renders it, with colors, arrow keys, full-screen programs such as `vim`/`top`, and window resizing;
27
28
  - **Backup and migration** — export/import hosts, groups, tags, notes, favorites and passwords from Settings; exports are re-encrypted with a passphrase (scrypt + AES-256-GCM) and imports merge by alias;
28
29
  - **Day-to-day operations** — batch commands, SFTP browse/mkdir/rename/delete/upload/download, connection tests;
@@ -97,16 +98,16 @@ Creating a host requires alias, hostname and password. The password is not writt
97
98
 
98
99
  ### From npm (recommended)
99
100
 
100
- npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.7`).
101
+ npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.8`).
101
102
 
102
103
  ```text
103
- plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.7
104
+ plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.8
104
105
  ```
105
106
 
106
107
  Or with the DSH CLI:
107
108
 
108
109
  ```bash
109
- dsh plugin add dsh-ssh-manage@0.3.7
110
+ dsh plugin add dsh-ssh-manage@0.3.8
110
111
  ```
111
112
 
112
113
  After installation, refresh the Web page; the sidebar shows an "SSH Hosts" entry with id `dsh-ssh-manage`.
package/README.md CHANGED
@@ -23,6 +23,7 @@
23
23
  `dsh-ssh-manage` 是 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 的 Web 插件。它把 `~/.ssh/config` 里的主机变成 DSH 侧边栏里可管理、可连接、可运维的对象:
24
24
 
25
25
  - **主机管理**:扫描配置、分组、标签、收藏、备注、连接延迟、主机巡检;SSH 别名全局唯一,不同分组如需同名请使用可重复的“显示名称”;
26
+ - **DSH Agent 集成**:主机目录会注入 DSH 系统提示,Agent 可以直接用 `ssh_manager` 对已录入的服务器执行命令、批量命令、巡检和 SFTP 操作;
26
27
  - **交互式终端**:ssh2 分配 PTY,xterm.js 渲染,支持彩色输出、方向键、`vim`/`top` 等全屏程序和窗口自适应;
27
28
  - **备份迁移**:设置页可导出/导入主机、分组、标签、备注、收藏和密码;导出文件用自定义口令二次加密(scrypt + AES-256-GCM),导入按别名合并;
28
29
  - **日常运维**:批量命令、SFTP 目录浏览 / 新建 / 重命名 / 删除 / 上传 / 下载、测试连接;
@@ -99,16 +100,16 @@
99
100
 
100
101
  ### 从 npm 安装(推荐)
101
102
 
102
- npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.7`)。
103
+ npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.8`)。
103
104
 
104
105
  ```text
105
- plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.7
106
+ plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.8
106
107
  ```
107
108
 
108
109
  或使用 DSH CLI:
109
110
 
110
111
  ```bash
111
- dsh plugin add dsh-ssh-manage@0.3.7
112
+ dsh plugin add dsh-ssh-manage@0.3.8
112
113
  ```
113
114
 
114
115
  安装后刷新 Web 页面,左侧栏会出现「SSH 主机」入口,入口 id 为 `dsh-ssh-manage`。
package/index.js CHANGED
@@ -78,6 +78,54 @@ function errorMessage(error) {
78
78
  return error instanceof Error ? error.message : String(error)
79
79
  }
80
80
 
81
+ let sshHostCatalogText = ''
82
+ let sshHostCatalogRefresh = null
83
+
84
+ function sshCatalogLine(host, metadata, passwordSet) {
85
+ const user = host.user ? `${host.user}@` : ''
86
+ const address = `${host.hostname || host.host}${host.port ? `:${host.port}` : ''}`
87
+ const group = String(metadata?.group || '').replace(/[\r\n]+/g, ' ').trim() || 'ungrouped'
88
+ const label = String(metadata?.label || '').replace(/[\r\n]+/g, ' ').trim()
89
+ const tags = Array.isArray(metadata?.tags) ? metadata.tags.map((tag) => String(tag).replace(/[\r\n]+/g, ' ')).filter(Boolean) : []
90
+ const parts = [
91
+ `alias=${host.host}`,
92
+ `target=${user}${address}`,
93
+ `group=${group}`,
94
+ ]
95
+ if (label && label !== host.host) parts.push(`display=${label}`)
96
+ if (tags.length) parts.push(`tags=${tags.slice(0, 4).join(',')}`)
97
+ parts.push(`password=${passwordSet.has(host.host) ? 'stored' : 'missing'}`)
98
+ return `- ${parts.join(' | ')}`
99
+ }
100
+
101
+ /** Refresh the host catalog injected into the DSH system prompt. */
102
+ export async function refreshSshHostCatalog(paths) {
103
+ if (sshHostCatalogRefresh) return sshHostCatalogRefresh
104
+ sshHostCatalogRefresh = (async () => {
105
+ try {
106
+ const [{ hosts }, metadata, passwords] = await Promise.all([
107
+ listSshHosts(paths),
108
+ loadMetadata(paths).catch(() => ({ entries: {} })),
109
+ listPasswordAliases(paths).catch(() => []),
110
+ ])
111
+ const passwordSet = new Set(passwords)
112
+ const lines = hosts.slice(0, 40).map((host) => sshCatalogLine(host, metadata.entries[host.host], passwordSet))
113
+ if (hosts.length > 40) lines.push(`- ... ${hosts.length - 40} more hosts; call list_hosts for the full list`)
114
+ sshHostCatalogText = [
115
+ 'SSH hosts managed by the dsh-ssh-manage plugin. When the user asks to inspect, run commands on, upload to or download from one of these servers, call the ssh_manager tool directly with the exact alias.',
116
+ 'Use action=exec with {host, command} for one command, batch_exec with {hosts, command} for several, and sftp_* actions for files. Use test_connection or inspect_host for status and system facts. list_hosts returns the full catalog.',
117
+ 'Passwords are never returned by the plugin; never ask the user to paste a password into the conversation.',
118
+ lines.length ? lines.join('\n') : '- No SSH hosts are configured yet; the user can add them in the dsh-ssh-manage sidebar panel.',
119
+ ].join('\n').slice(0, 6000)
120
+ } catch {
121
+ sshHostCatalogText = 'SSH host catalog is temporarily unavailable; call ssh_manager action=list_hosts to read it.'
122
+ } finally {
123
+ sshHostCatalogRefresh = null
124
+ }
125
+ })()
126
+ return sshHostCatalogRefresh
127
+ }
128
+
81
129
  async function state(paths) {
82
130
  const [current, metadataResult, passwordsResult] = await Promise.all([
83
131
  listSshHosts(paths),
@@ -93,6 +141,7 @@ async function state(paths) {
93
141
  else passwords = passwordsResult.value
94
142
  const passwordSet = new Set(passwords)
95
143
  const stateDir = storagePaths(paths).stateDir
144
+ void refreshSshHostCatalog(paths)
96
145
  return {
97
146
  ...current,
98
147
  stateDir,
@@ -253,7 +302,7 @@ export async function runAction(paths, action, params = {}) {
253
302
  function managerTool(paths) {
254
303
  return {
255
304
  name: 'ssh_manager',
256
- description: 'Manage SSH hosts, encrypted local passwords, remote commands and SFTP operations. Passwords are never returned.',
305
+ description: 'Manage SSH hosts, encrypted local passwords, remote commands and SFTP operations. Hosts entered in the dsh-ssh-manage panel are listed in the system prompt; when the user asks to operate on a server, call this tool directly with its alias. Use action=exec for one command, batch_exec for several hosts, sftp_* for files. Passwords are never returned.',
257
306
  parameters: {
258
307
  type: 'object',
259
308
  additionalProperties: false,
@@ -262,7 +311,7 @@ function managerTool(paths) {
262
311
  action: { type: 'string', enum: ACTIONS, description: 'The operation to run.' },
263
312
  host: {
264
313
  oneOf: [{ type: 'string' }, { type: 'object' }],
265
- description: 'Host alias as a string for every action except save_host, which takes a host object: { host, hostname, user, port, identityFile, proxyJump, forwardAgent }.',
314
+ description: 'Host alias as a string (see the SSH host catalog in the system prompt) for every action except save_host, which takes a host object: { host, hostname, user, port, identityFile, proxyJump, forwardAgent }.',
266
315
  },
267
316
  originalHost: { type: 'string', description: 'Existing alias when renaming a host.' },
268
317
  password: { type: 'string', description: 'Password to save for a host. It is write-only.' },
@@ -830,7 +879,7 @@ function registerAssetRoutes(ctx) {
830
879
  }
831
880
  }
832
881
 
833
- export const __test = { resolveSshPaths, listSshHosts, saveSshHost, deleteSshHost, runAction, managerTool, MAX_BODY_BYTES, PTY_ROUTE, PTY_EVENTS_ROUTE, PTY_OPEN_ROUTE, PTY_ATTACH_ROUTE, PTY_INPUT_ROUTE, PTY_RESIZE_ROUTE, PTY_CLOSE_ROUTE, PTY_CLEAR_ROUTE, PTY_TOUCH_ROUTE, PTY_IDLE_MS, sweepIdlePtySessions, MAX_PTY_SESSIONS, PTY_BUFFER_BYTES, ASSETS, handlePtyUpgrade, ptyTextSessions }
882
+ export const __test = { resolveSshPaths, listSshHosts, saveSshHost, deleteSshHost, runAction, managerTool, refreshSshHostCatalog, getSshHostCatalog: () => sshHostCatalogText, MAX_BODY_BYTES, PTY_ROUTE, PTY_EVENTS_ROUTE, PTY_OPEN_ROUTE, PTY_ATTACH_ROUTE, PTY_INPUT_ROUTE, PTY_RESIZE_ROUTE, PTY_CLOSE_ROUTE, PTY_CLEAR_ROUTE, PTY_TOUCH_ROUTE, PTY_IDLE_MS, sweepIdlePtySessions, MAX_PTY_SESSIONS, PTY_BUFFER_BYTES, ASSETS, handlePtyUpgrade, ptyTextSessions }
834
883
 
835
884
  export const inject = ['webServer']
836
885
 
@@ -839,7 +888,12 @@ export function apply(ctx) {
839
888
  throw new Error('dsh-ssh-manage requires DeepSeek Harness >=0.2.1-rc.3: ctx.webServer.register is unavailable.')
840
889
  }
841
890
  const paths = resolveSshPaths(process.env)
891
+ void refreshSshHostCatalog(paths)
842
892
  const runtime = createPtyRuntime()
893
+ const catalogTimer = setInterval(() => {
894
+ if (runtime.active) void refreshSshHostCatalog(paths)
895
+ }, 30_000)
896
+ catalogTimer.unref?.()
843
897
  const runtimeIdleTimer = setInterval(() => {
844
898
  if (!runtime.active) return
845
899
  // Browser heartbeats (/pty/touch) keep attached sessions alive, so a session
@@ -934,6 +988,7 @@ export function apply(ctx) {
934
988
  }
935
989
  runtime.upgrades.clear()
936
990
  clearInterval(runtimeIdleTimer)
991
+ clearInterval(catalogTimer)
937
992
  for (const session of ptySessions) {
938
993
  try { session.close() } catch {}
939
994
  }
@@ -950,4 +1005,15 @@ export function apply(ctx) {
950
1005
  ctx.inject(['tools'], (child) => {
951
1006
  child.effect(() => child.tools.register(managerTool(paths)), 'dsh-ssh-manage: agent tool')
952
1007
  })
1008
+ ctx.inject(['systemPrompt'], (child) => {
1009
+ if (!child.systemPrompt || typeof child.systemPrompt.context !== 'function') return
1010
+ child.effect(
1011
+ () => child.systemPrompt.context({
1012
+ name: 'dsh-ssh-manage-hosts',
1013
+ order: 60,
1014
+ text: () => sshHostCatalogText,
1015
+ }),
1016
+ 'dsh-ssh-manage: host catalog prompt context',
1017
+ )
1018
+ })
953
1019
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-manage",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "A DSH sidebar plugin for managing SSH host profiles with a real interactive PTY terminal.",
5
5
  "license": "MIT",
6
6
  "private": false,