dsh-remote-file-system 0.0.1 → 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/lib/index.js ADDED
@@ -0,0 +1,1002 @@
1
+ // src/remote-fs.ts
2
+ import {
3
+ FsError as FsError2,
4
+ FsTargetKey,
5
+ FsVersion
6
+ } from "@deepseek-ai/dsh-fs";
7
+
8
+ // src/protocol.ts
9
+ import { spawn } from "node:child_process";
10
+ import { FsError } from "@deepseek-ai/dsh-fs";
11
+ var ERR_PREFIX = "__ERR__";
12
+ function b64(input) {
13
+ return Buffer.from(input, "utf8").toString("base64");
14
+ }
15
+ var PATH_PROLOGUE = [
16
+ "set +e",
17
+ 'b64d() { printf %s "$1" | base64 -d 2>/dev/null; }',
18
+ 'P=$(b64d "$RM_PATH")',
19
+ 'case "$P" in "~"*) eval "P="$P"" ;; esac'
20
+ ].join("\n");
21
+ function envPrefix(env) {
22
+ return Object.entries(env).map(([k, v]) => k + "='" + v + "'").join(" ") + " bash -s";
23
+ }
24
+ async function sshExec(alias, env, scriptBody, opts = {}) {
25
+ const timeoutMs = opts.timeoutMs ?? 3e4;
26
+ return await new Promise((resolve, reject) => {
27
+ const child = spawn("ssh", [
28
+ "-o",
29
+ "BatchMode=yes",
30
+ "-o",
31
+ "ConnectTimeout=" + Math.max(1, Math.floor(timeoutMs / 1e3)),
32
+ alias,
33
+ envPrefix(env)
34
+ ], { stdio: ["pipe", "pipe", "pipe"] });
35
+ let stdout = "";
36
+ let stderr = "";
37
+ let timedOut = false;
38
+ let settled = false;
39
+ const timer = setTimeout(() => {
40
+ timedOut = true;
41
+ child.kill("SIGKILL");
42
+ }, timeoutMs);
43
+ child.stdout.setEncoding("utf8");
44
+ child.stderr.setEncoding("utf8");
45
+ child.stdout.on("data", (d) => {
46
+ stdout += d;
47
+ });
48
+ child.stderr.on("data", (d) => {
49
+ stderr += d;
50
+ });
51
+ child.on("error", (err) => {
52
+ if (settled) return;
53
+ settled = true;
54
+ clearTimeout(timer);
55
+ reject(new Error("ssh 连接失败:" + err.message));
56
+ });
57
+ child.on("close", (code) => {
58
+ if (settled) return;
59
+ settled = true;
60
+ clearTimeout(timer);
61
+ resolve({ exitCode: timedOut ? -1 : code, stdout, stderr });
62
+ });
63
+ child.stdin.write(scriptBody);
64
+ child.stdin.end();
65
+ });
66
+ }
67
+ async function sshOutcome(alias, env, scriptBody, opts = {}) {
68
+ const raw = await sshExec(alias, env, scriptBody, opts).catch((e) => {
69
+ throw new FsError(e.message, "FS_IO_ERROR");
70
+ });
71
+ if (raw.exitCode === -1) throw new FsError("远程执行超时", "FS_IO_ERROR");
72
+ if (raw.exitCode === 0) return { code: "OK", stdout: raw.stdout };
73
+ const line = raw.stderr.split("\n").find((l) => l.startsWith(ERR_PREFIX));
74
+ if (line) {
75
+ const rest = line.slice(ERR_PREFIX.length);
76
+ const sep = rest.indexOf("__");
77
+ const code = sep === -1 ? "FS_IO_ERROR" : rest.slice(0, sep);
78
+ const message = sep === -1 ? rest : rest.slice(sep + 2);
79
+ throw new FsError(message || "远程执行失败", code === "FS_IO_ERROR" ? "FS_IO_ERROR" : code);
80
+ }
81
+ const detail = raw.stderr.trim() || "远程执行失败且无输出";
82
+ throw new FsError(detail, "FS_IO_ERROR");
83
+ }
84
+ var DIE_HELPER = [
85
+ 'die() { local c="$1"; shift; local m="$*"; echo "__ERR__${c}__${m}" >&2; exit 1; }'
86
+ ].join("\n");
87
+ var B64_HELPER = [
88
+ 'b64out() { printf %s "$1" | base64 -w0 2>/dev/null; }'
89
+ ].join("\n");
90
+
91
+ // src/remote-fs.ts
92
+ var TAILS = /* @__PURE__ */ new Map();
93
+ async function withLock(key, fn) {
94
+ const prev = TAILS.get(key) ?? Promise.resolve();
95
+ const run = prev.then(() => fn());
96
+ TAILS.set(key, run.catch(() => {
97
+ }));
98
+ return run;
99
+ }
100
+ var ALIAS_RE = /^[A-Za-z0-9._-]+$/;
101
+ function splitAliasPath(filePath) {
102
+ const idx = filePath.indexOf(":");
103
+ if (idx <= 0) throw new FsError2("file_path 须为「别名:路径」形式,缺少别名前缀:" + filePath, "FS_IO_ERROR");
104
+ const alias = filePath.slice(0, idx);
105
+ const rawPath = filePath.slice(idx + 1);
106
+ if (!ALIAS_RE.test(alias)) throw new FsError2("非法的别名前缀:" + alias, "FS_IO_ERROR");
107
+ if (!(rawPath.startsWith("/") || rawPath.startsWith("~/"))) {
108
+ throw new FsError2("远程路径只接受绝对路径或 ~/ 开头,收到相对路径:" + rawPath, "FS_IO_ERROR");
109
+ }
110
+ return { alias, rawPath };
111
+ }
112
+ var RemoteFileSystem = class {
113
+ /** 单次读取字节上限,超限拒绝且不做流式,见 DESIGN §7 资源上限。 */
114
+ readMaxBytes = 50 * 1024 * 1024;
115
+ /** 解析「别名:路径」为稳定 FsTarget。targetKey 含 alias 与 rawPath,displayPath 即原名。 */
116
+ async resolve(path) {
117
+ const { alias, rawPath } = splitAliasPath(path);
118
+ const displayPath = alias + ":" + rawPath;
119
+ return { targetKey: FsTargetKey(alias + "::" + rawPath), displayPath };
120
+ }
121
+ /** FileSystem 同形占位:无进程内路径,返回 displayPath。 */
122
+ processPath(target) {
123
+ return target.displayPath;
124
+ }
125
+ /** stat:类型 + size + 版本依据,缺失返回 undefined。版本只作 host 进程内陈旧排序。 */
126
+ async stat(target) {
127
+ return withLock(target.targetKey, () => this.statLocked(target));
128
+ }
129
+ async statLocked(target) {
130
+ const { alias, rawPath } = splitAliasPath(target.displayPath);
131
+ const env = { RM_PATH: b64(rawPath) };
132
+ const action = [
133
+ PATH_PROLOGUE,
134
+ DIE_HELPER,
135
+ 'if [ ! -e "$P" ] && [ ! -L "$P" ]; then echo ABSENT; exit 0; fi',
136
+ 'if [ -L "$P" ]; then echo OTHER; exit 0; fi',
137
+ 'if [ -d "$P" ]; then echo DIR; exit 0; fi',
138
+ 'if [ ! -f "$P" ]; then echo OTHER; exit 0; fi',
139
+ 'SIZE=$(wc -c < "$P" 2>/dev/null | tr -d " ")',
140
+ 'MT=$(stat -c %Y "$P" 2>/dev/null || echo 0)',
141
+ 'echo "FILE:$SIZE:$MT"'
142
+ ].join("\n");
143
+ const out = await sshOutcome(alias, env, action);
144
+ const s = out.stdout.trim();
145
+ if (s === "ABSENT") return void 0;
146
+ if (s === "DIR") return { version: FsVersion("dir:" + Date.now()), type: "directory" };
147
+ if (s === "OTHER") return { version: FsVersion("other:" + Date.now()), type: "other" };
148
+ const m = /^FILE:(\d*):(\d*)$/.exec(s);
149
+ const size = m && m[1] !== "" ? Number(m[1]) : void 0;
150
+ const mtime = m && m[2] !== "" ? m[2] : "0";
151
+ return {
152
+ version: FsVersion("m:" + mtime + (size !== void 0 ? ":" + size : "")),
153
+ type: "file",
154
+ ...size !== void 0 ? { size } : {}
155
+ };
156
+ }
157
+ /** 读整文件 utf8。缺失归 FS_NOT_FOUND,非普通归 FS_NOT_REGULAR_FILE,超限归 FS_TOO_LARGE。 */
158
+ async readText(target) {
159
+ return withLock(target.targetKey, () => this.readTextLocked(target));
160
+ }
161
+ async readTextLocked(target) {
162
+ const { alias, rawPath } = splitAliasPath(target.displayPath);
163
+ const env = { RM_PATH: b64(rawPath), RM_MAX: String(this.readMaxBytes) };
164
+ const action = [
165
+ PATH_PROLOGUE,
166
+ DIE_HELPER,
167
+ 'if [ ! -e "$P" ] && [ ! -L "$P" ]; then die FS_NOT_FOUND 文件不存在; exit 0; fi',
168
+ 'if [ -d "$P" ]; then die FS_NOT_REGULAR_FILE 目标是一个目录; exit 0; fi',
169
+ 'if [ ! -f "$P" ]; then die FS_NOT_REGULAR_FILE 目标不是普通文件; exit 0; fi',
170
+ 'SIZE=$(wc -c < "$P" 2>/dev/null | tr -d " ")',
171
+ 'if [ -n "$SIZE" ] && [ "$SIZE" -gt "$RM_MAX" ] 2>/dev/null; then die FS_TOO_LARGE 文件超过单次读取上限,不做流式; exit 0; fi',
172
+ 'base64 -w0 < "$P" 2>/dev/null || die FS_IO_ERROR 读取内容失败'
173
+ ].join("\n");
174
+ const out = await sshOutcome(alias, env, action);
175
+ return Buffer.from(out.stdout.replace(/\s+$/u, ""), "base64").toString("utf8");
176
+ }
177
+ /** readBytes:raw 读取,文本只读,返回 utf8 字节。 */
178
+ async readBytes(target, maxBytes) {
179
+ const text = await this.readText(target);
180
+ const buf = Buffer.from(text, "utf8");
181
+ if (buf.byteLength > maxBytes) throw new FsError2("超出字节上限", "FS_TOO_LARGE");
182
+ return buf;
183
+ }
184
+ /**
185
+ * 全文件写,恒用 createIfAbsent,只新建。目标已存在即 FS_NOT_OBSERVED,绝无覆写。
186
+ * 只接受 createIfAbsent 意图,其他拒绝。
187
+ */
188
+ async writeText(target, content, expected) {
189
+ return withLock(target.targetKey, () => this.writeTextLocked(target, content, expected));
190
+ }
191
+ async writeTextLocked(target, content, expected) {
192
+ const { alias, rawPath } = splitAliasPath(target.displayPath);
193
+ if (expected !== void 0 && expected.kind !== "createIfAbsent") {
194
+ throw new FsError2("write_remote 只允许新建,仅支持 createIfAbsent 意图", "FS_NOT_OBSERVED");
195
+ }
196
+ const env = { RM_PATH: b64(rawPath), RM_DATA: b64(content) };
197
+ const action = [
198
+ PATH_PROLOGUE,
199
+ DIE_HELPER,
200
+ 'TMP=$(mktemp "$P.XXXXXXXX.tmp" 2>/dev/null)',
201
+ 'if [ -z "$TMP" ]; then die FS_IO_ERROR 无法创建临时文件,目录可能不可写; exit 0; fi',
202
+ 'printf %s "$(b64d "$RM_DATA")" > "$TMP" 2>/dev/null || { rm -f "$TMP"; die FS_IO_ERROR 写入临时文件失败; exit 0; }',
203
+ 'if [ -e "$P" ] || [ -L "$P" ]; then rm -f "$TMP"; die FS_NOT_OBSERVED 文件已存在,write_remote 只允许新建; exit 0; fi',
204
+ 'mv -f "$TMP" "$P" 2>/dev/null || { rm -f "$TMP"; die FS_IO_ERROR mv 发布失败; exit 0; }',
205
+ "echo OK"
206
+ ].join("\n");
207
+ await sshOutcome(alias, env, action);
208
+ return { operation: "create", version: FsVersion("w:" + Date.now()), before: null, after: content };
209
+ }
210
+ /** 编辑后写回,整块替换。 */
211
+ async writeReplaced(target, content) {
212
+ const { alias, rawPath } = splitAliasPath(target.displayPath);
213
+ const env = { RM_PATH: b64(rawPath), RM_DATA: b64(content) };
214
+ const action = [
215
+ PATH_PROLOGUE,
216
+ DIE_HELPER,
217
+ 'TMP=$(mktemp "$P.XXXXXXXX.tmp" 2>/dev/null)',
218
+ 'if [ -z "$TMP" ]; then die FS_IO_ERROR 无法创建临时文件; exit 0; fi',
219
+ 'printf %s "$(b64d "$RM_DATA")" > "$TMP" 2>/dev/null || { rm -f "$TMP"; die FS_IO_ERROR 写入临时文件失败; exit 0; }',
220
+ 'if [ ! -e "$P" ]; then rm -f "$TMP"; die FS_NOT_FOUND 文件已不存在; exit 0; fi',
221
+ 'mv -f "$TMP" "$P" 2>/dev/null || { rm -f "$TMP"; die FS_IO_ERROR mv 替换失败; exit 0; }',
222
+ "echo OK"
223
+ ].join("\n");
224
+ await sshOutcome(alias, env, action);
225
+ return content;
226
+ }
227
+ /**
228
+ * 字面编辑:读原文 → host 内唯一匹配或全替换 → 写回。匹配类错误走原生词表,即 FS_AMBIGUOUS_EDIT 与 FS_EDIT_NOT_FOUND;远端失败归远程失败类。
229
+ * 整块读改写,保留远端换行。
230
+ */
231
+ async editText(target, edit, _expected) {
232
+ return withLock(target.targetKey, async () => {
233
+ const { alias, rawPath } = splitAliasPath(target.displayPath);
234
+ const readTarget = { targetKey: target.targetKey, displayPath: alias + ":" + rawPath };
235
+ const before = await this.readTextLocked(readTarget);
236
+ const matches = this.countMatches(before, edit.oldString);
237
+ if (matches === 0) throw new FsError2("old_string 未命中任何匹配", "FS_EDIT_NOT_FOUND");
238
+ if (!edit.replaceAll && matches > 1) {
239
+ throw new FsError2("old_string 出现 " + matches + " 次,需更精确或设 replace_all", "FS_AMBIGUOUS_EDIT");
240
+ }
241
+ const after = edit.replaceAll ? before.split(edit.oldString).join(edit.newString) : before.replace(edit.oldString, edit.newString);
242
+ await this.writeReplaced(readTarget, after);
243
+ return { version: FsVersion("e:" + Date.now()), before, after };
244
+ });
245
+ }
246
+ countMatches(text, oldString) {
247
+ if (oldString === "") return 0;
248
+ let count = 0;
249
+ let idx = text.indexOf(oldString);
250
+ while (idx !== -1) {
251
+ count++;
252
+ idx = text.indexOf(oldString, idx + oldString.length);
253
+ }
254
+ return count;
255
+ }
256
+ };
257
+
258
+ // src/read.ts
259
+ import { defineTool } from "@deepseek-ai/dsh-tools";
260
+ import { FsError as FsError4 } from "@deepseek-ai/dsh-fs";
261
+
262
+ // src/render.ts
263
+ import { FsError as FsError3 } from "@deepseek-ai/dsh-fs";
264
+
265
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/base.js
266
+ var Diff = class {
267
+ diff(oldStr, newStr, options = {}) {
268
+ let callback;
269
+ if (typeof options === "function") {
270
+ callback = options;
271
+ options = {};
272
+ } else if ("callback" in options) {
273
+ callback = options.callback;
274
+ }
275
+ const oldString = this.castInput(oldStr, options);
276
+ const newString = this.castInput(newStr, options);
277
+ const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
278
+ const newTokens = this.removeEmpty(this.tokenize(newString, options));
279
+ return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
280
+ }
281
+ diffWithOptionsObj(oldTokens, newTokens, options, callback) {
282
+ var _a;
283
+ const done = (value) => {
284
+ value = this.postProcess(value, options);
285
+ if (callback) {
286
+ setTimeout(function() {
287
+ callback(value);
288
+ }, 0);
289
+ return void 0;
290
+ } else {
291
+ return value;
292
+ }
293
+ };
294
+ const newLen = newTokens.length, oldLen = oldTokens.length;
295
+ let editLength = 1;
296
+ let maxEditLength = newLen + oldLen;
297
+ if (options.maxEditLength != null) {
298
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
299
+ }
300
+ const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
301
+ const abortAfterTimestamp = Date.now() + maxExecutionTime;
302
+ const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
303
+ let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
304
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
305
+ return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
306
+ }
307
+ let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
308
+ const execEditLength = () => {
309
+ for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
310
+ let basePath;
311
+ const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
312
+ if (removePath) {
313
+ bestPath[diagonalPath - 1] = void 0;
314
+ }
315
+ let canAdd = false;
316
+ if (addPath) {
317
+ const addPathNewPos = addPath.oldPos - diagonalPath;
318
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
319
+ }
320
+ const canRemove = removePath && removePath.oldPos + 1 < oldLen;
321
+ if (!canAdd && !canRemove) {
322
+ bestPath[diagonalPath] = void 0;
323
+ continue;
324
+ }
325
+ if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
326
+ basePath = this.addToPath(addPath, true, false, 0, options);
327
+ } else {
328
+ basePath = this.addToPath(removePath, false, true, 1, options);
329
+ }
330
+ newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
331
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
332
+ return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
333
+ } else {
334
+ bestPath[diagonalPath] = basePath;
335
+ if (basePath.oldPos + 1 >= oldLen) {
336
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
337
+ }
338
+ if (newPos + 1 >= newLen) {
339
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
340
+ }
341
+ }
342
+ }
343
+ editLength++;
344
+ };
345
+ if (callback) {
346
+ (function exec() {
347
+ setTimeout(function() {
348
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
349
+ return callback(void 0);
350
+ }
351
+ if (!execEditLength()) {
352
+ exec();
353
+ }
354
+ }, 0);
355
+ })();
356
+ } else {
357
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
358
+ const ret = execEditLength();
359
+ if (ret) {
360
+ return ret;
361
+ }
362
+ }
363
+ }
364
+ }
365
+ addToPath(path, added, removed, oldPosInc, options) {
366
+ const last = path.lastComponent;
367
+ if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
368
+ return {
369
+ oldPos: path.oldPos + oldPosInc,
370
+ lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
371
+ };
372
+ } else {
373
+ return {
374
+ oldPos: path.oldPos + oldPosInc,
375
+ lastComponent: { count: 1, added, removed, previousComponent: last }
376
+ };
377
+ }
378
+ }
379
+ extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
380
+ const newLen = newTokens.length, oldLen = oldTokens.length;
381
+ let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
382
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
383
+ newPos++;
384
+ oldPos++;
385
+ commonCount++;
386
+ if (options.oneChangePerToken) {
387
+ basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
388
+ }
389
+ }
390
+ if (commonCount && !options.oneChangePerToken) {
391
+ basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
392
+ }
393
+ basePath.oldPos = oldPos;
394
+ return newPos;
395
+ }
396
+ equals(left, right, options) {
397
+ if (options.comparator) {
398
+ return options.comparator(left, right);
399
+ } else {
400
+ return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
401
+ }
402
+ }
403
+ removeEmpty(array) {
404
+ const ret = [];
405
+ for (let i = 0; i < array.length; i++) {
406
+ if (array[i]) {
407
+ ret.push(array[i]);
408
+ }
409
+ }
410
+ return ret;
411
+ }
412
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
413
+ castInput(value, options) {
414
+ return value;
415
+ }
416
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
417
+ tokenize(value, options) {
418
+ return Array.from(value);
419
+ }
420
+ join(chars) {
421
+ return chars.join("");
422
+ }
423
+ postProcess(changeObjects, options) {
424
+ return changeObjects;
425
+ }
426
+ get useLongestToken() {
427
+ return false;
428
+ }
429
+ buildValues(lastComponent, newTokens, oldTokens) {
430
+ const components = [];
431
+ let nextComponent;
432
+ while (lastComponent) {
433
+ components.push(lastComponent);
434
+ nextComponent = lastComponent.previousComponent;
435
+ delete lastComponent.previousComponent;
436
+ lastComponent = nextComponent;
437
+ }
438
+ components.reverse();
439
+ const componentLen = components.length;
440
+ let componentPos = 0, newPos = 0, oldPos = 0;
441
+ for (; componentPos < componentLen; componentPos++) {
442
+ const component = components[componentPos];
443
+ if (!component.removed) {
444
+ if (!component.added && this.useLongestToken) {
445
+ let value = newTokens.slice(newPos, newPos + component.count);
446
+ value = value.map(function(value2, i) {
447
+ const oldValue = oldTokens[oldPos + i];
448
+ return oldValue.length > value2.length ? oldValue : value2;
449
+ });
450
+ component.value = this.join(value);
451
+ } else {
452
+ component.value = this.join(newTokens.slice(newPos, newPos + component.count));
453
+ }
454
+ newPos += component.count;
455
+ if (!component.added) {
456
+ oldPos += component.count;
457
+ }
458
+ } else {
459
+ component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
460
+ oldPos += component.count;
461
+ }
462
+ }
463
+ return components;
464
+ }
465
+ };
466
+
467
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/line.js
468
+ var LineDiff = class extends Diff {
469
+ constructor() {
470
+ super(...arguments);
471
+ this.tokenize = tokenize;
472
+ }
473
+ equals(left, right, options) {
474
+ if (options.ignoreWhitespace) {
475
+ if (!options.newlineIsToken || !left.includes("\n")) {
476
+ left = left.trim();
477
+ }
478
+ if (!options.newlineIsToken || !right.includes("\n")) {
479
+ right = right.trim();
480
+ }
481
+ } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
482
+ if (left.endsWith("\n")) {
483
+ left = left.slice(0, -1);
484
+ }
485
+ if (right.endsWith("\n")) {
486
+ right = right.slice(0, -1);
487
+ }
488
+ }
489
+ return super.equals(left, right, options);
490
+ }
491
+ };
492
+ var lineDiff = new LineDiff();
493
+ function diffLines(oldStr, newStr, options) {
494
+ return lineDiff.diff(oldStr, newStr, options);
495
+ }
496
+ function tokenize(value, options) {
497
+ if (options.stripTrailingCr) {
498
+ value = value.replace(/\r\n/g, "\n");
499
+ }
500
+ const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
501
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
502
+ linesAndNewlines.pop();
503
+ }
504
+ for (let i = 0; i < linesAndNewlines.length; i++) {
505
+ const line = linesAndNewlines[i];
506
+ if (i % 2 && !options.newlineIsToken) {
507
+ retLines[retLines.length - 1] += line;
508
+ } else {
509
+ retLines.push(line);
510
+ }
511
+ }
512
+ return retLines;
513
+ }
514
+
515
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/patch/create.js
516
+ function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
517
+ let optionsObj;
518
+ if (!options) {
519
+ optionsObj = {};
520
+ } else if (typeof options === "function") {
521
+ optionsObj = { callback: options };
522
+ } else {
523
+ optionsObj = options;
524
+ }
525
+ if (typeof optionsObj.context === "undefined") {
526
+ optionsObj.context = 4;
527
+ }
528
+ const context = optionsObj.context;
529
+ if (optionsObj.newlineIsToken) {
530
+ throw new Error("newlineIsToken may not be used with patch-generation functions, only with diffing functions");
531
+ }
532
+ if (!optionsObj.callback) {
533
+ return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj));
534
+ } else {
535
+ const { callback } = optionsObj;
536
+ diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => {
537
+ const patch = diffLinesResultToPatch(diff);
538
+ callback(patch);
539
+ } }));
540
+ }
541
+ function diffLinesResultToPatch(diff) {
542
+ if (!diff) {
543
+ return;
544
+ }
545
+ diff.push({ value: "", lines: [] });
546
+ function contextLines(lines) {
547
+ return lines.map(function(entry) {
548
+ return " " + entry;
549
+ });
550
+ }
551
+ const hunks = [];
552
+ let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1;
553
+ for (let i = 0; i < diff.length; i++) {
554
+ const current = diff[i], lines = current.lines || splitLines(current.value);
555
+ current.lines = lines;
556
+ if (current.added || current.removed) {
557
+ if (!oldRangeStart) {
558
+ const prev = diff[i - 1];
559
+ oldRangeStart = oldLine;
560
+ newRangeStart = newLine;
561
+ if (prev) {
562
+ curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
563
+ oldRangeStart -= curRange.length;
564
+ newRangeStart -= curRange.length;
565
+ }
566
+ }
567
+ for (const line of lines) {
568
+ curRange.push((current.added ? "+" : "-") + line);
569
+ }
570
+ if (current.added) {
571
+ newLine += lines.length;
572
+ } else {
573
+ oldLine += lines.length;
574
+ }
575
+ } else {
576
+ if (oldRangeStart) {
577
+ if (lines.length <= context * 2 && i < diff.length - 2) {
578
+ for (const line of contextLines(lines)) {
579
+ curRange.push(line);
580
+ }
581
+ } else {
582
+ const contextSize = Math.min(lines.length, context);
583
+ for (const line of contextLines(lines.slice(0, contextSize))) {
584
+ curRange.push(line);
585
+ }
586
+ const hunk = {
587
+ oldStart: oldRangeStart,
588
+ oldLines: oldLine - oldRangeStart + contextSize,
589
+ newStart: newRangeStart,
590
+ newLines: newLine - newRangeStart + contextSize,
591
+ lines: curRange
592
+ };
593
+ hunks.push(hunk);
594
+ oldRangeStart = 0;
595
+ newRangeStart = 0;
596
+ curRange = [];
597
+ }
598
+ }
599
+ oldLine += lines.length;
600
+ newLine += lines.length;
601
+ }
602
+ }
603
+ for (const hunk of hunks) {
604
+ for (let i = 0; i < hunk.lines.length; i++) {
605
+ if (hunk.lines[i].endsWith("\n")) {
606
+ hunk.lines[i] = hunk.lines[i].slice(0, -1);
607
+ } else {
608
+ hunk.lines.splice(i + 1, 0, "\");
609
+ i++;
610
+ }
611
+ }
612
+ }
613
+ return {
614
+ oldFileName,
615
+ newFileName,
616
+ oldHeader,
617
+ newHeader,
618
+ hunks
619
+ };
620
+ }
621
+ }
622
+ function splitLines(text) {
623
+ const hasTrailingNl = text.endsWith("\n");
624
+ const result = text.split("\n").map((line) => line + "\n");
625
+ if (hasTrailingNl) {
626
+ result.pop();
627
+ } else {
628
+ result.push(result.pop().slice(0, -1));
629
+ }
630
+ return result;
631
+ }
632
+
633
+ // src/render.ts
634
+ function truncateLine(line, maxLineLength) {
635
+ return line.length > maxLineLength ? line.substring(0, maxLineLength) + "... (line truncated to " + maxLineLength + " chars)" : line;
636
+ }
637
+ function stripCarriageReturn(line) {
638
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
639
+ }
640
+ function buildWindow(text, request, displayPath) {
641
+ const linesOut = [];
642
+ let totalLines = 0;
643
+ let truncatedByBytes = false;
644
+ let outputBytes = 0;
645
+ const parts = text.split("\n");
646
+ const hasTrailingNewline = text.endsWith("\n");
647
+ const meaningful = hasTrailingNewline ? parts.slice(0, -1) : parts;
648
+ if (text.length === 0) {
649
+ return { lines: [], totalLines: 0, truncatedByBytes: false };
650
+ }
651
+ totalLines = meaningful.length;
652
+ for (let i = 0; i < meaningful.length; i += 1) {
653
+ const raw = meaningful[i];
654
+ if (truncatedByBytes) break;
655
+ const lineNumber = i + 1;
656
+ if (lineNumber < request.offset || linesOut.length >= request.limit) continue;
657
+ const shown = truncateLine(stripCarriageReturn(raw), request.maxLineLength);
658
+ const bytes = Buffer.byteLength(shown, "utf8") + (linesOut.length > 0 ? 1 : 0);
659
+ if (outputBytes + bytes > request.maxBytes) {
660
+ truncatedByBytes = true;
661
+ break;
662
+ }
663
+ outputBytes += bytes;
664
+ linesOut.push({ number: lineNumber, text: shown });
665
+ }
666
+ if (!truncatedByBytes && request.offset > totalLines && !(totalLines === 0 && request.offset === 1)) {
667
+ throw new FsError3("offset " + request.offset + ' is out of range for "' + displayPath + '" (' + totalLines + " lines)", "FS_NOT_FOUND");
668
+ }
669
+ return { lines: linesOut, totalLines, truncatedByBytes };
670
+ }
671
+ function formatReadOutput(displayPath, outcome) {
672
+ const endLine = outcome.lines.at(-1)?.number ?? Math.max(0, outcome.offset - 1);
673
+ let footer;
674
+ if (outcome.truncatedByBytes) {
675
+ footer = "(Output capped. Showing lines " + outcome.offset + "-" + endLine + ". Use offset=" + (endLine + 1) + " to continue.)";
676
+ } else if (endLine < outcome.totalLines) {
677
+ footer = "(Showing lines " + outcome.offset + "-" + endLine + " of " + outcome.totalLines + ". Use offset=" + (endLine + 1) + " to continue.)";
678
+ } else {
679
+ footer = "(End of file - total " + outcome.totalLines + " lines)";
680
+ }
681
+ const body = outcome.lines.length > 0 ? outcome.lines.map((line) => line.number + ": " + line.text).join("\n") + "\n\n" + footer : footer;
682
+ return "<path>" + displayPath + "</path>\n<type>file</type>\n<content>\n" + body + "\n</content>";
683
+ }
684
+ var LANG_BY_EXTENSION = {
685
+ ts: "ts",
686
+ tsx: "tsx",
687
+ mts: "ts",
688
+ cts: "ts",
689
+ js: "js",
690
+ jsx: "jsx",
691
+ mjs: "js",
692
+ cjs: "js",
693
+ json: "json",
694
+ jsonc: "json",
695
+ py: "py",
696
+ rb: "rb",
697
+ go: "go",
698
+ rs: "rs",
699
+ java: "java",
700
+ c: "c",
701
+ h: "c",
702
+ cc: "cpp",
703
+ cpp: "cpp",
704
+ hpp: "cpp",
705
+ cxx: "cpp",
706
+ cs: "cs",
707
+ kt: "kotlin",
708
+ swift: "swift",
709
+ php: "php",
710
+ sh: "sh",
711
+ bash: "sh",
712
+ zsh: "sh",
713
+ yaml: "yaml",
714
+ yml: "yaml",
715
+ toml: "toml",
716
+ ini: "ini",
717
+ md: "md",
718
+ markdown: "md",
719
+ mdx: "mdx",
720
+ html: "html",
721
+ htm: "html",
722
+ css: "css",
723
+ scss: "scss",
724
+ less: "less",
725
+ sql: "sql",
726
+ xml: "xml",
727
+ lua: "lua"
728
+ };
729
+ function langFromPath(path) {
730
+ const base = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
731
+ const dot = base.lastIndexOf(".");
732
+ if (dot <= 0) return void 0;
733
+ const ext = base.slice(dot + 1).toLowerCase();
734
+ return Object.hasOwn(LANG_BY_EXTENSION, ext) ? LANG_BY_EXTENSION[ext] : void 0;
735
+ }
736
+ function readMetaFromMeta(meta) {
737
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
738
+ const m = meta;
739
+ const { path, offset, lines, totalLines, lang } = m;
740
+ if (typeof path !== "string" || typeof totalLines !== "number" || typeof offset !== "number") return void 0;
741
+ if (!Number.isInteger(offset) || offset < 1) return void 0;
742
+ if (!Number.isInteger(totalLines) || totalLines < 0) return void 0;
743
+ if (!Array.isArray(lines) || !lines.every((l) => l !== null && typeof l === "object" && !Array.isArray(l) && typeof l.number === "number" && Number.isInteger(l.number) && l.number >= 1 && typeof l.text === "string")) return void 0;
744
+ if (lang !== void 0 && typeof lang !== "string") return void 0;
745
+ const typed = lines;
746
+ let prev = offset - 1;
747
+ for (const { number } of typed) {
748
+ if (number <= prev || number > totalLines) return void 0;
749
+ prev = number;
750
+ }
751
+ return { path, offset, lines: typed, totalLines, ...lang === void 0 ? {} : { lang } };
752
+ }
753
+ function computeHunkDiffs(path, before, after) {
754
+ const patch = structuredPatch("", "", before, after, void 0, void 0, { context: 3 });
755
+ const diffs = [];
756
+ for (const hunk of patch.hunks) {
757
+ const oldLines = [];
758
+ const newLines = [];
759
+ for (const line of hunk.lines) {
760
+ if (line.startsWith("\\")) continue;
761
+ const text = line.slice(1);
762
+ if (line.startsWith("-")) oldLines.push(text);
763
+ else if (line.startsWith("+")) newLines.push(text);
764
+ else {
765
+ oldLines.push(text);
766
+ newLines.push(text);
767
+ }
768
+ }
769
+ diffs.push({ path, oldText: oldLines.length > 0 ? oldLines.join("\n") : null, newText: newLines.join("\n") });
770
+ }
771
+ return diffs;
772
+ }
773
+ function diffsFromMeta(meta) {
774
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
775
+ const diffs = meta.diffs;
776
+ if (!Array.isArray(diffs) || diffs.length === 0) return void 0;
777
+ const valid = diffs.every((d) => d !== null && typeof d === "object" && !Array.isArray(d) && typeof d.path === "string" && (d.oldText === null || typeof d.oldText === "string") && typeof d.newText === "string");
778
+ return valid ? diffs : void 0;
779
+ }
780
+ function formatWriteOutput(displayPath, operation) {
781
+ const verb = operation === "create" ? "Created" : "Updated";
782
+ return "<path>" + displayPath + "</path>\n<type>file</type>\n<content>\n" + verb + " file\n</content>";
783
+ }
784
+ function formatEditOutput(displayPath, replaceAll) {
785
+ return replaceAll ? "The file " + displayPath + " has been updated. All occurrences were successfully replaced." : "The file " + displayPath + " has been updated successfully.";
786
+ }
787
+
788
+ // src/read.ts
789
+ var READ_LIMIT = 2e3;
790
+ var READ_MAX_LINE_LENGTH = 2e3;
791
+ var READ_MAX_BYTES = 50 * 1024;
792
+ function parsePositiveInteger(value, name2) {
793
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
794
+ throw new Error(name2 + " must be a positive integer");
795
+ }
796
+ return value;
797
+ }
798
+ function parseReadArgs(args) {
799
+ if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
800
+ const offset = args.offset === void 0 ? 1 : parsePositiveInteger(args.offset, "offset");
801
+ const limit = args.limit === void 0 ? READ_LIMIT : parsePositiveInteger(args.limit, "limit");
802
+ if (limit > READ_LIMIT) throw new Error("limit must be less than or equal to " + READ_LIMIT);
803
+ return { filePath: args.file_path, offset, limit };
804
+ }
805
+ function createReadRemoteTool(fs) {
806
+ return defineTool({
807
+ name: "read_remote",
808
+ description: "Read a UTF-8 text file on a remote host over ssh and return line-numbered content.",
809
+ parameters: {
810
+ file_path: { type: "string", required: true, description: "Remote target and path in one: alias:path, e.g. wsl:~/x.md or myserver:/etc/hosts. Alias comes from your ssh config." },
811
+ offset: { type: "number", description: "1-based first line to return. Defaults to 1." },
812
+ limit: { type: "number", description: "Maximum number of lines to return. Defaults to " + READ_LIMIT + "." }
813
+ },
814
+ output: {
815
+ schema: {
816
+ type: "object",
817
+ additionalProperties: false,
818
+ properties: {
819
+ path: { type: "string", required: true },
820
+ offset: { type: "integer", required: true },
821
+ lines: {
822
+ type: "array",
823
+ required: true,
824
+ items: { type: "object", additionalProperties: false, properties: { number: { type: "integer", required: true }, text: { type: "string", required: true } } }
825
+ },
826
+ totalLines: { type: "integer", required: true }
827
+ }
828
+ },
829
+ render: (args, value) => {
830
+ const input = parseReadArgs(args);
831
+ const endLine = value.lines.at(-1)?.number ?? Math.max(0, value.offset - 1);
832
+ const truncatedByBytes = value.lines.length < input.limit && endLine < value.totalLines;
833
+ return [{ type: "text", text: formatReadOutput(value.path, { offset: value.offset, lines: value.lines, totalLines: value.totalLines, ...truncatedByBytes ? { truncatedByBytes: true } : {} }) }];
834
+ },
835
+ presentationMeta: (_args, value) => {
836
+ const lang = langFromPath(value.path);
837
+ return { path: value.path, offset: value.offset, lines: value.lines.map(({ number, text }) => ({ number, text })), totalLines: value.totalLines, ...lang === void 0 ? {} : { lang } };
838
+ }
839
+ },
840
+ isConcurrencySafe: () => true,
841
+ async execute(args, exec) {
842
+ const input = parseReadArgs(args);
843
+ if (exec.signal.aborted) throw new FsError4("cancelled", "FS_ABORTED");
844
+ const target = await fs.resolve(input.filePath);
845
+ const text = await fs.readText(target);
846
+ const window = buildWindow(text, { offset: input.offset, limit: input.limit, maxLineLength: READ_MAX_LINE_LENGTH, maxBytes: READ_MAX_BYTES }, target.displayPath);
847
+ return { path: target.displayPath, offset: input.offset, lines: window.lines, totalLines: window.totalLines };
848
+ },
849
+ presentResult(_args, result) {
850
+ if (result.isError) return void 0;
851
+ const meta = readMetaFromMeta(result.meta);
852
+ if (meta === void 0) return void 0;
853
+ const only = result.content.length === 1 ? result.content[0] : void 0;
854
+ const text = only?.type === "text" ? only.text : void 0;
855
+ if (text === void 0) return void 0;
856
+ const body = /^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u.exec(text)?.[1];
857
+ if (body === void 0) return void 0;
858
+ return { card: "read", path: meta.path, offset: meta.offset, lines: meta.lines, totalLines: meta.totalLines, ...meta.lang === void 0 ? {} : { lang: meta.lang }, content: [{ type: "text", text: body }] };
859
+ },
860
+ presentCall(args) {
861
+ const a = args;
862
+ const windowText = a.limit !== void 0 && a.limit > 0 ? " (" + (a.offset ?? 1) + " - " + ((a.offset ?? 1) + a.limit - 1) + ")" : a.offset !== void 0 ? " (from line " + a.offset + ")" : "";
863
+ return { card: "generic", title: "Read " + a.file_path + windowText, kind: "read", locations: [{ path: a.file_path, line: a.offset ?? 1 }] };
864
+ }
865
+ });
866
+ }
867
+
868
+ // src/write.ts
869
+ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
870
+ import { FsError as FsError5 } from "@deepseek-ai/dsh-fs";
871
+ function parseWriteArgs(args) {
872
+ if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
873
+ return { filePath: args.file_path, content: args.content };
874
+ }
875
+ function createWriteRemoteTool(fs) {
876
+ return defineTool2({
877
+ name: "write_remote",
878
+ description: "Create a new UTF-8 text file on a remote host over ssh. The target must NOT already exist — write_remote only creates new files, never overwrites.",
879
+ parameters: {
880
+ file_path: { type: "string", required: true, description: "Remote target and path in one: alias:path, e.g. wsl:~/x.md or myserver:/etc/hosts. Must not already exist." },
881
+ content: { type: "string", required: true, description: "Full UTF-8 text content to write." }
882
+ },
883
+ output: {
884
+ schema: {
885
+ type: "object",
886
+ additionalProperties: false,
887
+ properties: {
888
+ path: { type: "string", required: true },
889
+ operation: { type: "string", required: true, enum: ["create", "update"] },
890
+ before: { required: true, oneOf: [{ type: "string" }, { type: "null" }] },
891
+ after: { type: "string", required: true }
892
+ }
893
+ },
894
+ render: (_args, value) => [{ type: "text", text: formatWriteOutput(value.path, value.operation) }],
895
+ presentationMeta: (args, value) => ({ diffs: value.before === null ? [] : computeHunkDiffs(args.file_path, value.before, value.after).map(({ path, oldText, newText }) => ({ path, oldText, newText })) })
896
+ },
897
+ isConcurrencySafe: () => true,
898
+ async execute(args, exec) {
899
+ const input = parseWriteArgs(args);
900
+ if (exec.signal.aborted) throw new FsError5("cancelled", "FS_ABORTED");
901
+ const target = await fs.resolve(input.filePath);
902
+ const outcome = await fs.writeText(target, input.content, { kind: "createIfAbsent" });
903
+ return { path: target.displayPath, operation: outcome.operation, before: outcome.before, after: outcome.after };
904
+ },
905
+ presentCall(args) {
906
+ const a = args;
907
+ return { card: "diff", title: "Write " + a.file_path, diffs: [{ path: a.file_path, oldText: null, newText: a.content }], locations: [{ path: a.file_path }] };
908
+ },
909
+ presentResult(args, result) {
910
+ if (result.isError) return void 0;
911
+ const a = args;
912
+ const diffs = diffsFromMeta(result.meta) ?? [{ path: a.file_path, oldText: null, newText: a.content }];
913
+ return { card: "diff", title: "Write " + a.file_path, diffs };
914
+ }
915
+ });
916
+ }
917
+
918
+ // src/edit.ts
919
+ import { defineTool as defineTool3 } from "@deepseek-ai/dsh-tools";
920
+ import { FsError as FsError6 } from "@deepseek-ai/dsh-fs";
921
+ function parseEditArgs(args) {
922
+ if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
923
+ if (args.old_string.length === 0) throw new Error("old_string must be a non-empty string");
924
+ if (args.old_string === args.new_string) throw new Error("old_string and new_string must differ");
925
+ return { filePath: args.file_path, oldString: args.old_string, newString: args.new_string, replaceAll: args.replace_all ?? false };
926
+ }
927
+ function createEditRemoteTool(fs) {
928
+ return defineTool3({
929
+ name: "edit_remote",
930
+ description: "Edit an existing UTF-8 text file on a remote host over ssh by replacing literal text.",
931
+ parameters: {
932
+ file_path: { type: "string", required: true, description: "Remote target and path in one: alias:path, e.g. wsl:~/x.md or myserver:/etc/hosts." },
933
+ old_string: { type: "string", required: true, description: "Literal text to replace. Must match exactly." },
934
+ new_string: { type: "string", required: true, description: "Literal replacement text. Use an empty string to delete the match." },
935
+ replace_all: { type: "boolean", description: "Replace all matches. Defaults to false; when false, old_string must appear exactly once." }
936
+ },
937
+ output: {
938
+ schema: {
939
+ type: "object",
940
+ additionalProperties: false,
941
+ properties: {
942
+ path: { type: "string", required: true },
943
+ before: { type: "string", required: true },
944
+ after: { type: "string", required: true }
945
+ }
946
+ },
947
+ render: (args, value) => [{ type: "text", text: formatEditOutput(value.path, args.replace_all ?? false) }],
948
+ presentationMeta: (args, value) => ({ diffs: computeHunkDiffs(args.file_path, value.before, value.after).map(({ path, oldText, newText }) => ({ path, oldText, newText })) })
949
+ },
950
+ isConcurrencySafe: () => false,
951
+ async execute(args, exec) {
952
+ const input = parseEditArgs(args);
953
+ if (exec.signal.aborted) throw new FsError6("cancelled", "FS_ABORTED");
954
+ const target = await fs.resolve(input.filePath);
955
+ const outcome = await fs.editText(target, { oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll });
956
+ return { path: target.displayPath, before: outcome.before, after: outcome.after };
957
+ },
958
+ presentCall(args) {
959
+ const a = args;
960
+ return { card: "diff", title: "Edit " + a.file_path, diffs: [{ path: a.file_path, oldText: a.old_string || null, newText: a.new_string }], locations: [{ path: a.file_path }] };
961
+ },
962
+ presentResult(args, result) {
963
+ if (result.isError) return void 0;
964
+ const a = args;
965
+ const diffs = diffsFromMeta(result.meta);
966
+ if (diffs === void 0) return void 0;
967
+ return { card: "diff", title: "Edit " + a.file_path, diffs };
968
+ }
969
+ });
970
+ }
971
+
972
+ // src/index.ts
973
+ var name = "dsh-remote-file-system";
974
+ var inject = ["tools", "systemPrompt"];
975
+ function apply(ctx, config = {}) {
976
+ if (config.enabled === false) return;
977
+ const fs = new RemoteFileSystem();
978
+ ctx.systemPrompt.section({
979
+ name: "tool:read_remote",
980
+ order: ctx.systemPrompt.getSectionOrder("TOOL_READ"),
981
+ text: "Use read_remote (not shell commands) to inspect UTF-8 text files on remote hosts over ssh. file_path carries the target as alias:path (e.g. wsl:~/x.md). Results include line numbers; use offset and limit to continue reading large files."
982
+ });
983
+ ctx.systemPrompt.section({
984
+ name: "tool:write_remote",
985
+ order: ctx.systemPrompt.getSectionOrder("TOOL_WRITE"),
986
+ text: "Use write_remote to create a NEW UTF-8 text file on a remote host. It only creates — the target must not already exist, and it never overwrites."
987
+ });
988
+ ctx.systemPrompt.section({
989
+ name: "tool:edit_remote",
990
+ order: ctx.systemPrompt.getSectionOrder("TOOL_EDIT"),
991
+ text: "Use edit_remote for targeted literal changes to an existing remote text file. It replaces literal old_string with new_string; by default old_string must appear exactly once."
992
+ });
993
+ ctx.tools.register(createReadRemoteTool(fs));
994
+ ctx.tools.register(createWriteRemoteTool(fs));
995
+ ctx.tools.register(createEditRemoteTool(fs));
996
+ }
997
+ export {
998
+ apply,
999
+ inject,
1000
+ name
1001
+ };
1002
+ //# sourceMappingURL=index.js.map