@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 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 { Stats } from "node:fs";
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":"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","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":"AACA,OAAO,EAAkD,KAAK,KAAK,EAAoB,MAAM,SAAS,CAAC;AAoBvG,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,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,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;AAiLD,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,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3E;IAEK,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;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,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3E;IAEK,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7E;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,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;IAEK,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7E;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;AA6CD,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,mBAAmB,CAQvB;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;IAYjB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,aAAa;IAiCrB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;IAqBjB,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,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3E;IAEK,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC7E;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 { 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
- this.handleExecEvent(parsed);
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
  }