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 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.3 (45b5042). Do not edit by hand:
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.suffixLines.map((line) => ` __pi_repl_builtins.print(${JSON.stringify(line)})`);
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.suffixLines.map((line) => ` Base.println(${juliaStringLiteral(line)})`);
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.suffixLines.map((line) => ` base::cat(${JSON.stringify(`${line}\n`)})`);
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.suffixLines.map((line) => ` (clojure.core/println ${JSON.stringify(line)})`);
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
- const pasteResult = await execTmux(pi, ["paste-buffer", "-d", "-b", bufferName, "-t", target], cwd, 5_000);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-repl",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "pi-repl's shared tmux REPLs (Python, Julia, R and more) for Claude Code, Codex, OpenCode and other agents, over MCP and a CLI.",
5
5
  "keywords": [
6
6
  "repl",