talon-agent 3.0.1 → 3.0.3
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/package.json
CHANGED
|
@@ -75,6 +75,12 @@ const TELEPORT_TIMEOUT_HINT =
|
|
|
75
75
|
"background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll the log " +
|
|
76
76
|
"with read, or bound the command itself: `logcat -d`, `timeout 30 …`, `head -n 200`.)";
|
|
77
77
|
const MAX_READ_LINES = 2_000;
|
|
78
|
+
/**
|
|
79
|
+
* Refuse to slurp huge files into memory: `read` loads the whole file to
|
|
80
|
+
* slice lines, so an unbounded readFile of a multi-GB log balloons RSS.
|
|
81
|
+
* Past this, bash is the right tool (`sed -n`, `tail`, `head`).
|
|
82
|
+
*/
|
|
83
|
+
const MAX_READ_FILE_BYTES = 32 * 1024 * 1024;
|
|
78
84
|
/**
|
|
79
85
|
* Image extensions the model can actually view, mapped to their MIME type.
|
|
80
86
|
* Reading one returns an image content block instead of the file's raw bytes
|
|
@@ -169,7 +175,19 @@ async function bash(
|
|
|
169
175
|
const active = await getTeleport(chatId);
|
|
170
176
|
|
|
171
177
|
let dir = typeof cwd === "string" && cwd.trim() ? cwd.trim() : undefined;
|
|
172
|
-
if (dir !== undefined && !active)
|
|
178
|
+
if (dir !== undefined && !active) {
|
|
179
|
+
dir = resolvePathParam(dir, undefined);
|
|
180
|
+
// Validate here: a bad cwd makes spawn fail with "spawn bash ENOENT",
|
|
181
|
+
// which reads as "bash is missing" and sends the model down the wrong
|
|
182
|
+
// path. Name the actual problem instead.
|
|
183
|
+
try {
|
|
184
|
+
const st = await fsStat(dir);
|
|
185
|
+
if (!st.isDirectory())
|
|
186
|
+
return { ok: false, text: `cwd is not a directory: ${dir}` };
|
|
187
|
+
} catch {
|
|
188
|
+
return { ok: false, text: `Working directory does not exist: ${dir}` };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
173
191
|
|
|
174
192
|
const result = await (async () => {
|
|
175
193
|
if (background === true) {
|
|
@@ -509,6 +527,10 @@ async function read(
|
|
|
509
527
|
bytes = res.data;
|
|
510
528
|
} else {
|
|
511
529
|
try {
|
|
530
|
+
// stat first — no point slurping a 40MB photo just to refuse it.
|
|
531
|
+
const st = await fsStat(p);
|
|
532
|
+
if (st.size > MAX_IMAGE_BYTES)
|
|
533
|
+
return oversizeImageResult(shown, where, st.size, p);
|
|
512
534
|
bytes = await readFile(p);
|
|
513
535
|
} catch (err) {
|
|
514
536
|
return {
|
|
@@ -518,13 +540,7 @@ async function read(
|
|
|
518
540
|
}
|
|
519
541
|
}
|
|
520
542
|
if (bytes.length > MAX_IMAGE_BYTES) {
|
|
521
|
-
return
|
|
522
|
-
ok: false,
|
|
523
|
-
text:
|
|
524
|
-
`${shown} [${where}] is ${(bytes.length / 1_048_576).toFixed(1)}MB — over the ` +
|
|
525
|
-
`${MAX_IMAGE_BYTES / 1_048_576}MB image limit. Downscale it first ` +
|
|
526
|
-
`(e.g. \`convert '${p}' -resize 1568x /tmp/small.jpg\`) and read that.`,
|
|
527
|
-
};
|
|
543
|
+
return oversizeImageResult(shown, where, bytes.length, p);
|
|
528
544
|
}
|
|
529
545
|
return {
|
|
530
546
|
ok: true,
|
|
@@ -533,15 +549,26 @@ async function read(
|
|
|
533
549
|
};
|
|
534
550
|
}
|
|
535
551
|
|
|
536
|
-
|
|
537
|
-
|
|
552
|
+
// Clamp: a negative offset would silently flip slice() into
|
|
553
|
+
// count-from-the-end (with line numbers that lie), and a zero/negative
|
|
554
|
+
// limit would return an empty read that looks like an empty file.
|
|
555
|
+
const start = Math.max(0, Math.trunc(num(offset) ?? 0));
|
|
556
|
+
const max = Math.min(
|
|
557
|
+
Math.max(1, Math.trunc(num(limit) ?? MAX_READ_LINES)),
|
|
558
|
+
MAX_READ_LINES,
|
|
559
|
+
);
|
|
538
560
|
let content: string;
|
|
539
561
|
if (active) {
|
|
540
562
|
const res = await getMeshService().readFileBytes(active.deviceId, p);
|
|
541
563
|
if ("error" in res) return { ok: false, text: res.error };
|
|
564
|
+
if (res.data.length > MAX_READ_FILE_BYTES)
|
|
565
|
+
return oversizeReadResult(shown, where, res.data.length, p);
|
|
542
566
|
content = res.data.toString("utf8");
|
|
543
567
|
} else {
|
|
544
568
|
try {
|
|
569
|
+
const st = await fsStat(p);
|
|
570
|
+
if (st.isFile() && st.size > MAX_READ_FILE_BYTES)
|
|
571
|
+
return oversizeReadResult(shown, where, st.size, p);
|
|
545
572
|
content = await readFile(p, "utf8");
|
|
546
573
|
} catch (err) {
|
|
547
574
|
return {
|
|
@@ -550,7 +577,24 @@ async function read(
|
|
|
550
577
|
};
|
|
551
578
|
}
|
|
552
579
|
}
|
|
580
|
+
// Binary files decoded as UTF-8 are mojibake the model can't use — same
|
|
581
|
+
// rationale as the image branch, but with no viewable representation to
|
|
582
|
+
// return. Point at the shell tools that can actually inspect the bytes.
|
|
583
|
+
if (content.includes("\0")) {
|
|
584
|
+
return {
|
|
585
|
+
ok: false,
|
|
586
|
+
text:
|
|
587
|
+
`${shown} [${where}] looks binary — refusing to render it as text. ` +
|
|
588
|
+
`Inspect it with bash instead: \`file '${p}'\`, \`xxd '${p}' | head\`, \`strings '${p}'\`.`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
553
591
|
const lines = content.split("\n");
|
|
592
|
+
if (start >= lines.length) {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
text: `${shown} [${where}] has ${lines.length} lines — offset ${start} is past the end.`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
554
598
|
const slice = lines.slice(start, start + max);
|
|
555
599
|
const numbered = slice
|
|
556
600
|
.map((line, i) => `${String(start + i + 1).padStart(6)}\t${line}`)
|
|
@@ -588,7 +632,10 @@ async function write(
|
|
|
588
632
|
text: `Cannot write ${shown}: ${(err as Error).message}`,
|
|
589
633
|
};
|
|
590
634
|
}
|
|
591
|
-
return {
|
|
635
|
+
return {
|
|
636
|
+
ok: true,
|
|
637
|
+
text: `Wrote ${Buffer.byteLength(body, "utf8")} bytes to ${shown} [local].`,
|
|
638
|
+
};
|
|
592
639
|
}
|
|
593
640
|
|
|
594
641
|
async function edit(
|
|
@@ -638,10 +685,14 @@ async function edit(
|
|
|
638
685
|
text: `old_string appears ${count}× in ${shown}; pass replace_all or make it unique.`,
|
|
639
686
|
};
|
|
640
687
|
}
|
|
688
|
+
// Splice, not String.replace: replace() treats dollar-sign substitution
|
|
689
|
+
// patterns ($&, $', $BACKTICK, $$) in the replacement as directives, silently
|
|
690
|
+
// corrupting any new_string that contains them (shell snippets, regexes,
|
|
691
|
+
// Makefiles...). split/join and index-splice are both literal.
|
|
641
692
|
const updated =
|
|
642
693
|
replaceAll === true
|
|
643
694
|
? content.split(from).join(to)
|
|
644
|
-
: content
|
|
695
|
+
: spliceOnce(content, from, to);
|
|
645
696
|
|
|
646
697
|
if (active) {
|
|
647
698
|
const res = await svc.writeFileToDevice(active.deviceId, p, updated);
|
|
@@ -705,6 +756,9 @@ async function glob(
|
|
|
705
756
|
} else {
|
|
706
757
|
files = res.stdout.trim().split("\n").filter(Boolean);
|
|
707
758
|
}
|
|
759
|
+
// rg's parallel directory walk (and the JS fallback's) emit in
|
|
760
|
+
// nondeterministic order — sort so identical calls render identically.
|
|
761
|
+
files.sort();
|
|
708
762
|
return {
|
|
709
763
|
ok: true,
|
|
710
764
|
text: files.length
|
|
@@ -840,6 +894,36 @@ async function searchJs(
|
|
|
840
894
|
return lines;
|
|
841
895
|
}
|
|
842
896
|
|
|
897
|
+
function oversizeReadResult(
|
|
898
|
+
shown: string,
|
|
899
|
+
where: string,
|
|
900
|
+
size: number,
|
|
901
|
+
p: string,
|
|
902
|
+
): Result {
|
|
903
|
+
return {
|
|
904
|
+
ok: false,
|
|
905
|
+
text:
|
|
906
|
+
`${shown} [${where}] is ${(size / 1_048_576).toFixed(1)}MB — over the ` +
|
|
907
|
+
`${MAX_READ_FILE_BYTES / 1_048_576}MB read limit. Slice it with bash instead: ` +
|
|
908
|
+
`\`sed -n '1,200p' '${p}'\`, \`tail -n 200 '${p}'\`, or \`grep\` for what you need.`,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function oversizeImageResult(
|
|
913
|
+
shown: string,
|
|
914
|
+
where: string,
|
|
915
|
+
size: number,
|
|
916
|
+
p: string,
|
|
917
|
+
): Result {
|
|
918
|
+
return {
|
|
919
|
+
ok: false,
|
|
920
|
+
text:
|
|
921
|
+
`${shown} [${where}] is ${(size / 1_048_576).toFixed(1)}MB — over the ` +
|
|
922
|
+
`${MAX_IMAGE_BYTES / 1_048_576}MB image limit. Downscale it first ` +
|
|
923
|
+
`(e.g. \`convert '${p}' -resize 1568x /tmp/small.jpg\`) and read that.`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
843
927
|
/**
|
|
844
928
|
* When an edit's old_string doesn't match verbatim, the cause is almost
|
|
845
929
|
* always invisible: indentation, tabs vs spaces, or a trailing space. Point
|
|
@@ -862,6 +946,13 @@ function nearMissHint(content: string, from: string): string {
|
|
|
862
946
|
);
|
|
863
947
|
}
|
|
864
948
|
|
|
949
|
+
/** Replace the first occurrence of `from` with `to`, both taken literally. */
|
|
950
|
+
function spliceOnce(content: string, from: string, to: string): string {
|
|
951
|
+
const idx = content.indexOf(from);
|
|
952
|
+
if (idx === -1) return content;
|
|
953
|
+
return content.slice(0, idx) + to + content.slice(idx + from.length);
|
|
954
|
+
}
|
|
955
|
+
|
|
865
956
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
866
957
|
|
|
867
958
|
function runLocal(
|