agent-repl 0.1.2 → 0.1.5
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 +24 -0
- package/dist/pi-repl.js +124 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to `agent-repl` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.5] — 2026-10-19
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Generated from pi-repl 0.8.6.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- The 0.1.3 done-marker footer no longer waits without limit for its Node helper. In Julia and R, a Node startup hook (`NODE_OPTIONS`) that kept Node running held the cell's completion indefinitely. In Clojure, an interrupt while the footer waited left the tmux query running.
|
|
14
|
+
|
|
15
|
+
## [0.1.4] — 2026-10-19
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Generated from pi-repl 0.8.5.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Julia no longer leaves copies of a wrapped submission line in the output, as it did in panes narrower than the `include(...)` line, such as a detached session's default 80 columns.
|
|
22
|
+
|
|
23
|
+
## [0.1.3] — 2026-10-19
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Generated from pi-repl 0.8.4.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- In the Summary and Full displays, output that doesn't end its line no longer runs into the done marker in Python, IPython, Julia, R and Clojure. The marker then stayed in the clean output, as in `c── done · id: … ──`.
|
|
30
|
+
|
|
7
31
|
## [0.1.2] — 2026-10-19
|
|
8
32
|
|
|
9
33
|
### Changed
|
package/dist/pi-repl.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by scripts/sync-from-pi-repl.mjs from pi-repl 0.8.
|
|
1
|
+
// GENERATED by scripts/sync-from-pi-repl.mjs from pi-repl 0.8.6 (7dd015b). Do not edit by hand:
|
|
2
2
|
// change pi-repl and re-sync. Identical to pi-repl's index.ts except that its Pi
|
|
3
3
|
// imports come from ./pi-compat/index.js.
|
|
4
4
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateTail, } from "./pi-compat/index.js";
|
|
@@ -887,9 +887,36 @@ function buildClojureDisplayStatements(display, indent = "") {
|
|
|
887
887
|
return [];
|
|
888
888
|
return display.prefixLines.map((line) => `${indent}(clojure.core/println ${JSON.stringify(line)})`);
|
|
889
889
|
}
|
|
890
|
+
// Prints the pane's cursor column, or exits non-zero; tmux gets 500 ms. A
|
|
891
|
+
// wrapper runs it with Node and ends a line before its footer unless it
|
|
892
|
+
// prints 0. Only the wrapper reads its output, so it never reaches the pane.
|
|
893
|
+
// process.exit ends Node even if a startup hook in NODE_OPTIONS would keep it
|
|
894
|
+
// running, and each wrapper also limits the whole helper to 5 s.
|
|
895
|
+
const CURSOR_COLUMN_QUERY = 'let code = 1; try { require("node:fs").writeSync(1, require("node:child_process").execFileSync("tmux", ["-N", "display-message", "-p", "-t", process.env.TMUX_PANE, "#{cursor_x}"], { encoding: "utf8", timeout: 500, killSignal: "SIGKILL", maxBuffer: 128, stdio: ["ignore", "pipe", "ignore"] }).trim()); code = 0; } catch {} process.exit(code);';
|
|
896
|
+
function buildPythonFooter(display) {
|
|
897
|
+
if (!display.enabled)
|
|
898
|
+
return [];
|
|
899
|
+
return [
|
|
900
|
+
" import subprocess as __pi_repl_subprocess",
|
|
901
|
+
" import sys as __pi_repl_sys",
|
|
902
|
+
" __pi_repl_column = ''",
|
|
903
|
+
" try:",
|
|
904
|
+
" __pi_repl_sys.stdout.flush()",
|
|
905
|
+
" __pi_repl_sys.stderr.flush()",
|
|
906
|
+
` __pi_repl_query = __pi_repl_subprocess.run([${JSON.stringify(process.execPath)}, '-e', ${JSON.stringify(CURSOR_COLUMN_QUERY)}], stdin=__pi_repl_subprocess.DEVNULL, stdout=__pi_repl_subprocess.PIPE, stderr=__pi_repl_subprocess.DEVNULL, timeout=5)`,
|
|
907
|
+
" if __pi_repl_query.returncode == 0:",
|
|
908
|
+
" __pi_repl_column = __pi_repl_query.stdout.decode('utf-8', 'replace').strip()",
|
|
909
|
+
// run() kills the query on any exception. Ctrl-C here must not skip the done file.
|
|
910
|
+
" except BaseException:",
|
|
911
|
+
" pass",
|
|
912
|
+
" if __pi_repl_column != '0':",
|
|
913
|
+
" __pi_repl_builtins.print()",
|
|
914
|
+
...display.suffixLines.map((line) => ` __pi_repl_builtins.print(${JSON.stringify(line)})`),
|
|
915
|
+
];
|
|
916
|
+
}
|
|
890
917
|
function buildPythonControlSource(runtime, code, doneFile, display) {
|
|
891
918
|
const prefix = buildPythonDisplayStatements(display);
|
|
892
|
-
const completion = display
|
|
919
|
+
const completion = buildPythonFooter(display);
|
|
893
920
|
if (runtime === "ipython") {
|
|
894
921
|
return [
|
|
895
922
|
"from pathlib import Path as __pi_repl_path",
|
|
@@ -923,8 +950,45 @@ function buildPythonControlSource(runtime, code, doneFile, display) {
|
|
|
923
950
|
` __pi_repl_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
|
|
924
951
|
].join("\n");
|
|
925
952
|
}
|
|
953
|
+
function buildJuliaFooter(display) {
|
|
954
|
+
if (!display.enabled)
|
|
955
|
+
return [];
|
|
956
|
+
const query = `Cmd([${juliaStringLiteral(process.execPath)}, "-e", ${juliaStringLiteral(CURSOR_COLUMN_QUERY)}])`;
|
|
957
|
+
return [
|
|
958
|
+
' local __pi_repl_column = ""',
|
|
959
|
+
" local __pi_repl_query = nothing",
|
|
960
|
+
// The wait, not the read, is bounded: a child the helper started could
|
|
961
|
+
// keep the pipe open after the helper exits. An interrupt does not end
|
|
962
|
+
// the wait, since killing Node early would leave its tmux child
|
|
963
|
+
// running. After 5 s a helper still running is killed, and the pipe
|
|
964
|
+
// is closed so that the reader ends too.
|
|
965
|
+
" try",
|
|
966
|
+
" flush(stdout)",
|
|
967
|
+
" flush(stderr)",
|
|
968
|
+
` __pi_repl_query = open(pipeline(${query}, stdin=devnull, stderr=devnull), "r")`,
|
|
969
|
+
" local __pi_repl_reader = @async read(__pi_repl_query, String)",
|
|
970
|
+
" local __pi_repl_deadline = time_ns() + 5_000_000_000",
|
|
971
|
+
" local __pi_repl_finished = false",
|
|
972
|
+
" while !__pi_repl_finished && time_ns() < __pi_repl_deadline",
|
|
973
|
+
" try",
|
|
974
|
+
" __pi_repl_finished = timedwait(() -> istaskdone(__pi_repl_reader) && process_exited(__pi_repl_query), max(0.01, (signed(__pi_repl_deadline) - signed(time_ns())) / 1e9); pollint=0.01) === :ok",
|
|
975
|
+
" catch",
|
|
976
|
+
" end",
|
|
977
|
+
" end",
|
|
978
|
+
" __pi_repl_finished && success(__pi_repl_query) && (__pi_repl_column = strip(fetch(__pi_repl_reader)))",
|
|
979
|
+
" catch",
|
|
980
|
+
" finally",
|
|
981
|
+
" if !isnothing(__pi_repl_query)",
|
|
982
|
+
" process_exited(__pi_repl_query) || kill(__pi_repl_query, Base.SIGKILL)",
|
|
983
|
+
" try close(__pi_repl_query.out) catch end",
|
|
984
|
+
" end",
|
|
985
|
+
" end",
|
|
986
|
+
' __pi_repl_column == "0" || Base.println()',
|
|
987
|
+
...display.suffixLines.map((line) => ` Base.println(${juliaStringLiteral(line)})`),
|
|
988
|
+
];
|
|
989
|
+
}
|
|
926
990
|
function buildJuliaControlSource(code, doneFile, display) {
|
|
927
|
-
const completion = display
|
|
991
|
+
const completion = buildJuliaFooter(display);
|
|
928
992
|
return [
|
|
929
993
|
...buildJuliaDisplayStatements(display),
|
|
930
994
|
"try",
|
|
@@ -940,8 +1004,21 @@ function buildJuliaControlSource(code, doneFile, display) {
|
|
|
940
1004
|
"end",
|
|
941
1005
|
].join("\n");
|
|
942
1006
|
}
|
|
1007
|
+
function buildRFooter(display) {
|
|
1008
|
+
if (!display.enabled)
|
|
1009
|
+
return [];
|
|
1010
|
+
return [
|
|
1011
|
+
" try({ flush(stdout()); flush(stderr()) }, silent = TRUE)",
|
|
1012
|
+
// system2 quotes the command but not its arguments, and kills it after
|
|
1013
|
+
// 5 s. A failed query sets a status; an error or Ctrl-C here gives
|
|
1014
|
+
// NULL, not a lost done file.
|
|
1015
|
+
` .__pi_repl_column <- tryCatch(suppressWarnings(system2(${JSON.stringify(process.execPath)}, c("-e", ${JSON.stringify(shellQuote(CURSOR_COLUMN_QUERY))}), stdout = TRUE, stderr = FALSE, timeout = 5)), error = function(e) NULL, interrupt = function(e) NULL)`,
|
|
1016
|
+
' if (!is.null(attr(.__pi_repl_column, "status")) || !identical(trimws(paste(.__pi_repl_column, collapse = "")), "0")) base::cat("\\n")',
|
|
1017
|
+
...display.suffixLines.map((line) => ` base::cat(${JSON.stringify(`${line}\n`)})`),
|
|
1018
|
+
];
|
|
1019
|
+
}
|
|
943
1020
|
function buildRControlSource(code, doneFile, display) {
|
|
944
|
-
const completion = display
|
|
1021
|
+
const completion = buildRFooter(display);
|
|
945
1022
|
return [
|
|
946
1023
|
"local({",
|
|
947
1024
|
...buildRDisplayStatements(display, " "),
|
|
@@ -1065,8 +1142,44 @@ function buildJavaControlSource(code, doneFile, display) {
|
|
|
1065
1142
|
// stays in the outer driver, outside this source file's parser state.
|
|
1066
1143
|
return [...prefix, code.replace(/\r/g, "").trimEnd(), ""].join("\n");
|
|
1067
1144
|
}
|
|
1145
|
+
function buildClojureFooter(display) {
|
|
1146
|
+
if (!display.enabled)
|
|
1147
|
+
return [];
|
|
1148
|
+
const builder = `(java.lang.ProcessBuilder. ^java.util.List [${JSON.stringify(process.execPath)} "-e" ${JSON.stringify(CURSOR_COLUMN_QUERY)}])`;
|
|
1149
|
+
return [
|
|
1150
|
+
" (try (.flush *out*) (.flush *err*) (catch Throwable _))",
|
|
1151
|
+
// An interrupt left pending by the code would end the wait at once, so
|
|
1152
|
+
// it is cleared for the query and restored afterwards. One that arrives
|
|
1153
|
+
// during the wait is noted and the wait goes on: killing Node early
|
|
1154
|
+
// would leave its tmux child running. The query is killed after 5 s.
|
|
1155
|
+
" (let [pi-repl-interrupted (volatile! (Thread/interrupted))",
|
|
1156
|
+
` pi-repl-query (try (.start (.redirectError ${builder} java.lang.ProcessBuilder$Redirect/DISCARD)) (catch Throwable _ nil))`,
|
|
1157
|
+
" pi-repl-deadline (+ (System/nanoTime) 5000000000)",
|
|
1158
|
+
" pi-repl-column (when pi-repl-query",
|
|
1159
|
+
" (try",
|
|
1160
|
+
" (.close (.getOutputStream pi-repl-query))",
|
|
1161
|
+
" (when (and (loop []",
|
|
1162
|
+
" (let [left (- pi-repl-deadline (System/nanoTime))",
|
|
1163
|
+
" exited (when (pos? left)",
|
|
1164
|
+
" (try (.waitFor pi-repl-query left java.util.concurrent.TimeUnit/NANOSECONDS)",
|
|
1165
|
+
" (catch InterruptedException _ (vreset! pi-repl-interrupted true) :interrupted)))]",
|
|
1166
|
+
" (if (= exited :interrupted) (recur) exited)))",
|
|
1167
|
+
" (zero? (.exitValue pi-repl-query)))",
|
|
1168
|
+
// Only what is waiting: a child of Node could hold the pipe open.
|
|
1169
|
+
" (let [in (.getInputStream pi-repl-query)",
|
|
1170
|
+
" bytes (byte-array (min 32 (.available in)))",
|
|
1171
|
+
" n (.read in bytes)]",
|
|
1172
|
+
' (.trim (String. bytes 0 (int (max 0 n)) "UTF-8"))))',
|
|
1173
|
+
" (catch Throwable _ nil)",
|
|
1174
|
+
" (finally (.destroyForcibly pi-repl-query))))]",
|
|
1175
|
+
" (when @pi-repl-interrupted (.interrupt (Thread/currentThread)))",
|
|
1176
|
+
' (when-not (= pi-repl-column "0") (clojure.core/println))',
|
|
1177
|
+
...display.suffixLines.map((line) => ` (clojure.core/println ${JSON.stringify(line)})`),
|
|
1178
|
+
" nil)",
|
|
1179
|
+
];
|
|
1180
|
+
}
|
|
1068
1181
|
function buildClojureControlSource(code, doneFile, display) {
|
|
1069
|
-
const completion = display
|
|
1182
|
+
const completion = buildClojureFooter(display);
|
|
1070
1183
|
return [
|
|
1071
1184
|
"(let [code " + JSON.stringify(code) + "]",
|
|
1072
1185
|
...buildClojureDisplayStatements(display, " "),
|
|
@@ -1283,7 +1396,7 @@ function prepareReplControlFiles(runtime, code, details) {
|
|
|
1283
1396
|
throw error;
|
|
1284
1397
|
}
|
|
1285
1398
|
}
|
|
1286
|
-
async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted) {
|
|
1399
|
+
async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted, bracketed = false) {
|
|
1287
1400
|
const bufferName = `pi-repl-${randomUUID()}`;
|
|
1288
1401
|
const target = await getPaneTarget(pi, sessionTarget, cwd);
|
|
1289
1402
|
const controlPaths = createPrivateReplControlFiles({ ...REPL_CONTROL_OPTIONS, extension: "txt", buildSource: () => text });
|
|
@@ -1295,7 +1408,10 @@ async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted) {
|
|
|
1295
1408
|
const reason = loadResult.stderr.trim() || loadResult.stdout.trim() || `exit code ${loadResult.code}`;
|
|
1296
1409
|
throw new Error(`Failed to load tmux buffer: ${reason}`);
|
|
1297
1410
|
}
|
|
1298
|
-
|
|
1411
|
+
// -p brackets the paste only if the REPL asked for bracketed pastes.
|
|
1412
|
+
// Julia then draws a line that wraps in the pane once, not once per
|
|
1413
|
+
// character, which left copies of its first row in the output.
|
|
1414
|
+
const pasteResult = await execTmux(pi, ["paste-buffer", ...(bracketed ? ["-p"] : []), "-d", "-b", bufferName, "-t", target], cwd, 5_000);
|
|
1299
1415
|
if (pasteResult.code !== 0) {
|
|
1300
1416
|
const reason = pasteResult.stderr.trim() || pasteResult.stdout.trim() || `exit code ${pasteResult.code}`;
|
|
1301
1417
|
throw new Error(`Failed to paste tmux buffer: ${reason}`);
|
|
@@ -1595,7 +1711,7 @@ async function runReplCode(pi, params, ctx, signal, options = {}) {
|
|
|
1595
1711
|
await pasteTextToTmuxPane(pi, paneTarget, ctx.cwd, prepared.submissionText, () => {
|
|
1596
1712
|
submissionStarted = true;
|
|
1597
1713
|
options.onSubmissionStarted?.(submissionState);
|
|
1598
|
-
});
|
|
1714
|
+
}, runtime === "julia");
|
|
1599
1715
|
await waitForReplDoneFile(pi, sessionName, sessionTarget, paneTarget, ctx.cwd, prepared.controlPaths.doneFile, timeoutMs, signal, { beforeCapture, prepared, state: submissionState });
|
|
1600
1716
|
submissionState.completionObserved = true;
|
|
1601
1717
|
const afterCapture = await capturePaneOutput(pi, paneTarget, ctx.cwd);
|
package/package.json
CHANGED