dsh-remote 0.5.4 → 0.5.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/README.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  # dsh-remote
6
6
 
7
+ [![npm version](https://img.shields.io/npm/v/dsh-remote)](https://www.npmjs.com/package/dsh-remote)
8
+ [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
+ [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a3ef3)](https://github.com/topics/dsh-plugin)
10
+
7
11
  **Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
8
12
 
9
13
  Manage several SSH machines, then pick a **remote workspace** (or a **local** one) and let the agent operate right there without leaving the harness — listing files, reading code, running builds & commands over the remote host, and keeping that remote directory mirrored into a real local workspace object.
@@ -14,7 +18,7 @@ The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.
14
18
 
15
19
  Settings → **远程工作区** — a multi-machine SSH registry (add / edit / delete / set-current, password stored locally):
16
20
 
17
- <img src="https://raw.githubusercontent.com/flymysql/dsh-remote/main/docs/ui-settings.svg" alt="dsh-remote settings — multi-machine registry" width="720"/>
21
+ <img src="https://raw.githubusercontent.com/flymysql/dsh-remote/main/docs/ui-settings-panel.png" alt="dsh-remote settings — multi-machine registry (light theme, host scrubbed)" width="720"/>
18
22
 
19
23
  The native **"Add workspace" / "Select workspace"** flow — a centered modal, two tabs, opens on **本机 (local)**; switch to **远程 (remote)**:
20
24
 
@@ -122,4 +126,8 @@ Giving the plugin a machine's credentials lets the agent run **shell commands as
122
126
 
123
127
  ## License
124
128
 
125
- MIT
129
+ MIT
130
+
131
+ ## Changelog
132
+
133
+ See [CHANGELOG.md](./CHANGELOG.md).
package/README.zh.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  # dsh-remote
6
6
 
7
+ [![npm version](https://img.shields.io/npm/v/dsh-remote)](https://www.npmjs.com/package/dsh-remote)
8
+ [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
+ [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a)](https://github.com/topics/dsh-plugin)
10
+
7
11
  **为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的远程工作助手。**
8
12
 
9
13
  维护多台 SSH 机器,然后在「选择工作区」时选一个**远程工作区**(或**本地工作区**),Agent 就能在不离开 harness 的情况下直接操作——列文件、读代码、在远程主机上跑构建/命令,并把远程目录镜像成一个真实的本地工作区对象。
@@ -14,7 +18,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
14
18
 
15
19
  设置 → **远程工作区** —— 多机 SSH 列表(增/删/改/设为当前,密码本地保存、不回显):
16
20
 
17
- <img src="https://raw.githubusercontent.com/flymysql/dsh-remote/main/docs/ui-settings.svg" alt="dsh-remote 设置页 — 多机列表" width="720"/>
21
+ <img src="https://raw.githubusercontent.com/flymysql/dsh-remote/main/docs/ui-settings-panel.png" alt="dsh-remote 设置页 — 多机列表(浅色主题,主机已打码)" width="720"/>
18
22
 
19
23
  原生 **「Add workspace / 选择工作区」** 流程 —— **居中弹窗**、两个 tab,**默认落在「本机」**;切到 **「远程」**:
20
24
 
@@ -123,4 +127,8 @@ npx --yes @deepseek-ai/dsh plugin --profile web remove dsh-remote # 恢复用
123
127
 
124
128
  ## License
125
129
 
126
- MIT
130
+ MIT
131
+
132
+ ## 变更记录
133
+
134
+ 见 [CHANGELOG.md](./CHANGELOG.md)。
package/lib/index.js CHANGED
@@ -41,7 +41,7 @@ export const Config = z.object({
41
41
  username: z.string().default(''),
42
42
  /** Password login (only when the remote has no key. Override the fallback below). */
43
43
  password: z.string().default(''),
44
- /** Absolute private-key path; empty → ~/.ssh/id_rsa. */
44
+ /** Explicit SSH private-key path (optional; only used when supplied). Never auto-reads ~/.ssh. */
45
45
  privateKeyPath: z.string().default(''),
46
46
  /** Key passphrase when the key is encrypted. */
47
47
  passphrase: z.string().default(''),
@@ -236,7 +236,7 @@ class SshPool {
236
236
 
237
237
  resolveKeyPath() {
238
238
  const p = this.config.privateKeyPath
239
- if (!p) return path.join(homedir(), '.ssh', 'id_rsa')
239
+ if (!p) return ''
240
240
  if (p.startsWith('~/') || p === '~') return path.join(homedir(), p.slice(1))
241
241
  return p
242
242
  }
@@ -296,13 +296,15 @@ class SshPool {
296
296
  if (this.config.password) {
297
297
  opts.password = this.config.password
298
298
  } else {
299
+ const keyPath = this.resolveKeyPath()
300
+ if (!keyPath) {
301
+ return fail(new Error('no credentials: set a password or a privateKeyPath to connect'))
302
+ }
299
303
  let key
300
304
  try {
301
- key = readFileSync(this.resolveKeyPath())
305
+ key = readFileSync(keyPath)
302
306
  } catch (err) {
303
- return fail(
304
- new Error(`cannot read private key "${this.resolveKeyPath()}": ${err && err.message}`),
305
- )
307
+ return fail(new Error(`cannot read private key "${keyPath}": ${err && err.message}`))
306
308
  }
307
309
  opts.privateKey = key
308
310
  opts.passphrase = this.config.passphrase || undefined
@@ -496,7 +498,7 @@ export async function apply(ctx, config) {
496
498
  'Show the remote environment: host/user/port, connection health, and the current remote workspace path. Call this first to orient, or when a remote_* call fails to check connectivity.',
497
499
  parameters: {},
498
500
  output: {
499
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
501
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
500
502
  render: (_a, v) => [{ type: 'text', text: v.text }],
501
503
  },
502
504
  async execute() {
@@ -528,14 +530,15 @@ export async function apply(ctx, config) {
528
530
  description:
529
531
  'Connect SSH to a remote host for remote workspace work. Provide host (required), user, optional password or privateKeyPath/port. Once connected, call rw_pick_workspace to pick the workspace directory this session should work in.',
530
532
  parameters: {
531
- host: { type: 'string', required: true, description: 'Remote host IP or hostname' },
533
+ required: ['host'],
534
+ host: { type: 'string', description: 'Remote host IP or hostname' },
532
535
  username: { type: 'string', description: 'SSH user (default from config or root)' },
533
536
  port: { type: 'integer', description: 'SSH port (default 22)' },
534
537
  password: { type: 'string', description: 'SSH password (prefer SSH key when possible)' },
535
538
  privateKeyPath: { type: 'string', description: 'Absolute private-key path' },
536
539
  },
537
540
  output: {
538
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
541
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
539
542
  render: (_a, a) => [{ type: 'text', text: a.text }],
540
543
  },
541
544
  async execute(args) {
@@ -563,10 +566,11 @@ export async function apply(ctx, config) {
563
566
  description:
564
567
  'Set the remote workspace directory this session should treat as its working root on the connected remote. Verifies it exists (a directory). Use rw_list_dir to browse first if unsure.',
565
568
  parameters: {
566
- path: { type: 'string', required: true, description: 'Absolute remote directory path, e.g. /home/dev/code/project' },
569
+ required: ['path'],
570
+ path: { type: 'string', description: 'Absolute remote directory path, e.g. /home/dev/code/project' },
567
571
  },
568
572
  output: {
569
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
573
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
570
574
  render: (_a, a) => [{ type: 'text', text: a.text }],
571
575
  },
572
576
  async execute(args) {
@@ -592,7 +596,7 @@ export async function apply(ctx, config) {
592
596
  maxFiles: { type: 'integer', description: 'Max files to download (default 500)' },
593
597
  },
594
598
  output: {
595
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
599
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
596
600
  render: (_a, a) => [{ type: 'text', text: a.text }],
597
601
  },
598
602
  async execute(args) {
@@ -621,7 +625,7 @@ export async function apply(ctx, config) {
621
625
  maxFiles: { type: 'integer', description: 'Max files to upload (default 500)' },
622
626
  },
623
627
  output: {
624
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
628
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
625
629
  render: (_a, a) => [{ type: 'text', text: a.text }],
626
630
  },
627
631
  async execute(args) {
@@ -649,7 +653,7 @@ export async function apply(ctx, config) {
649
653
  path: { type: 'string', description: 'Absolute remote path (default: current remote workspace)' },
650
654
  },
651
655
  output: {
652
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
656
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
653
657
  render: (_a, a) => [{ type: 'text', text: a.text }],
654
658
  },
655
659
  async execute(args) {
@@ -664,13 +668,14 @@ export async function apply(ctx, config) {
664
668
  description:
665
669
  'Read a text file on the remote host with line numbers. Supports paging with startLine/endLine. Path is absolute.',
666
670
  parameters: {
667
- path: { type: 'string', required: true, description: 'Absolute remote file path' },
671
+ required: ['path'],
672
+ path: { type: 'string', description: 'Absolute remote file path' },
668
673
  startLine: { type: 'integer', description: '1-based first line (default 1)' },
669
674
  endLine: { type: 'integer', description: '1-based last line (inclusive)' },
670
675
  maxLines: { type: 'integer', description: 'Max lines (default 2000)' },
671
676
  },
672
677
  output: {
673
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
678
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
674
679
  render: (_a, a) => [{ type: 'text', text: a.text }],
675
680
  },
676
681
  async execute(args) {
@@ -693,10 +698,11 @@ export async function apply(ctx, config) {
693
698
  description:
694
699
  'Run a shell command on the remote host. Use for anything that is not reading a file (build, test, grep, etc). Output is capped.',
695
700
  parameters: {
696
- command: { type: 'string', required: true, description: 'Shell command (run on the remote host)' },
701
+ required: ['command'],
702
+ command: { type: 'string', description: 'Shell command (run on the remote host)' },
697
703
  },
698
704
  output: {
699
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', required: true } } },
705
+ schema: { type: 'object', additionalProperties: false, required: ['text'], properties: { text: { type: 'string' } } },
700
706
  render: (_a, a) => [{ type: 'text', text: a.text }],
701
707
  },
702
708
  async execute(args) {
@@ -711,12 +717,13 @@ export async function apply(ctx, config) {
711
717
  description:
712
718
  'Write text to a file on the remote host (creating parent directories if needed). Path is absolute. Use this to create or overwrite a remote file directly, instead of round-tripping through a local mirror.',
713
719
  parameters: {
714
- path: { type: 'string', required: true, description: 'Absolute remote file path' },
715
- content: { type: 'string', required: true, description: 'File content to write (overwrites existing file)' },
720
+ required: ['path', 'content'],
721
+ path: { type: 'string', description: 'Absolute remote file path' },
722
+ content: { type: 'string', description: 'File content to write (overwrites existing file)' },
716
723
  mkdir: { type: 'boolean', description: 'Create missing parent directories (default true)' },
717
724
  },
718
725
  output: {
719
- schema: { type: 'object', additionalProperties: false, properties: { ok: { type: 'boolean', required: true }, bytes: { type: 'integer' }, text: { type: 'string' } } },
726
+ schema: { type: 'object', additionalProperties: false, required: ['ok'], properties: { ok: { type: 'boolean' }, bytes: { type: 'integer' }, text: { type: 'string' } } },
720
727
  render: (_a, a) => [{ type: 'text', text: a.text || (a.ok ? 'written' : 'failed') }],
721
728
  },
722
729
  async execute(args) {
@@ -754,7 +761,7 @@ export async function apply(ctx, config) {
754
761
  'Close the current SSH connection to the remote host, releasing the persistent pool. Useful to rotate connections or after a long idle.',
755
762
  parameters: {},
756
763
  output: {
757
- schema: { type: 'object', additionalProperties: false, properties: { ok: { type: 'boolean', required: true }, text: { type: 'string' } } },
764
+ schema: { type: 'object', additionalProperties: false, required: ['ok'], properties: { ok: { type: 'boolean' }, text: { type: 'string' } } },
758
765
  render: (_a, a) => [{ type: 'text', text: a.text }],
759
766
  },
760
767
  async execute() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password or key), pick a remote workspace, operate on it with rw_pick_workspace / rw_list_dir / rw_read_file / rw_exec / rw_sync tools, and mirror it to a real local directory (SFTP) so the DSH native workspace can adopt it.",
5
5
  "license": "MIT",
6
6
  "author": "flymysql <flyphp@outlook.com>",