pi-ssh-remote 0.1.12 → 0.1.13
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/CHANGELOG.md +18 -1
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/index.ts +37 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Rel
|
|
|
18
18
|
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [0.1.13] - 2026-09-18
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- None.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Increased the SSH handshake timeout to 30 seconds for slower remote connections.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Stored SSH config, known hosts, and server memory in Pi's agent directory (`~/.pi/agent`, or `PI_CODING_AGENT_DIR`) instead of falling back to the current working directory when `HOME` is unset, which mainly affected Windows.
|
|
34
|
+
- Expanded `ssh -i ~/...` identity paths with the user home directory from `os.homedir()`, so private keys resolve on Windows.
|
|
35
|
+
- Fell back to the remote login directory with a warning when a saved remote working directory is unavailable, instead of failing the SSH connection.
|
|
36
|
+
|
|
21
37
|
## [0.1.12] - 2026-08-25
|
|
22
38
|
|
|
23
39
|
### Added
|
|
@@ -220,7 +236,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Rel
|
|
|
220
236
|
|
|
221
237
|
- None.
|
|
222
238
|
|
|
223
|
-
[Unreleased]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.
|
|
239
|
+
[Unreleased]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.13...HEAD
|
|
240
|
+
[0.1.13]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.12...v0.1.13
|
|
224
241
|
[0.1.12]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.11...v0.1.12
|
|
225
242
|
[0.1.11]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.10...v0.1.11
|
|
226
243
|
[0.1.10]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.9...v0.1.10
|
package/README.md
CHANGED
|
@@ -234,13 +234,13 @@ The extension currently supports direct SSH commands with `-p`, `-l`, and `-i`.
|
|
|
234
234
|
|
|
235
235
|
## Releases
|
|
236
236
|
|
|
237
|
-
Latest release: [v0.1.
|
|
237
|
+
Latest release: [v0.1.13](https://github.com/petrichor20211/pi-ssh-remote/releases/tag/v0.1.13)
|
|
238
238
|
|
|
239
239
|
| Version | Date | Highlights |
|
|
240
240
|
|---|---|---|
|
|
241
|
+
| [0.1.13](CHANGELOG.md#0113---2026-09-18) | 2026-09-18 | Graceful working-directory fallback, longer SSH handshakes, and Windows path fixes |
|
|
241
242
|
| [0.1.12](CHANGELOG.md#0112---2026-08-25) | 2026-08-25 | Safe handling of repeated SSH errors when a connection is lost during handshake |
|
|
242
243
|
| [0.1.11](CHANGELOG.md#0111---2026-08-16) | 2026-08-16 | JSON memory entries, prompt-guided CRUD, and `/remote memory` listing |
|
|
243
|
-
| [0.1.10](CHANGELOG.md#0110---2026-08-16) | 2026-08-16 | Safe inspect-by-default and append-only agent updates for server memory |
|
|
244
244
|
|
|
245
245
|
See [CHANGELOG.md](CHANGELOG.md) for the complete release history, including additions, behavior changes, and bug fixes.
|
|
246
246
|
|
package/README.zh-CN.md
CHANGED
|
@@ -312,13 +312,13 @@ pi install npm:pi-ssh-remote
|
|
|
312
312
|
|
|
313
313
|
## 版本发布
|
|
314
314
|
|
|
315
|
-
最新版本:[v0.1.
|
|
315
|
+
最新版本:[v0.1.13](https://github.com/petrichor20211/pi-ssh-remote/releases/tag/v0.1.13)
|
|
316
316
|
|
|
317
317
|
| 版本 | 日期 | 主要内容 |
|
|
318
318
|
|---|---|---|
|
|
319
|
+
| [0.1.13](CHANGELOG.md#0113---2026-09-18) | 2026-09-18 | 工作目录不可用时平滑回退、延长 SSH 握手等待并修复 Windows 路径 |
|
|
319
320
|
| [0.1.12](CHANGELOG.md#0112---2026-08-25) | 2026-08-25 | 安全处理 SSH 握手期间断线所触发的连续错误 |
|
|
320
321
|
| [0.1.11](CHANGELOG.md#0111---2026-08-16) | 2026-08-16 | JSON 记忆条目、Prompt 引导增删查改与 `/remote memory` 展示命令 |
|
|
321
|
-
| [0.1.10](CHANGELOG.md#0110---2026-08-16) | 2026-08-16 | 默认安全读取且仅追加的服务器记忆 agent 更新逻辑 |
|
|
322
322
|
|
|
323
323
|
完整的新增内容、行为变更和 Bug 修复记录请查看 [CHANGELOG.md](CHANGELOG.md)。
|
|
324
324
|
|
package/index.ts
CHANGED
|
@@ -10,12 +10,12 @@ import ssh2, { type Client as SshClient, type ClientChannel, type ConnectConfig,
|
|
|
10
10
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
11
11
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { createServer, type Server, type Socket } from "node:net";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
13
|
+
import { homedir, tmpdir } from "node:os";
|
|
14
14
|
import { Type } from "typebox";
|
|
15
15
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import {
|
|
18
|
-
|
|
18
|
+
getAgentDir,
|
|
19
19
|
DEFAULT_MAX_BYTES,
|
|
20
20
|
DEFAULT_MAX_LINES,
|
|
21
21
|
createBashTool,
|
|
@@ -47,6 +47,7 @@ interface ParsedSsh {
|
|
|
47
47
|
interface RemoteState extends ParsedSsh {
|
|
48
48
|
client: SshClient;
|
|
49
49
|
cwd: string;
|
|
50
|
+
unavailableCwd?: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
interface CredentialCache {
|
|
@@ -106,7 +107,8 @@ interface SessionRemoteState {
|
|
|
106
107
|
forwards?: string[];
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
// Use Pi's agent dir so Windows follows USERPROFILE and PI_CODING_AGENT_DIR is honored.
|
|
111
|
+
const AGENT_DIR = getAgentDir();
|
|
110
112
|
const KNOWN_HOSTS_FILE = join(AGENT_DIR, "ssh-remote-known-hosts.json");
|
|
111
113
|
const REMOTE_CONFIG_FILE = join(AGENT_DIR, "ssh-remote-config.json");
|
|
112
114
|
const SERVER_MEMORY_DIR = join(AGENT_DIR, "ssh-remote-memories");
|
|
@@ -120,6 +122,7 @@ const DEFAULT_EXEC_MAX_BYTES = 8 * 1024;
|
|
|
120
122
|
const DEFAULT_TURN_MAX_BYTES = 32 * 1024;
|
|
121
123
|
const MIN_MODEL_OUTPUT_BYTES = 1024;
|
|
122
124
|
const OUTPUT_FOOTER_RESERVE_BYTES = 512;
|
|
125
|
+
const SSH_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
123
126
|
const DEFAULT_REMOTE_TIMEOUT_SECONDS = 30;
|
|
124
127
|
const MAX_REMOTE_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
|
|
125
128
|
const MAX_PRIVATE_KEY_BYTES = 1024 * 1024;
|
|
@@ -147,6 +150,10 @@ function shellWords(input: string): string[] {
|
|
|
147
150
|
return words;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function isHomeRelativePath(filePath: string): boolean {
|
|
154
|
+
return filePath === "~" || filePath.startsWith("~/");
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
function parseSshCommand(command: string): ParsedSsh {
|
|
151
158
|
const args = shellWords(command);
|
|
152
159
|
if (args[0] !== "ssh") throw new Error("Command must start with ssh, for example: ssh root@host -p 22");
|
|
@@ -175,7 +182,7 @@ function parseSshCommand(command: string): ParsedSsh {
|
|
|
175
182
|
else throw new Error("Unexpected extra argument in SSH command");
|
|
176
183
|
}
|
|
177
184
|
if (!target || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid SSH host or port");
|
|
178
|
-
if (identityFile &&
|
|
185
|
+
if (identityFile && !isHomeRelativePath(identityFile) && !isAbsolute(identityFile)) {
|
|
179
186
|
throw new Error("SSH identity file must use an absolute path or ~/...");
|
|
180
187
|
}
|
|
181
188
|
const at = target.lastIndexOf("@");
|
|
@@ -207,10 +214,9 @@ function deleteCachedPassword(config: ParsedSsh): void {
|
|
|
207
214
|
|
|
208
215
|
function resolveIdentityPath(config: ParsedSsh): string {
|
|
209
216
|
if (!config.identityFile) throw new Error("No SSH identity file is configured");
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return config.identityFile === "~" ? home : join(home, config.identityFile.slice(2));
|
|
217
|
+
// Expand ~ with the user home directory, not Pi's agent directory.
|
|
218
|
+
if (isHomeRelativePath(config.identityFile)) {
|
|
219
|
+
return config.identityFile === "~" ? homedir() : join(homedir(), config.identityFile.slice(2));
|
|
214
220
|
}
|
|
215
221
|
return config.identityFile;
|
|
216
222
|
}
|
|
@@ -690,7 +696,7 @@ function probeFingerprint(config: ParsedSsh): Promise<string> {
|
|
|
690
696
|
let settled = false;
|
|
691
697
|
const timer = setTimeout(() => {
|
|
692
698
|
if (!settled) { settled = true; client.end(); reject(new Error("Connection timed out")); }
|
|
693
|
-
},
|
|
699
|
+
}, SSH_HANDSHAKE_TIMEOUT_MS + 2000);
|
|
694
700
|
client.on("error", (error) => {
|
|
695
701
|
if (!settled) { settled = true; clearTimeout(timer); reject(error); }
|
|
696
702
|
});
|
|
@@ -698,7 +704,7 @@ function probeFingerprint(config: ParsedSsh): Promise<string> {
|
|
|
698
704
|
host: config.host,
|
|
699
705
|
port: config.port,
|
|
700
706
|
username: config.username,
|
|
701
|
-
readyTimeout:
|
|
707
|
+
readyTimeout: SSH_HANDSHAKE_TIMEOUT_MS,
|
|
702
708
|
hostHash: "sha256",
|
|
703
709
|
hostVerifier: (hash) => {
|
|
704
710
|
if (!settled) { settled = true; clearTimeout(timer); resolve(hash); }
|
|
@@ -719,7 +725,7 @@ function connect(config: ParsedSsh, authentication: SshAuthentication, fingerpri
|
|
|
719
725
|
port: config.port,
|
|
720
726
|
username: config.username,
|
|
721
727
|
...authentication,
|
|
722
|
-
readyTimeout:
|
|
728
|
+
readyTimeout: SSH_HANDSHAKE_TIMEOUT_MS,
|
|
723
729
|
keepaliveInterval: 15000,
|
|
724
730
|
keepaliveCountMax: 3,
|
|
725
731
|
hostHash: "sha256",
|
|
@@ -1038,8 +1044,16 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1038
1044
|
const client = await connect(parsed, authentication, fingerprint);
|
|
1039
1045
|
try {
|
|
1040
1046
|
const cdCommand = cwd === FALLBACK_REMOTE_CWD ? "cd -- ~" : `cd -- ${quote(cwd)}`;
|
|
1041
|
-
|
|
1042
|
-
|
|
1047
|
+
let resolved: string;
|
|
1048
|
+
let unavailableCwd: string | undefined;
|
|
1049
|
+
try {
|
|
1050
|
+
resolved = (await execRemote(client, `${cdCommand} && pwd -P`)).toString().trim();
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
if (cwd === FALLBACK_REMOTE_CWD) throw error;
|
|
1053
|
+
unavailableCwd = cwd;
|
|
1054
|
+
resolved = (await execRemote(client, "cd -- ~ && pwd -P")).toString().trim();
|
|
1055
|
+
}
|
|
1056
|
+
const state = { ...parsed, client, cwd: resolved, ...(unavailableCwd ? { unavailableCwd } : {}) };
|
|
1043
1057
|
attachClient(state);
|
|
1044
1058
|
return state;
|
|
1045
1059
|
} catch (error) {
|
|
@@ -1067,7 +1081,11 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1067
1081
|
oldClient?.end();
|
|
1068
1082
|
if (currentCtx) {
|
|
1069
1083
|
status(currentCtx);
|
|
1070
|
-
|
|
1084
|
+
if (next.unavailableCwd) {
|
|
1085
|
+
currentCtx.ui.notify(`SSH remote directory unavailable: ${next.unavailableCwd}; reconnected in default directory ${next.cwd}`, "warning");
|
|
1086
|
+
} else {
|
|
1087
|
+
currentCtx.ui.notify(`SSH remote reconnected automatically: ${endpointDisplayLabel(next)}:${next.cwd}`, "info");
|
|
1088
|
+
}
|
|
1071
1089
|
}
|
|
1072
1090
|
return next;
|
|
1073
1091
|
})().finally(() => { reconnectPromise = null; });
|
|
@@ -1168,7 +1186,11 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1168
1186
|
saveEndpointConfig(command, { remoteCwd: next.cwd }, true);
|
|
1169
1187
|
status(ctx);
|
|
1170
1188
|
persistSessionRemoteState();
|
|
1171
|
-
|
|
1189
|
+
if (next.unavailableCwd) {
|
|
1190
|
+
ctx.ui.notify(`SSH remote directory unavailable: ${next.unavailableCwd}; connected in default directory ${next.cwd}`, "warning");
|
|
1191
|
+
} else {
|
|
1192
|
+
ctx.ui.notify(`SSH remote connected: ${endpointDisplayLabel(next)}:${next.cwd}`, "info");
|
|
1193
|
+
}
|
|
1172
1194
|
return next;
|
|
1173
1195
|
} catch (error) {
|
|
1174
1196
|
if (!parsed.identityFile) deleteCachedPassword(parsed);
|