@fleetagent/pi-coding-agent 0.0.12 → 0.1.1
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 +38 -0
- package/dist/core/tools/operations.d.ts +13 -1
- package/dist/core/tools/operations.d.ts.map +1 -1
- package/dist/core/tools/operations.js +176 -2
- package/dist/core/tools/operations.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +12 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +14 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +18 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +26 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/rpc.md +22 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.1](https://github.com/fleetagent/pi/compare/@fleetagent/pi-coding-agent-v0.1.0...@fleetagent/pi-coding-agent-v0.1.1) (2026-06-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **coding-agent:** add streamed sandbox file transfer ([4199641](https://github.com/fleetagent/pi/commit/41996414740a0a752c8eafac16f48d38ded76c78))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @fleetagent/pi-agent-core bumped from ^0.1.0 to ^0.1.1
|
|
16
|
+
* @fleetagent/pi-ai bumped from ^0.1.0 to ^0.1.1
|
|
17
|
+
* @fleetagent/pi-tui bumped from ^0.1.0 to ^0.1.1
|
|
18
|
+
|
|
19
|
+
## [Unreleased]
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added RPC `upload_file` and `download_file` commands for streaming files to and from the active sandbox backend.
|
|
24
|
+
|
|
25
|
+
## [0.1.0](https://github.com/fleetagent/pi/compare/@fleetagent/pi-coding-agent-v0.0.12...@fleetagent/pi-coding-agent-v0.1.0) (2026-06-12)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* **coding-agent:** add remote commander daemon ([92655ae](https://github.com/fleetagent/pi/commit/92655ae7d69d8868d98377cf8204a5d35a8cafed))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Dependencies
|
|
34
|
+
|
|
35
|
+
* The following workspace dependencies were updated
|
|
36
|
+
* dependencies
|
|
37
|
+
* @fleetagent/pi-agent-core bumped from ^0.0.12 to ^0.1.0
|
|
38
|
+
* @fleetagent/pi-ai bumped from ^0.0.12 to ^0.1.0
|
|
39
|
+
* @fleetagent/pi-tui bumped from ^0.0.12 to ^0.1.0
|
|
40
|
+
|
|
3
41
|
## [0.0.12](https://github.com/fleetagent/pi/compare/@fleetagent/pi-coding-agent-v0.0.11...@fleetagent/pi-coding-agent-v0.0.12) (2026-06-12)
|
|
4
42
|
|
|
5
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
2
|
export type ToolAccessMode = "exists" | "read" | "write" | "readwrite";
|
|
3
3
|
export interface ToolFileStat {
|
|
4
4
|
isDirectory: () => boolean;
|
|
@@ -59,6 +59,8 @@ export interface ToolOperations {
|
|
|
59
59
|
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
60
60
|
readFile(path: string): Promise<Buffer>;
|
|
61
61
|
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
62
|
+
uploadFile?(sourcePath: string, destinationPath: string): Promise<void>;
|
|
63
|
+
downloadFile?(sourcePath: string, destinationPath: string): Promise<void>;
|
|
62
64
|
mkdir(path: string, options?: {
|
|
63
65
|
recursive?: boolean;
|
|
64
66
|
}): Promise<void>;
|
|
@@ -95,6 +97,8 @@ export declare class LocalToolOperations implements ToolOperations {
|
|
|
95
97
|
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
96
98
|
readFile(path: string): Promise<Buffer>;
|
|
97
99
|
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
100
|
+
uploadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
101
|
+
downloadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
98
102
|
mkdir(path: string, options?: {
|
|
99
103
|
recursive?: boolean;
|
|
100
104
|
}): Promise<void>;
|
|
@@ -114,6 +118,8 @@ export declare class SshToolOperations implements ToolOperations {
|
|
|
114
118
|
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
115
119
|
readFile(path: string): Promise<Buffer>;
|
|
116
120
|
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
121
|
+
uploadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
122
|
+
downloadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
117
123
|
mkdir(path: string, options?: {
|
|
118
124
|
recursive?: boolean;
|
|
119
125
|
}): Promise<void>;
|
|
@@ -139,6 +145,8 @@ export declare class DeferredRemoteToolOperations implements ToolOperations {
|
|
|
139
145
|
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
140
146
|
readFile(path: string): Promise<Buffer>;
|
|
141
147
|
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
148
|
+
uploadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
149
|
+
downloadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
142
150
|
mkdir(path: string, options?: {
|
|
143
151
|
recursive?: boolean;
|
|
144
152
|
}): Promise<void>;
|
|
@@ -157,6 +165,7 @@ export declare class RemoteToolOperations implements ToolOperations {
|
|
|
157
165
|
private nextId;
|
|
158
166
|
private pending;
|
|
159
167
|
private execPending;
|
|
168
|
+
private fileDownloadPending;
|
|
160
169
|
private keepAliveInterval;
|
|
161
170
|
private lastPongAt;
|
|
162
171
|
private constructor();
|
|
@@ -168,12 +177,15 @@ export declare class RemoteToolOperations implements ToolOperations {
|
|
|
168
177
|
private request;
|
|
169
178
|
private handleMessage;
|
|
170
179
|
private handleExecEvent;
|
|
180
|
+
private handleFileEvent;
|
|
171
181
|
exec(command: string, options: ToolExecOptions): Promise<{
|
|
172
182
|
exitCode: number | null;
|
|
173
183
|
}>;
|
|
174
184
|
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
175
185
|
readFile(path: string): Promise<Buffer>;
|
|
176
186
|
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
187
|
+
uploadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
188
|
+
downloadFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
177
189
|
mkdir(path: string, options?: {
|
|
178
190
|
recursive?: boolean;
|
|
179
191
|
}): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/core/tools/operations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAoBrC,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,MAAM,EAAE,MAAM,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAElF,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACtF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjF,IAAI,CAAC,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACzD,mBAAmB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,eAAe,CAAC;IACnC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,+CAA+C;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAqHD,qBAAa,mBAAoB,YAAW,cAAc;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,SAAS,CAAqB;IAEtC,YAAY,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B,EAGhE;IAEK,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CA2D1F;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/D;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5C;IAEK,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;IAEK,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAEvC;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE7C;IAEK,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAE1E;IAED,cAAc,IAAI,eAAe,CAEhC;CACD;AAED,qBAAa,iBAAkB,YAAW,cAAc;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IAEZ,YAAY,OAAO,EAAE,wBAAwB,EAG5C;IAED,OAAa,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAKlE;IAEK,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAmC1F;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/D;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5C;IAEK,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIrE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAEK,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAU9C;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAM7C;IAEK,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIpF;IAEK,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAkC5D;IAEK,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAQ1E;IAED,cAAc,IAAI,eAAe,CAEhC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAG;CACjC;AAED,qBAAa,4BAA6B,YAAW,cAAc;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,UAAU,CAAuD;IAEzE,YAAY,GAAG,EAAE,MAAM,EAEtB;IAEK,SAAS,CAAC,OAAO,EAAE,+CAA+C,GAAG,OAAO,CAAC,eAAe,CAAC,CAUlG;IAEK,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAU3D;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,iBAAiB;IAOnB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAE1F;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/D;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5C;IAEK,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;IAEK,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAE9C;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE7C;IAEK,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEpF;IAEK,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAE5D;IAEK,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAE1E;IAED,cAAc,IAAI,eAAe,CAEhC;CACD;AAwCD,qBAAa,oBAAqB,YAAW,cAAc;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAMX;IACJ,OAAO,CAAC,WAAW,CAOf;IACJ,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,UAAU,CAAc;IAEhC,OAAO,eAeN;IAED,OAAa,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAyB/D;IAED,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,eAAe;IAmBjB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAwD1F;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/D;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5C;IAEK,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;IAEK,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAO9C;IAEK,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAK7C;IAEK,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKpF;IAEK,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAc5D;IAEK,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAK1E;IAED,cAAc,IAAI,eAAe,CAEhC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAG7B;CACD;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAErF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAElF","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { Stats } from \"node:fs\";\nimport { constants } from \"node:fs\";\nimport {\n\taccess as fsAccess,\n\tmkdir as fsMkdir,\n\treaddir as fsReaddir,\n\treadFile as fsReadFile,\n\tstat as fsStat,\n\twriteFile as fsWriteFile,\n} from \"node:fs/promises\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\n\nexport type ToolAccessMode = \"exists\" | \"read\" | \"write\" | \"readwrite\";\n\nexport interface ToolFileStat {\n\tisDirectory: () => boolean;\n\tisFile: () => boolean;\n}\n\nexport interface ToolExecOptions {\n\tcwd?: string;\n\tonData: (data: Buffer) => void;\n\tsignal?: AbortSignal;\n\ttimeout?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nexport interface ToolGlobOptions {\n\tignore: string[];\n\tlimit: number;\n}\n\nexport interface ToolGrepOptions {\n\tpattern: string;\n\tpath: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tlimit: number;\n}\n\nexport interface ToolGrepMatch {\n\tfilePath: string;\n\tlineNumber: number;\n\tlineText?: string;\n}\n\nexport interface ToolGrepResult {\n\tisDirectory: boolean;\n\tmatches: ToolGrepMatch[];\n}\n\nexport type ToolBackendInfo =\n\t| { type: \"local\"; cwd: string }\n\t| { type: \"ssh\"; cwd: string; remote: string; configured: true }\n\t| { type: \"remote\"; cwd: string; configured: false }\n\t| { type: \"remote\"; cwd: string; url: string; protocol: \"ws\"; configured: true };\n\nexport interface ToolOperations {\n\tcwd: string;\n\texec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }>;\n\taccess(path: string, mode?: ToolAccessMode): Promise<void>;\n\treadFile(path: string): Promise<Buffer>;\n\twriteFile(path: string, content: string | Buffer): Promise<void>;\n\tmkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n\tstat(path: string): Promise<ToolFileStat>;\n\treaddir(path: string): Promise<string[]>;\n\tglob?(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]>;\n\tgrep?(options: ToolGrepOptions): Promise<ToolGrepResult>;\n\tdetectImageMimeType?(path: string): Promise<string | null | undefined>;\n\tgetBackendInfo?(): ToolBackendInfo;\n\tdispose?(): Promise<void>;\n}\n\nexport interface LocalToolOperationsOptions {\n\tshellPath?: string;\n}\n\nexport interface SshToolOperationsOptions {\n\tremote: string;\n\tcwd: string;\n}\n\nexport interface DeferredRemoteToolOperationsConfigureSshOptions {\n\tremote: string;\n\tcwd?: string;\n}\n\nexport interface ParsedSshTarget {\n\tremote: string;\n\tcwd?: string;\n}\n\nfunction accessModeToFsMode(mode: ToolAccessMode | undefined): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction parseSshTarget(value: string): ParsedSshTarget {\n\tconst separatorIndex = value.indexOf(\":\");\n\tif (separatorIndex === -1) {\n\t\treturn { remote: value };\n\t}\n\tconst remote = value.slice(0, separatorIndex);\n\tconst cwd = value.slice(separatorIndex + 1);\n\treturn cwd ? { remote, cwd } : { remote };\n}\n\nfunction validateSshRemote(remote: string): void {\n\tif (!remote) {\n\t\tthrow new Error(\"--ssh requires a remote target like user@host or user@host:/path\");\n\t}\n\tif (remote.startsWith(\"-\")) {\n\t\tthrow new Error(\"--ssh remote target must not start with '-'\");\n\t}\n}\n\nfunction sshArgs(remote: string, command: string): string[] {\n\tvalidateSshRemote(remote);\n\treturn [\"--\", remote, command];\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(options: ToolGrepOptions): string[] {\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (options.ignoreCase) args.push(\"--ignore-case\");\n\tif (options.literal) args.push(\"--fixed-strings\");\n\tif (options.glob) args.push(\"--glob\", options.glob);\n\targs.push(\"--\", options.pattern, options.path);\n\treturn args;\n}\n\nfunction commandWithArgs(command: string, args: string[]): string {\n\treturn [command, ...args.map(shellQuote)].join(\" \");\n}\n\nasync function runSshBuffer(\n\tremote: string,\n\tcommand: string,\n\toptions: { input?: Buffer | string; signal?: AbortSignal; timeout?: number } = {},\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(\"ssh\", sshArgs(remote, command), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet timedOut = false;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\ttimedOut = true;\n\t\t\t\tchild.kill();\n\t\t\t}, options.timeout * 1000);\n\t\t}\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tconst onAbort = () => child.kill();\n\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (options.input !== undefined) {\n\t\t\tchild.stdin.end(options.input);\n\t\t} else {\n\t\t\tchild.stdin.end();\n\t\t}\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (timedOut) {\n\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (code !== 0) {\n\t\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `ssh exited with code ${code}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(Buffer.concat(stdout));\n\t\t});\n\t});\n}\n\nexport class LocalToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate shellPath: string | undefined;\n\n\tconstructor(cwd: string, options: LocalToolOperationsOptions = {}) {\n\t\tthis.cwd = cwd;\n\t\tthis.shellPath = options.shellPath;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst { shell, args } = getShellConfig(this.shellPath);\n\t\ttry {\n\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t} catch {\n\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t}\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(\"aborted\");\n\t\t}\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: options.env ?? getShellEnv(),\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\t\t\tif (options.signal) {\n\t\t\t\tif (options.signal.aborted) onAbort();\n\t\t\t\telse options.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\t\t\twaitForChildProcess(child)\n\t\t\t\t.then((code) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(error);\n\t\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait fsAccess(path, accessModeToFsMode(mode));\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn fsReadFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait fsWriteFile(path, content, typeof content === \"string\" ? \"utf-8\" : undefined);\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait fsMkdir(path, { recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<Stats> {\n\t\treturn fsStat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn fsReaddir(path);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn detectSupportedImageMimeTypeFromFile(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"local\", cwd: this.cwd };\n\t}\n}\n\nexport class SshToolOperations implements ToolOperations {\n\treadonly remote: string;\n\tcwd: string;\n\n\tconstructor(options: SshToolOperationsOptions) {\n\t\tthis.remote = options.remote;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\tstatic async fromTarget(target: string): Promise<SshToolOperations> {\n\t\tconst parsed = parseSshTarget(target);\n\t\tvalidateSshRemote(parsed.remote);\n\t\tconst cwd = parsed.cwd ?? (await runSshBuffer(parsed.remote, \"pwd\")).toString(\"utf-8\").trim();\n\t\treturn new SshToolOperations({ remote: parsed.remote, cwd });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst remoteCommand = `cd ${shellQuote(cwd)} && bash -s`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(\"ssh\", sshArgs(this.remote, remoteCommand), {\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tchild.kill();\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.stdin.end(command);\n\t\t\tconst onAbort = () => child.kill();\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve({ exitCode: code });\n\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tconst remotePath = shellQuote(path);\n\t\tif (mode === \"readwrite\") {\n\t\t\tawait runSshBuffer(this.remote, `test -r ${remotePath} && test -w ${remotePath}`);\n\t\t\treturn;\n\t\t}\n\t\tconst flag = mode === \"read\" ? \"-r\" : mode === \"write\" ? \"-w\" : \"-e\";\n\t\tawait runSshBuffer(this.remote, `test ${flag} ${remotePath}`);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn runSshBuffer(this.remote, `cat ${shellQuote(path)}`);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait runSshBuffer(this.remote, `base64 -d > ${shellQuote(path)}`, {\n\t\t\tinput: Buffer.from(content).toString(\"base64\"),\n\t\t});\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tconst flag = options.recursive ? \"-p \" : \"\";\n\t\tawait runSshBuffer(this.remote, `mkdir ${flag}${shellQuote(path)}`);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`if test -d ${shellQuote(path)}; then echo d; elif test -f ${shellQuote(path)}; then echo f; else test -e ${shellQuote(path)} && echo o || exit 1; fi`,\n\t\t);\n\t\tconst kind = output.toString(\"utf-8\").trim();\n\t\treturn {\n\t\t\tisDirectory: () => kind === \"d\",\n\t\t\tisFile: () => kind === \"f\",\n\t\t};\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`find ${shellQuote(path)} -maxdepth 1 -mindepth 1 -printf '%f\\\\n'`,\n\t\t);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst command = commandWithArgs(\"fd\", buildFdArgs(pattern, cwd, options.limit));\n\t\tconst output = await runSshBuffer(this.remote, command);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst isDirectory = (await this.stat(options.path)).isDirectory();\n\t\tconst command = commandWithArgs(\"rg\", buildRgArgs(options));\n\t\tconst output = await runSshBuffer(this.remote, command).catch((error: unknown) => {\n\t\t\tif (error instanceof Error && error.message.includes(\"ssh exited with code 1\")) {\n\t\t\t\treturn Buffer.alloc(0);\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tconst matches: ToolGrepMatch[] = [];\n\t\tfor (const line of output.toString(\"utf-8\").split(\"\\n\")) {\n\t\t\tif (!line.trim() || matches.length >= options.limit) continue;\n\t\t\tlet event: unknown;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!event || typeof event !== \"object\" || !(\"type\" in event) || event.type !== \"match\") continue;\n\t\t\tconst data = \"data\" in event && event.data && typeof event.data === \"object\" ? event.data : undefined;\n\t\t\tconst filePath =\n\t\t\t\tdata && \"path\" in data && data.path && typeof data.path === \"object\" && \"text\" in data.path\n\t\t\t\t\t? data.path.text\n\t\t\t\t\t: undefined;\n\t\t\tconst lineNumber = data && \"line_number\" in data ? data.line_number : undefined;\n\t\t\tconst lineText =\n\t\t\t\tdata && \"lines\" in data && data.lines && typeof data.lines === \"object\" && \"text\" in data.lines\n\t\t\t\t\t? data.lines.text\n\t\t\t\t\t: undefined;\n\t\t\tif (typeof filePath === \"string\" && typeof lineNumber === \"number\") {\n\t\t\t\tmatches.push({ filePath, lineNumber, lineText: typeof lineText === \"string\" ? lineText : undefined });\n\t\t\t}\n\t\t}\n\t\treturn { isDirectory, matches };\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\ttry {\n\t\t\tconst output = await runSshBuffer(this.remote, `file --mime-type -b ${shellQuote(path)}`);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\treturn [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"ssh\", remote: this.remote, cwd: this.cwd, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {}\n}\n\nexport class DeferredRemoteToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate operations: SshToolOperations | RemoteToolOperations | undefined;\n\n\tconstructor(cwd: string) {\n\t\tthis.cwd = cwd;\n\t}\n\n\tasync configure(options: DeferredRemoteToolOperationsConfigureSshOptions): Promise<ToolBackendInfo> {\n\t\tconst next = new SshToolOperations({ remote: options.remote, cwd: options.cwd ?? this.cwd });\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`SSH backend cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tasync configureRemote(url: string): Promise<ToolBackendInfo> {\n\t\tconst next = await createRemoteToolOperations(url);\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`Remote daemon cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tclear(): void {\n\t\tvoid this.operations?.dispose?.();\n\t\tthis.operations = undefined;\n\t}\n\n\tprivate requireOperations(): SshToolOperations | RemoteToolOperations {\n\t\tif (!this.operations) {\n\t\t\tthrow new Error(\"Remote backend is not configured. Configure it over RPC or with /remote before using tools.\");\n\t\t}\n\t\treturn this.operations;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\treturn this.requireOperations().exec(command, options);\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.requireOperations().access(path, mode);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn this.requireOperations().readFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.requireOperations().writeFile(path, content);\n\t}\n\n\tasync mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {\n\t\tawait this.requireOperations().mkdir(path, options);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\treturn this.requireOperations().stat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn this.requireOperations().readdir(path);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\treturn this.requireOperations().glob(pattern, cwd, options);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\treturn this.requireOperations().grep(options);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn this.requireOperations().detectImageMimeType(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn this.operations?.getBackendInfo() ?? { type: \"remote\", cwd: this.cwd, configured: false };\n\t}\n}\n\ntype RemoteResponse = { id: string; result: unknown } | { id: string; error: { message?: unknown } | string };\n\ntype RemoteExecEvent =\n\t| { id: string; event: \"data\"; dataBase64?: unknown; data?: unknown; stream?: unknown }\n\t| { id: string; event: \"exit\"; exitCode?: unknown; cancelled?: unknown }\n\t| { id: string; event: \"error\"; error?: { message?: unknown } | string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction remoteErrorMessage(error: unknown): string {\n\tif (typeof error === \"string\") return error;\n\tif (isRecord(error) && typeof error.message === \"string\") return error.message;\n\treturn \"remote operation failed\";\n}\n\nfunction requireString(value: unknown, name: string): string {\n\tif (typeof value !== \"string\") throw new Error(`Remote response missing string ${name}`);\n\treturn value;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction optionalNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" ? value : undefined;\n}\n\nfunction normalizeRemoteUrl(url: string): { url: string; protocol: \"ws\" } {\n\tconst parsed = new URL(url);\n\tif (parsed.protocol !== \"ws:\" && parsed.protocol !== \"wss:\") {\n\t\tthrow new Error(`--remote currently supports ws:// and wss:// URLs, got ${parsed.protocol}`);\n\t}\n\treturn { url, protocol: \"ws\" };\n}\n\nexport class RemoteToolOperations implements ToolOperations {\n\treadonly url: string;\n\treadonly protocol: \"ws\";\n\tcwd: string;\n\tprivate socket: WebSocket;\n\tprivate nextId = 1;\n\tprivate pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tresolve: (value: unknown) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate execPending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tresolve: (value: { exitCode: number | null }) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate keepAliveInterval: NodeJS.Timeout | undefined;\n\tprivate lastPongAt = Date.now();\n\n\tprivate constructor(url: string, protocol: \"ws\", socket: WebSocket, cwd: string) {\n\t\tthis.url = url;\n\t\tthis.protocol = protocol;\n\t\tthis.socket = socket;\n\t\tthis.cwd = cwd;\n\t\tthis.socket.addEventListener(\"message\", (event) => this.handleMessage(event.data));\n\t\tthis.socket.addEventListener(\"close\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection closed\"));\n\t\t});\n\t\tthis.socket.addEventListener(\"error\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection error\"));\n\t\t});\n\t\tthis.startKeepAlive();\n\t}\n\n\tstatic async connect(url: string): Promise<RemoteToolOperations> {\n\t\tconst normalized = normalizeRemoteUrl(url);\n\t\tconst socket = await new Promise<WebSocket>((resolveSocket, rejectSocket) => {\n\t\t\tconst ws = new WebSocket(normalized.url);\n\t\t\tconst cleanup = () => {\n\t\t\t\tws.removeEventListener(\"open\", onOpen);\n\t\t\t\tws.removeEventListener(\"error\", onError);\n\t\t\t};\n\t\t\tconst onOpen = () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolveSocket(ws);\n\t\t\t};\n\t\t\tconst onError = () => {\n\t\t\t\tcleanup();\n\t\t\t\trejectSocket(new Error(`failed to connect remote commander: ${normalized.url}`));\n\t\t\t};\n\t\t\tws.addEventListener(\"open\", onOpen, { once: true });\n\t\t\tws.addEventListener(\"error\", onError, { once: true });\n\t\t});\n\t\tconst operations = new RemoteToolOperations(normalized.url, normalized.protocol, socket, \"/\");\n\t\tconst capabilities = await operations.request(\"capabilities\", {});\n\t\tif (isRecord(capabilities) && typeof capabilities.cwd === \"string\") {\n\t\t\toperations.cwd = capabilities.cwd;\n\t\t}\n\t\treturn operations;\n\t}\n\n\tprivate startKeepAlive(): void {\n\t\tthis.keepAliveInterval = setInterval(() => {\n\t\t\tif (Date.now() - this.lastPongAt > 90_000) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(new Error(\"remote connection heartbeat timed out\"));\n\t\t\t\tthis.socket.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({ type: \"ping\", timestamp: Date.now() });\n\t\t\t} catch (error) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t}, 30_000);\n\t\tthis.keepAliveInterval.unref?.();\n\t}\n\n\tprivate stopKeepAlive(): void {\n\t\tif (this.keepAliveInterval) {\n\t\t\tclearInterval(this.keepAliveInterval);\n\t\t\tthis.keepAliveInterval = undefined;\n\t\t}\n\t}\n\n\tprivate rejectAll(error: Error): void {\n\t\tfor (const pending of this.pending.values()) pending.reject(error);\n\t\tthis.pending.clear();\n\t\tfor (const pending of this.execPending.values()) pending.reject(error);\n\t\tthis.execPending.clear();\n\t}\n\n\tprivate send(message: Record<string, unknown>): void {\n\t\tif (this.socket.readyState !== WebSocket.OPEN) {\n\t\t\tthrow new Error(\"remote connection is not open\");\n\t\t}\n\t\tthis.socket.send(JSON.stringify(message));\n\t}\n\n\tprivate request(method: string, params: Record<string, unknown>): Promise<unknown> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\ttry {\n\t\t\t\tthis.send({ id, method, params });\n\t\t\t} catch (error) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(data: unknown): void {\n\t\tif (typeof data !== \"string\") return;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(data);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!isRecord(parsed)) return;\n\t\tif (parsed.type === \"pong\") {\n\t\t\tthis.lastPongAt = Date.now();\n\t\t\treturn;\n\t\t}\n\t\tif (typeof parsed.id !== \"string\") return;\n\t\tif (typeof parsed.event === \"string\") {\n\t\t\tthis.handleExecEvent(parsed as RemoteExecEvent);\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.pending.get(parsed.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(parsed.id);\n\t\tconst response = parsed as RemoteResponse;\n\t\tif (\"error\" in response) {\n\t\t\tpending.reject(new Error(remoteErrorMessage(response.error)));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExecEvent(event: RemoteExecEvent): void {\n\t\tconst pending = this.execPending.get(event.id);\n\t\tif (!pending) return;\n\t\tif (event.event === \"data\") {\n\t\t\tconst encoded = optionalString(event.dataBase64);\n\t\t\tconst text = optionalString(event.data);\n\t\t\tif (encoded !== undefined) pending.onData(Buffer.from(encoded, \"base64\"));\n\t\t\telse if (text !== undefined) pending.onData(Buffer.from(text));\n\t\t\treturn;\n\t\t}\n\t\tthis.execPending.delete(event.id);\n\t\tif (event.event === \"error\") {\n\t\t\tpending.reject(new Error(remoteErrorMessage(event.error)));\n\t\t\treturn;\n\t\t}\n\t\tconst exitCode = optionalNumber(event.exitCode) ?? null;\n\t\tpending.resolve({ exitCode });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t};\n\t\t\tconst onAbort = () => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t} catch {}\n\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\tif (pending) {\n\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\tcleanup();\n\t\t\t\t\tpending.reject(new Error(\"aborted\"));\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis.execPending.set(id, {\n\t\t\t\tonData: options.onData,\n\t\t\t\tresolve: (value) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t\t} catch {}\n\t\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\t\tif (pending) {\n\t\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tpending.reject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t}\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({\n\t\t\t\t\tid,\n\t\t\t\t\tmethod: \"exec\",\n\t\t\t\t\tparams: { command, cwd: options.cwd ?? this.cwd, env: options.env, timeout: options.timeout },\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tthis.execPending.delete(id);\n\t\t\t\tcleanup();\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.request(\"access\", { path, mode });\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\tconst result = await this.request(\"readFile\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote readFile response\");\n\t\treturn Buffer.from(requireString(result.contentBase64, \"contentBase64\"), \"base64\");\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.request(\"writeFile\", { path, contentBase64: Buffer.from(content).toString(\"base64\") });\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait this.request(\"mkdir\", { path, recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst result = await this.request(\"stat\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote stat response\");\n\t\tconst kind = optionalString(result.kind) ?? optionalString(result.type);\n\t\tconst isDirectory = result.isDirectory === true || kind === \"directory\" || kind === \"dir\";\n\t\tconst isFile = result.isFile === true || kind === \"file\";\n\t\treturn { isDirectory: () => isDirectory, isFile: () => isFile };\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst result = await this.request(\"readdir\", { path });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.entries)) throw new Error(\"Invalid remote readdir response\");\n\t\treturn result.entries.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst result = await this.request(\"glob\", { pattern, cwd, ignore: options.ignore, limit: options.limit });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.matches)) throw new Error(\"Invalid remote glob response\");\n\t\treturn result.matches.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst result = await this.request(\"grep\", options as unknown as Record<string, unknown>);\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote grep response\");\n\t\tconst matches = Array.isArray(result.matches) ? result.matches : [];\n\t\treturn {\n\t\t\tisDirectory: result.isDirectory === true,\n\t\t\tmatches: matches.flatMap((entry): ToolGrepMatch[] => {\n\t\t\t\tif (!isRecord(entry)) return [];\n\t\t\t\tconst filePath = optionalString(entry.filePath);\n\t\t\t\tconst lineNumber = optionalNumber(entry.lineNumber);\n\t\t\t\tif (!filePath || lineNumber === undefined) return [];\n\t\t\t\treturn [{ filePath, lineNumber, lineText: optionalString(entry.lineText) }];\n\t\t\t}),\n\t\t};\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\tconst result = await this.request(\"detectImageMimeType\", { path });\n\t\tif (!isRecord(result)) return undefined;\n\t\tconst mimeType = optionalString(result.mimeType);\n\t\treturn mimeType && [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"remote\", cwd: this.cwd, url: this.url, protocol: this.protocol, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.stopKeepAlive();\n\t\tthis.socket.close();\n\t}\n}\n\nexport function createRemoteToolOperations(url: string): Promise<RemoteToolOperations> {\n\treturn RemoteToolOperations.connect(url);\n}\n\nexport function createSshToolOperations(target: string): Promise<SshToolOperations> {\n\treturn SshToolOperations.fromTarget(target);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/core/tools/operations.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { constants, createReadStream, createWriteStream, type Stats, type WriteStream } from \"node:fs\";\nimport {\n\taccess as fsAccess,\n\tmkdir as fsMkdir,\n\treaddir as fsReaddir,\n\treadFile as fsReadFile,\n\tstat as fsStat,\n\twriteFile as fsWriteFile,\n} from \"node:fs/promises\";\nimport { pipeline } from \"node:stream/promises\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\n\nexport type ToolAccessMode = \"exists\" | \"read\" | \"write\" | \"readwrite\";\n\nexport interface ToolFileStat {\n\tisDirectory: () => boolean;\n\tisFile: () => boolean;\n}\n\nexport interface ToolExecOptions {\n\tcwd?: string;\n\tonData: (data: Buffer) => void;\n\tsignal?: AbortSignal;\n\ttimeout?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nexport interface ToolGlobOptions {\n\tignore: string[];\n\tlimit: number;\n}\n\nexport interface ToolGrepOptions {\n\tpattern: string;\n\tpath: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tlimit: number;\n}\n\nexport interface ToolGrepMatch {\n\tfilePath: string;\n\tlineNumber: number;\n\tlineText?: string;\n}\n\nexport interface ToolGrepResult {\n\tisDirectory: boolean;\n\tmatches: ToolGrepMatch[];\n}\n\nexport type ToolBackendInfo =\n\t| { type: \"local\"; cwd: string }\n\t| { type: \"ssh\"; cwd: string; remote: string; configured: true }\n\t| { type: \"remote\"; cwd: string; configured: false }\n\t| { type: \"remote\"; cwd: string; url: string; protocol: \"ws\"; configured: true };\n\nexport interface ToolOperations {\n\tcwd: string;\n\texec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }>;\n\taccess(path: string, mode?: ToolAccessMode): Promise<void>;\n\treadFile(path: string): Promise<Buffer>;\n\twriteFile(path: string, content: string | Buffer): Promise<void>;\n\tuploadFile?(sourcePath: string, destinationPath: string): Promise<void>;\n\tdownloadFile?(sourcePath: string, destinationPath: string): Promise<void>;\n\tmkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n\tstat(path: string): Promise<ToolFileStat>;\n\treaddir(path: string): Promise<string[]>;\n\tglob?(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]>;\n\tgrep?(options: ToolGrepOptions): Promise<ToolGrepResult>;\n\tdetectImageMimeType?(path: string): Promise<string | null | undefined>;\n\tgetBackendInfo?(): ToolBackendInfo;\n\tdispose?(): Promise<void>;\n}\n\nexport interface LocalToolOperationsOptions {\n\tshellPath?: string;\n}\n\nexport interface SshToolOperationsOptions {\n\tremote: string;\n\tcwd: string;\n}\n\nexport interface DeferredRemoteToolOperationsConfigureSshOptions {\n\tremote: string;\n\tcwd?: string;\n}\n\nexport interface ParsedSshTarget {\n\tremote: string;\n\tcwd?: string;\n}\n\nfunction accessModeToFsMode(mode: ToolAccessMode | undefined): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction parseSshTarget(value: string): ParsedSshTarget {\n\tconst separatorIndex = value.indexOf(\":\");\n\tif (separatorIndex === -1) {\n\t\treturn { remote: value };\n\t}\n\tconst remote = value.slice(0, separatorIndex);\n\tconst cwd = value.slice(separatorIndex + 1);\n\treturn cwd ? { remote, cwd } : { remote };\n}\n\nfunction validateSshRemote(remote: string): void {\n\tif (!remote) {\n\t\tthrow new Error(\"--ssh requires a remote target like user@host or user@host:/path\");\n\t}\n\tif (remote.startsWith(\"-\")) {\n\t\tthrow new Error(\"--ssh remote target must not start with '-'\");\n\t}\n}\n\nfunction sshArgs(remote: string, command: string): string[] {\n\tvalidateSshRemote(remote);\n\treturn [\"--\", remote, command];\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(options: ToolGrepOptions): string[] {\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (options.ignoreCase) args.push(\"--ignore-case\");\n\tif (options.literal) args.push(\"--fixed-strings\");\n\tif (options.glob) args.push(\"--glob\", options.glob);\n\targs.push(\"--\", options.pattern, options.path);\n\treturn args;\n}\n\nfunction commandWithArgs(command: string, args: string[]): string {\n\treturn [command, ...args.map(shellQuote)].join(\" \");\n}\n\nasync function copyFileStream(sourcePath: string, destinationPath: string): Promise<void> {\n\tawait pipeline(createReadStream(sourcePath), createWriteStream(destinationPath));\n}\n\nfunction writeStreamChunk(stream: WriteStream, chunk: Buffer): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst onError = (error: Error) => {\n\t\t\tstream.off(\"error\", onError);\n\t\t\treject(error);\n\t\t};\n\t\tstream.once(\"error\", onError);\n\t\tstream.write(chunk, (error) => {\n\t\t\tstream.off(\"error\", onError);\n\t\t\tif (error) {\n\t\t\t\treject(error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve();\n\t\t});\n\t});\n}\n\nfunction endWriteStream(stream: WriteStream): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst onError = (error: Error) => {\n\t\t\tstream.off(\"error\", onError);\n\t\t\treject(error);\n\t\t};\n\t\tstream.once(\"error\", onError);\n\t\tstream.end(() => {\n\t\t\tstream.off(\"error\", onError);\n\t\t\tresolve();\n\t\t});\n\t});\n}\n\nfunction waitForSshFileTransfer(\n\tremote: string,\n\tcommand: string,\n\twireStreams: (child: ReturnType<typeof spawn>) => Promise<void>,\n): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(\"ssh\", sshArgs(remote, command), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tconst stderr: Buffer[] = [];\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\twireStreams(child).catch((error: unknown) => {\n\t\t\tchild.kill();\n\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code !== 0) {\n\t\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `ssh exited with code ${code}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve();\n\t\t});\n\t});\n}\n\nasync function runSshBuffer(\n\tremote: string,\n\tcommand: string,\n\toptions: { input?: Buffer | string; signal?: AbortSignal; timeout?: number } = {},\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(\"ssh\", sshArgs(remote, command), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet timedOut = false;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\ttimedOut = true;\n\t\t\t\tchild.kill();\n\t\t\t}, options.timeout * 1000);\n\t\t}\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tconst onAbort = () => child.kill();\n\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (options.input !== undefined) {\n\t\t\tchild.stdin.end(options.input);\n\t\t} else {\n\t\t\tchild.stdin.end();\n\t\t}\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (timedOut) {\n\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (code !== 0) {\n\t\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `ssh exited with code ${code}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(Buffer.concat(stdout));\n\t\t});\n\t});\n}\n\nexport class LocalToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate shellPath: string | undefined;\n\n\tconstructor(cwd: string, options: LocalToolOperationsOptions = {}) {\n\t\tthis.cwd = cwd;\n\t\tthis.shellPath = options.shellPath;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst { shell, args } = getShellConfig(this.shellPath);\n\t\ttry {\n\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t} catch {\n\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t}\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(\"aborted\");\n\t\t}\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: options.env ?? getShellEnv(),\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\t\t\tif (options.signal) {\n\t\t\t\tif (options.signal.aborted) onAbort();\n\t\t\t\telse options.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\t\t\twaitForChildProcess(child)\n\t\t\t\t.then((code) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(error);\n\t\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait fsAccess(path, accessModeToFsMode(mode));\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn fsReadFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait fsWriteFile(path, content, typeof content === \"string\" ? \"utf-8\" : undefined);\n\t}\n\n\tasync uploadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tawait copyFileStream(sourcePath, destinationPath);\n\t}\n\n\tasync downloadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tawait copyFileStream(sourcePath, destinationPath);\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait fsMkdir(path, { recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<Stats> {\n\t\treturn fsStat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn fsReaddir(path);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn detectSupportedImageMimeTypeFromFile(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"local\", cwd: this.cwd };\n\t}\n}\n\nexport class SshToolOperations implements ToolOperations {\n\treadonly remote: string;\n\tcwd: string;\n\n\tconstructor(options: SshToolOperationsOptions) {\n\t\tthis.remote = options.remote;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\tstatic async fromTarget(target: string): Promise<SshToolOperations> {\n\t\tconst parsed = parseSshTarget(target);\n\t\tvalidateSshRemote(parsed.remote);\n\t\tconst cwd = parsed.cwd ?? (await runSshBuffer(parsed.remote, \"pwd\")).toString(\"utf-8\").trim();\n\t\treturn new SshToolOperations({ remote: parsed.remote, cwd });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst remoteCommand = `cd ${shellQuote(cwd)} && bash -s`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(\"ssh\", sshArgs(this.remote, remoteCommand), {\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tchild.kill();\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.stdin.end(command);\n\t\t\tconst onAbort = () => child.kill();\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve({ exitCode: code });\n\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tconst remotePath = shellQuote(path);\n\t\tif (mode === \"readwrite\") {\n\t\t\tawait runSshBuffer(this.remote, `test -r ${remotePath} && test -w ${remotePath}`);\n\t\t\treturn;\n\t\t}\n\t\tconst flag = mode === \"read\" ? \"-r\" : mode === \"write\" ? \"-w\" : \"-e\";\n\t\tawait runSshBuffer(this.remote, `test ${flag} ${remotePath}`);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn runSshBuffer(this.remote, `cat ${shellQuote(path)}`);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait runSshBuffer(this.remote, `base64 -d > ${shellQuote(path)}`, {\n\t\t\tinput: Buffer.from(content).toString(\"base64\"),\n\t\t});\n\t}\n\n\tasync uploadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tawait waitForSshFileTransfer(this.remote, `cat > ${shellQuote(destinationPath)}`, async (child) => {\n\t\t\tif (!child.stdin) throw new Error(\"ssh stdin is unavailable\");\n\t\t\tawait pipeline(createReadStream(sourcePath), child.stdin);\n\t\t});\n\t}\n\n\tasync downloadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tawait waitForSshFileTransfer(this.remote, `cat ${shellQuote(sourcePath)}`, async (child) => {\n\t\t\tif (!child.stdout) throw new Error(\"ssh stdout is unavailable\");\n\t\t\tawait pipeline(child.stdout, createWriteStream(destinationPath));\n\t\t});\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tconst flag = options.recursive ? \"-p \" : \"\";\n\t\tawait runSshBuffer(this.remote, `mkdir ${flag}${shellQuote(path)}`);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`if test -d ${shellQuote(path)}; then echo d; elif test -f ${shellQuote(path)}; then echo f; else test -e ${shellQuote(path)} && echo o || exit 1; fi`,\n\t\t);\n\t\tconst kind = output.toString(\"utf-8\").trim();\n\t\treturn {\n\t\t\tisDirectory: () => kind === \"d\",\n\t\t\tisFile: () => kind === \"f\",\n\t\t};\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`find ${shellQuote(path)} -maxdepth 1 -mindepth 1 -printf '%f\\\\n'`,\n\t\t);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst command = commandWithArgs(\"fd\", buildFdArgs(pattern, cwd, options.limit));\n\t\tconst output = await runSshBuffer(this.remote, command);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst isDirectory = (await this.stat(options.path)).isDirectory();\n\t\tconst command = commandWithArgs(\"rg\", buildRgArgs(options));\n\t\tconst output = await runSshBuffer(this.remote, command).catch((error: unknown) => {\n\t\t\tif (error instanceof Error && error.message.includes(\"ssh exited with code 1\")) {\n\t\t\t\treturn Buffer.alloc(0);\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tconst matches: ToolGrepMatch[] = [];\n\t\tfor (const line of output.toString(\"utf-8\").split(\"\\n\")) {\n\t\t\tif (!line.trim() || matches.length >= options.limit) continue;\n\t\t\tlet event: unknown;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!event || typeof event !== \"object\" || !(\"type\" in event) || event.type !== \"match\") continue;\n\t\t\tconst data = \"data\" in event && event.data && typeof event.data === \"object\" ? event.data : undefined;\n\t\t\tconst filePath =\n\t\t\t\tdata && \"path\" in data && data.path && typeof data.path === \"object\" && \"text\" in data.path\n\t\t\t\t\t? data.path.text\n\t\t\t\t\t: undefined;\n\t\t\tconst lineNumber = data && \"line_number\" in data ? data.line_number : undefined;\n\t\t\tconst lineText =\n\t\t\t\tdata && \"lines\" in data && data.lines && typeof data.lines === \"object\" && \"text\" in data.lines\n\t\t\t\t\t? data.lines.text\n\t\t\t\t\t: undefined;\n\t\t\tif (typeof filePath === \"string\" && typeof lineNumber === \"number\") {\n\t\t\t\tmatches.push({ filePath, lineNumber, lineText: typeof lineText === \"string\" ? lineText : undefined });\n\t\t\t}\n\t\t}\n\t\treturn { isDirectory, matches };\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\ttry {\n\t\t\tconst output = await runSshBuffer(this.remote, `file --mime-type -b ${shellQuote(path)}`);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\treturn [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"ssh\", remote: this.remote, cwd: this.cwd, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {}\n}\n\nexport class DeferredRemoteToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate operations: SshToolOperations | RemoteToolOperations | undefined;\n\n\tconstructor(cwd: string) {\n\t\tthis.cwd = cwd;\n\t}\n\n\tasync configure(options: DeferredRemoteToolOperationsConfigureSshOptions): Promise<ToolBackendInfo> {\n\t\tconst next = new SshToolOperations({ remote: options.remote, cwd: options.cwd ?? this.cwd });\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`SSH backend cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tasync configureRemote(url: string): Promise<ToolBackendInfo> {\n\t\tconst next = await createRemoteToolOperations(url);\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`Remote daemon cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tclear(): void {\n\t\tvoid this.operations?.dispose?.();\n\t\tthis.operations = undefined;\n\t}\n\n\tprivate requireOperations(): SshToolOperations | RemoteToolOperations {\n\t\tif (!this.operations) {\n\t\t\tthrow new Error(\"Remote backend is not configured. Configure it over RPC or with /remote before using tools.\");\n\t\t}\n\t\treturn this.operations;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\treturn this.requireOperations().exec(command, options);\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.requireOperations().access(path, mode);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn this.requireOperations().readFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.requireOperations().writeFile(path, content);\n\t}\n\n\tasync uploadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tconst operations = this.requireOperations();\n\t\tif (!operations.uploadFile) throw new Error(\"Remote backend does not support file upload\");\n\t\tawait operations.uploadFile(sourcePath, destinationPath);\n\t}\n\n\tasync downloadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tconst operations = this.requireOperations();\n\t\tif (!operations.downloadFile) throw new Error(\"Remote backend does not support file download\");\n\t\tawait operations.downloadFile(sourcePath, destinationPath);\n\t}\n\n\tasync mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {\n\t\tawait this.requireOperations().mkdir(path, options);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\treturn this.requireOperations().stat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn this.requireOperations().readdir(path);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\treturn this.requireOperations().glob(pattern, cwd, options);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\treturn this.requireOperations().grep(options);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn this.requireOperations().detectImageMimeType(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn this.operations?.getBackendInfo() ?? { type: \"remote\", cwd: this.cwd, configured: false };\n\t}\n}\n\ntype RemoteResponse = { id: string; result: unknown } | { id: string; error: { message?: unknown } | string };\n\ntype RemoteExecEvent =\n\t| { id: string; event: \"data\"; dataBase64?: unknown; data?: unknown; stream?: unknown }\n\t| { id: string; event: \"exit\"; exitCode?: unknown; cancelled?: unknown }\n\t| { id: string; event: \"error\"; error?: { message?: unknown } | string };\n\ntype RemoteFileEvent =\n\t| { id: string; event: \"fileData\"; dataBase64?: unknown }\n\t| { id: string; event: \"fileEnd\" }\n\t| { id: string; event: \"fileError\"; error?: { message?: unknown } | string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction remoteErrorMessage(error: unknown): string {\n\tif (typeof error === \"string\") return error;\n\tif (isRecord(error) && typeof error.message === \"string\") return error.message;\n\treturn \"remote operation failed\";\n}\n\nfunction requireString(value: unknown, name: string): string {\n\tif (typeof value !== \"string\") throw new Error(`Remote response missing string ${name}`);\n\treturn value;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction optionalNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" ? value : undefined;\n}\n\nfunction normalizeRemoteUrl(url: string): { url: string; protocol: \"ws\" } {\n\tconst parsed = new URL(url);\n\tif (parsed.protocol !== \"ws:\" && parsed.protocol !== \"wss:\") {\n\t\tthrow new Error(`--remote currently supports ws:// and wss:// URLs, got ${parsed.protocol}`);\n\t}\n\treturn { url, protocol: \"ws\" };\n}\n\nexport class RemoteToolOperations implements ToolOperations {\n\treadonly url: string;\n\treadonly protocol: \"ws\";\n\tcwd: string;\n\tprivate socket: WebSocket;\n\tprivate nextId = 1;\n\tprivate pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tresolve: (value: unknown) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate execPending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tresolve: (value: { exitCode: number | null }) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate fileDownloadPending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tstream: WriteStream;\n\t\t\twritePromise: Promise<void>;\n\t\t\tresolve: () => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate keepAliveInterval: NodeJS.Timeout | undefined;\n\tprivate lastPongAt = Date.now();\n\n\tprivate constructor(url: string, protocol: \"ws\", socket: WebSocket, cwd: string) {\n\t\tthis.url = url;\n\t\tthis.protocol = protocol;\n\t\tthis.socket = socket;\n\t\tthis.cwd = cwd;\n\t\tthis.socket.addEventListener(\"message\", (event) => this.handleMessage(event.data));\n\t\tthis.socket.addEventListener(\"close\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection closed\"));\n\t\t});\n\t\tthis.socket.addEventListener(\"error\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection error\"));\n\t\t});\n\t\tthis.startKeepAlive();\n\t}\n\n\tstatic async connect(url: string): Promise<RemoteToolOperations> {\n\t\tconst normalized = normalizeRemoteUrl(url);\n\t\tconst socket = await new Promise<WebSocket>((resolveSocket, rejectSocket) => {\n\t\t\tconst ws = new WebSocket(normalized.url);\n\t\t\tconst cleanup = () => {\n\t\t\t\tws.removeEventListener(\"open\", onOpen);\n\t\t\t\tws.removeEventListener(\"error\", onError);\n\t\t\t};\n\t\t\tconst onOpen = () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolveSocket(ws);\n\t\t\t};\n\t\t\tconst onError = () => {\n\t\t\t\tcleanup();\n\t\t\t\trejectSocket(new Error(`failed to connect remote commander: ${normalized.url}`));\n\t\t\t};\n\t\t\tws.addEventListener(\"open\", onOpen, { once: true });\n\t\t\tws.addEventListener(\"error\", onError, { once: true });\n\t\t});\n\t\tconst operations = new RemoteToolOperations(normalized.url, normalized.protocol, socket, \"/\");\n\t\tconst capabilities = await operations.request(\"capabilities\", {});\n\t\tif (isRecord(capabilities) && typeof capabilities.cwd === \"string\") {\n\t\t\toperations.cwd = capabilities.cwd;\n\t\t}\n\t\treturn operations;\n\t}\n\n\tprivate startKeepAlive(): void {\n\t\tthis.keepAliveInterval = setInterval(() => {\n\t\t\tif (Date.now() - this.lastPongAt > 90_000) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(new Error(\"remote connection heartbeat timed out\"));\n\t\t\t\tthis.socket.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({ type: \"ping\", timestamp: Date.now() });\n\t\t\t} catch (error) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t}, 30_000);\n\t\tthis.keepAliveInterval.unref?.();\n\t}\n\n\tprivate stopKeepAlive(): void {\n\t\tif (this.keepAliveInterval) {\n\t\t\tclearInterval(this.keepAliveInterval);\n\t\t\tthis.keepAliveInterval = undefined;\n\t\t}\n\t}\n\n\tprivate rejectAll(error: Error): void {\n\t\tfor (const pending of this.pending.values()) pending.reject(error);\n\t\tthis.pending.clear();\n\t\tfor (const pending of this.execPending.values()) pending.reject(error);\n\t\tthis.execPending.clear();\n\t\tfor (const pending of this.fileDownloadPending.values()) {\n\t\t\tpending.stream.destroy(error);\n\t\t\tpending.reject(error);\n\t\t}\n\t\tthis.fileDownloadPending.clear();\n\t}\n\n\tprivate send(message: Record<string, unknown>): void {\n\t\tif (this.socket.readyState !== WebSocket.OPEN) {\n\t\t\tthrow new Error(\"remote connection is not open\");\n\t\t}\n\t\tthis.socket.send(JSON.stringify(message));\n\t}\n\n\tprivate request(method: string, params: Record<string, unknown>): Promise<unknown> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\ttry {\n\t\t\t\tthis.send({ id, method, params });\n\t\t\t} catch (error) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(data: unknown): void {\n\t\tif (typeof data !== \"string\") return;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(data);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!isRecord(parsed)) return;\n\t\tif (parsed.type === \"pong\") {\n\t\t\tthis.lastPongAt = Date.now();\n\t\t\treturn;\n\t\t}\n\t\tif (typeof parsed.id !== \"string\") return;\n\t\tif (typeof parsed.event === \"string\") {\n\t\t\tif (parsed.event === \"fileData\" || parsed.event === \"fileEnd\" || parsed.event === \"fileError\") {\n\t\t\t\tthis.handleFileEvent(parsed as RemoteFileEvent);\n\t\t\t} else {\n\t\t\t\tthis.handleExecEvent(parsed as RemoteExecEvent);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.pending.get(parsed.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(parsed.id);\n\t\tconst response = parsed as RemoteResponse;\n\t\tif (\"error\" in response) {\n\t\t\tpending.reject(new Error(remoteErrorMessage(response.error)));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExecEvent(event: RemoteExecEvent): void {\n\t\tconst pending = this.execPending.get(event.id);\n\t\tif (!pending) return;\n\t\tif (event.event === \"data\") {\n\t\t\tconst encoded = optionalString(event.dataBase64);\n\t\t\tconst text = optionalString(event.data);\n\t\t\tif (encoded !== undefined) pending.onData(Buffer.from(encoded, \"base64\"));\n\t\t\telse if (text !== undefined) pending.onData(Buffer.from(text));\n\t\t\treturn;\n\t\t}\n\t\tthis.execPending.delete(event.id);\n\t\tif (event.event === \"error\") {\n\t\t\tpending.reject(new Error(remoteErrorMessage(event.error)));\n\t\t\treturn;\n\t\t}\n\t\tconst exitCode = optionalNumber(event.exitCode) ?? null;\n\t\tpending.resolve({ exitCode });\n\t}\n\n\tprivate handleFileEvent(event: RemoteFileEvent): void {\n\t\tconst pending = this.fileDownloadPending.get(event.id);\n\t\tif (!pending) return;\n\t\tif (event.event === \"fileData\") {\n\t\t\tconst encoded = optionalString(event.dataBase64);\n\t\t\tif (encoded === undefined) return;\n\t\t\tpending.writePromise = pending.writePromise.then(() =>\n\t\t\t\twriteStreamChunk(pending.stream, Buffer.from(encoded, \"base64\")),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis.fileDownloadPending.delete(event.id);\n\t\tif (event.event === \"fileError\") {\n\t\t\tconst error = new Error(remoteErrorMessage(event.error));\n\t\t\tpending.reject(error);\n\t\t\tpending.stream.destroy(error);\n\t\t\treturn;\n\t\t}\n\t\tpending.writePromise.then(() => endWriteStream(pending.stream)).then(pending.resolve, pending.reject);\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t};\n\t\t\tconst onAbort = () => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t} catch {}\n\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\tif (pending) {\n\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\tcleanup();\n\t\t\t\t\tpending.reject(new Error(\"aborted\"));\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis.execPending.set(id, {\n\t\t\t\tonData: options.onData,\n\t\t\t\tresolve: (value) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t\t} catch {}\n\t\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\t\tif (pending) {\n\t\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tpending.reject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t}\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({\n\t\t\t\t\tid,\n\t\t\t\t\tmethod: \"exec\",\n\t\t\t\t\tparams: { command, cwd: options.cwd ?? this.cwd, env: options.env, timeout: options.timeout },\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tthis.execPending.delete(id);\n\t\t\t\tcleanup();\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.request(\"access\", { path, mode });\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\tconst result = await this.request(\"readFile\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote readFile response\");\n\t\treturn Buffer.from(requireString(result.contentBase64, \"contentBase64\"), \"base64\");\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.request(\"writeFile\", { path, contentBase64: Buffer.from(content).toString(\"base64\") });\n\t}\n\n\tasync uploadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tconst uploadId = `remote-${this.nextId++}`;\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.pending.set(uploadId, { resolve: () => resolve(), reject });\n\t\t\ttry {\n\t\t\t\tthis.send({ id: uploadId, method: \"uploadFileStart\", params: { path: destinationPath } });\n\t\t\t} catch (error) {\n\t\t\t\tthis.pending.delete(uploadId);\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t\ttry {\n\t\t\tfor await (const chunk of createReadStream(sourcePath, { highWaterMark: 64 * 1024 })) {\n\t\t\t\tconst buffer = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n\t\t\t\tawait this.request(\"uploadFileChunk\", { uploadId, dataBase64: buffer.toString(\"base64\") });\n\t\t\t}\n\t\t\tawait this.request(\"uploadFileEnd\", { uploadId });\n\t\t} catch (error) {\n\t\t\tawait this.request(\"uploadFileCancel\", { uploadId }).catch(() => undefined);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync downloadFile(sourcePath: string, destinationPath: string): Promise<void> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\tconst stream = createWriteStream(destinationPath);\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst cleanup = () => stream.off(\"error\", onStreamError);\n\t\t\tconst onStreamError = (error: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\tthis.fileDownloadPending.delete(id);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tstream.once(\"error\", onStreamError);\n\t\t\tthis.fileDownloadPending.set(id, {\n\t\t\t\tstream,\n\t\t\t\twritePromise: Promise.resolve(),\n\t\t\t\tresolve: () => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tthis.send({ id, method: \"downloadFile\", params: { path: sourcePath } });\n\t\t\t} catch (error) {\n\t\t\t\tcleanup();\n\t\t\t\tthis.fileDownloadPending.delete(id);\n\t\t\t\tstream.destroy();\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait this.request(\"mkdir\", { path, recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst result = await this.request(\"stat\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote stat response\");\n\t\tconst kind = optionalString(result.kind) ?? optionalString(result.type);\n\t\tconst isDirectory = result.isDirectory === true || kind === \"directory\" || kind === \"dir\";\n\t\tconst isFile = result.isFile === true || kind === \"file\";\n\t\treturn { isDirectory: () => isDirectory, isFile: () => isFile };\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst result = await this.request(\"readdir\", { path });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.entries)) throw new Error(\"Invalid remote readdir response\");\n\t\treturn result.entries.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst result = await this.request(\"glob\", { pattern, cwd, ignore: options.ignore, limit: options.limit });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.matches)) throw new Error(\"Invalid remote glob response\");\n\t\treturn result.matches.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst result = await this.request(\"grep\", options as unknown as Record<string, unknown>);\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote grep response\");\n\t\tconst matches = Array.isArray(result.matches) ? result.matches : [];\n\t\treturn {\n\t\t\tisDirectory: result.isDirectory === true,\n\t\t\tmatches: matches.flatMap((entry): ToolGrepMatch[] => {\n\t\t\t\tif (!isRecord(entry)) return [];\n\t\t\t\tconst filePath = optionalString(entry.filePath);\n\t\t\t\tconst lineNumber = optionalNumber(entry.lineNumber);\n\t\t\t\tif (!filePath || lineNumber === undefined) return [];\n\t\t\t\treturn [{ filePath, lineNumber, lineText: optionalString(entry.lineText) }];\n\t\t\t}),\n\t\t};\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\tconst result = await this.request(\"detectImageMimeType\", { path });\n\t\tif (!isRecord(result)) return undefined;\n\t\tconst mimeType = optionalString(result.mimeType);\n\t\treturn mimeType && [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"remote\", cwd: this.cwd, url: this.url, protocol: this.protocol, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.stopKeepAlive();\n\t\tthis.socket.close();\n\t}\n}\n\nexport function createRemoteToolOperations(url: string): Promise<RemoteToolOperations> {\n\treturn RemoteToolOperations.connect(url);\n}\n\nexport function createSshToolOperations(target: string): Promise<SshToolOperations> {\n\treturn SshToolOperations.fromTarget(target);\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { constants } from "node:fs";
|
|
2
|
+
import { constants, createReadStream, createWriteStream } from "node:fs";
|
|
3
3
|
import { access as fsAccess, mkdir as fsMkdir, readdir as fsReaddir, readFile as fsReadFile, stat as fsStat, writeFile as fsWriteFile, } from "node:fs/promises";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
4
5
|
import { waitForChildProcess } from "../../utils/child-process.js";
|
|
5
6
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
6
7
|
import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
|
|
@@ -67,6 +68,58 @@ function buildRgArgs(options) {
|
|
|
67
68
|
function commandWithArgs(command, args) {
|
|
68
69
|
return [command, ...args.map(shellQuote)].join(" ");
|
|
69
70
|
}
|
|
71
|
+
async function copyFileStream(sourcePath, destinationPath) {
|
|
72
|
+
await pipeline(createReadStream(sourcePath), createWriteStream(destinationPath));
|
|
73
|
+
}
|
|
74
|
+
function writeStreamChunk(stream, chunk) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const onError = (error) => {
|
|
77
|
+
stream.off("error", onError);
|
|
78
|
+
reject(error);
|
|
79
|
+
};
|
|
80
|
+
stream.once("error", onError);
|
|
81
|
+
stream.write(chunk, (error) => {
|
|
82
|
+
stream.off("error", onError);
|
|
83
|
+
if (error) {
|
|
84
|
+
reject(error);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function endWriteStream(stream) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const onError = (error) => {
|
|
94
|
+
stream.off("error", onError);
|
|
95
|
+
reject(error);
|
|
96
|
+
};
|
|
97
|
+
stream.once("error", onError);
|
|
98
|
+
stream.end(() => {
|
|
99
|
+
stream.off("error", onError);
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function waitForSshFileTransfer(remote, command, wireStreams) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const child = spawn("ssh", sshArgs(remote, command), { stdio: ["pipe", "pipe", "pipe"] });
|
|
107
|
+
const stderr = [];
|
|
108
|
+
child.stderr.on("data", (data) => stderr.push(data));
|
|
109
|
+
child.on("error", reject);
|
|
110
|
+
wireStreams(child).catch((error) => {
|
|
111
|
+
child.kill();
|
|
112
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
113
|
+
});
|
|
114
|
+
child.on("close", (code) => {
|
|
115
|
+
if (code !== 0) {
|
|
116
|
+
reject(new Error(Buffer.concat(stderr).toString("utf-8").trim() || `ssh exited with code ${code}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
70
123
|
async function runSshBuffer(remote, command, options = {}) {
|
|
71
124
|
return new Promise((resolve, reject) => {
|
|
72
125
|
const child = spawn("ssh", sshArgs(remote, command), { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -199,6 +252,12 @@ export class LocalToolOperations {
|
|
|
199
252
|
async writeFile(path, content) {
|
|
200
253
|
await fsWriteFile(path, content, typeof content === "string" ? "utf-8" : undefined);
|
|
201
254
|
}
|
|
255
|
+
async uploadFile(sourcePath, destinationPath) {
|
|
256
|
+
await copyFileStream(sourcePath, destinationPath);
|
|
257
|
+
}
|
|
258
|
+
async downloadFile(sourcePath, destinationPath) {
|
|
259
|
+
await copyFileStream(sourcePath, destinationPath);
|
|
260
|
+
}
|
|
202
261
|
async mkdir(path, options = {}) {
|
|
203
262
|
await fsMkdir(path, { recursive: options.recursive ?? false });
|
|
204
263
|
}
|
|
@@ -282,6 +341,20 @@ export class SshToolOperations {
|
|
|
282
341
|
input: Buffer.from(content).toString("base64"),
|
|
283
342
|
});
|
|
284
343
|
}
|
|
344
|
+
async uploadFile(sourcePath, destinationPath) {
|
|
345
|
+
await waitForSshFileTransfer(this.remote, `cat > ${shellQuote(destinationPath)}`, async (child) => {
|
|
346
|
+
if (!child.stdin)
|
|
347
|
+
throw new Error("ssh stdin is unavailable");
|
|
348
|
+
await pipeline(createReadStream(sourcePath), child.stdin);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async downloadFile(sourcePath, destinationPath) {
|
|
352
|
+
await waitForSshFileTransfer(this.remote, `cat ${shellQuote(sourcePath)}`, async (child) => {
|
|
353
|
+
if (!child.stdout)
|
|
354
|
+
throw new Error("ssh stdout is unavailable");
|
|
355
|
+
await pipeline(child.stdout, createWriteStream(destinationPath));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
285
358
|
async mkdir(path, options = {}) {
|
|
286
359
|
const flag = options.recursive ? "-p " : "";
|
|
287
360
|
await runSshBuffer(this.remote, `mkdir ${flag}${shellQuote(path)}`);
|
|
@@ -404,6 +477,18 @@ export class DeferredRemoteToolOperations {
|
|
|
404
477
|
async writeFile(path, content) {
|
|
405
478
|
await this.requireOperations().writeFile(path, content);
|
|
406
479
|
}
|
|
480
|
+
async uploadFile(sourcePath, destinationPath) {
|
|
481
|
+
const operations = this.requireOperations();
|
|
482
|
+
if (!operations.uploadFile)
|
|
483
|
+
throw new Error("Remote backend does not support file upload");
|
|
484
|
+
await operations.uploadFile(sourcePath, destinationPath);
|
|
485
|
+
}
|
|
486
|
+
async downloadFile(sourcePath, destinationPath) {
|
|
487
|
+
const operations = this.requireOperations();
|
|
488
|
+
if (!operations.downloadFile)
|
|
489
|
+
throw new Error("Remote backend does not support file download");
|
|
490
|
+
await operations.downloadFile(sourcePath, destinationPath);
|
|
491
|
+
}
|
|
407
492
|
async mkdir(path, options) {
|
|
408
493
|
await this.requireOperations().mkdir(path, options);
|
|
409
494
|
}
|
|
@@ -462,6 +547,7 @@ export class RemoteToolOperations {
|
|
|
462
547
|
nextId = 1;
|
|
463
548
|
pending = new Map();
|
|
464
549
|
execPending = new Map();
|
|
550
|
+
fileDownloadPending = new Map();
|
|
465
551
|
keepAliveInterval;
|
|
466
552
|
lastPongAt = Date.now();
|
|
467
553
|
constructor(url, protocol, socket, cwd) {
|
|
@@ -537,6 +623,11 @@ export class RemoteToolOperations {
|
|
|
537
623
|
for (const pending of this.execPending.values())
|
|
538
624
|
pending.reject(error);
|
|
539
625
|
this.execPending.clear();
|
|
626
|
+
for (const pending of this.fileDownloadPending.values()) {
|
|
627
|
+
pending.stream.destroy(error);
|
|
628
|
+
pending.reject(error);
|
|
629
|
+
}
|
|
630
|
+
this.fileDownloadPending.clear();
|
|
540
631
|
}
|
|
541
632
|
send(message) {
|
|
542
633
|
if (this.socket.readyState !== WebSocket.OPEN) {
|
|
@@ -576,7 +667,12 @@ export class RemoteToolOperations {
|
|
|
576
667
|
if (typeof parsed.id !== "string")
|
|
577
668
|
return;
|
|
578
669
|
if (typeof parsed.event === "string") {
|
|
579
|
-
|
|
670
|
+
if (parsed.event === "fileData" || parsed.event === "fileEnd" || parsed.event === "fileError") {
|
|
671
|
+
this.handleFileEvent(parsed);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
this.handleExecEvent(parsed);
|
|
675
|
+
}
|
|
580
676
|
return;
|
|
581
677
|
}
|
|
582
678
|
const pending = this.pending.get(parsed.id);
|
|
@@ -611,6 +707,26 @@ export class RemoteToolOperations {
|
|
|
611
707
|
const exitCode = optionalNumber(event.exitCode) ?? null;
|
|
612
708
|
pending.resolve({ exitCode });
|
|
613
709
|
}
|
|
710
|
+
handleFileEvent(event) {
|
|
711
|
+
const pending = this.fileDownloadPending.get(event.id);
|
|
712
|
+
if (!pending)
|
|
713
|
+
return;
|
|
714
|
+
if (event.event === "fileData") {
|
|
715
|
+
const encoded = optionalString(event.dataBase64);
|
|
716
|
+
if (encoded === undefined)
|
|
717
|
+
return;
|
|
718
|
+
pending.writePromise = pending.writePromise.then(() => writeStreamChunk(pending.stream, Buffer.from(encoded, "base64")));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.fileDownloadPending.delete(event.id);
|
|
722
|
+
if (event.event === "fileError") {
|
|
723
|
+
const error = new Error(remoteErrorMessage(event.error));
|
|
724
|
+
pending.reject(error);
|
|
725
|
+
pending.stream.destroy(error);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
pending.writePromise.then(() => endWriteStream(pending.stream)).then(pending.resolve, pending.reject);
|
|
729
|
+
}
|
|
614
730
|
async exec(command, options) {
|
|
615
731
|
const id = `remote-${this.nextId++}`;
|
|
616
732
|
let timeoutHandle;
|
|
@@ -684,6 +800,64 @@ export class RemoteToolOperations {
|
|
|
684
800
|
async writeFile(path, content) {
|
|
685
801
|
await this.request("writeFile", { path, contentBase64: Buffer.from(content).toString("base64") });
|
|
686
802
|
}
|
|
803
|
+
async uploadFile(sourcePath, destinationPath) {
|
|
804
|
+
const uploadId = `remote-${this.nextId++}`;
|
|
805
|
+
await new Promise((resolve, reject) => {
|
|
806
|
+
this.pending.set(uploadId, { resolve: () => resolve(), reject });
|
|
807
|
+
try {
|
|
808
|
+
this.send({ id: uploadId, method: "uploadFileStart", params: { path: destinationPath } });
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
this.pending.delete(uploadId);
|
|
812
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
try {
|
|
816
|
+
for await (const chunk of createReadStream(sourcePath, { highWaterMark: 64 * 1024 })) {
|
|
817
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
818
|
+
await this.request("uploadFileChunk", { uploadId, dataBase64: buffer.toString("base64") });
|
|
819
|
+
}
|
|
820
|
+
await this.request("uploadFileEnd", { uploadId });
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
await this.request("uploadFileCancel", { uploadId }).catch(() => undefined);
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async downloadFile(sourcePath, destinationPath) {
|
|
828
|
+
const id = `remote-${this.nextId++}`;
|
|
829
|
+
const stream = createWriteStream(destinationPath);
|
|
830
|
+
await new Promise((resolve, reject) => {
|
|
831
|
+
const cleanup = () => stream.off("error", onStreamError);
|
|
832
|
+
const onStreamError = (error) => {
|
|
833
|
+
cleanup();
|
|
834
|
+
this.fileDownloadPending.delete(id);
|
|
835
|
+
reject(error);
|
|
836
|
+
};
|
|
837
|
+
stream.once("error", onStreamError);
|
|
838
|
+
this.fileDownloadPending.set(id, {
|
|
839
|
+
stream,
|
|
840
|
+
writePromise: Promise.resolve(),
|
|
841
|
+
resolve: () => {
|
|
842
|
+
cleanup();
|
|
843
|
+
resolve();
|
|
844
|
+
},
|
|
845
|
+
reject: (error) => {
|
|
846
|
+
cleanup();
|
|
847
|
+
reject(error);
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
try {
|
|
851
|
+
this.send({ id, method: "downloadFile", params: { path: sourcePath } });
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
cleanup();
|
|
855
|
+
this.fileDownloadPending.delete(id);
|
|
856
|
+
stream.destroy();
|
|
857
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
687
861
|
async mkdir(path, options = {}) {
|
|
688
862
|
await this.request("mkdir", { path, recursive: options.recursive ?? false });
|
|
689
863
|
}
|