agent-repl 0.1.3 → 0.1.6

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,27 @@ All notable changes to `agent-repl` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.6] — 2026-10-19
8
+
9
+ ### Fixed
10
+ - `agent-repl attach`, and starting a runtime in a terminal, pass `-u` to tmux. In a terminal without a UTF-8 locale (`LANG` unset, as in Muxy), non-ASCII output such as λ or é showed as underscores.
11
+
12
+ ## [0.1.5] — 2026-10-19
13
+
14
+ ### Changed
15
+ - Generated from pi-repl 0.8.6.
16
+
17
+ ### Fixed
18
+ - 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.
19
+
20
+ ## [0.1.4] — 2026-10-19
21
+
22
+ ### Changed
23
+ - Generated from pi-repl 0.8.5.
24
+
25
+ ### Fixed
26
+ - 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.
27
+
7
28
  ## [0.1.3] — 2026-10-19
8
29
 
9
30
  ### Changed
package/dist/attach.js ADDED
@@ -0,0 +1,7 @@
1
+ /** The tmux arguments that show a session in this terminal. */
2
+ export function attachArgs(sessionName, insideTmux) {
3
+ // Inside tmux, switch this client to the session instead of nesting tmux.
4
+ // Otherwise -u: without a UTF-8 locale (LANG is unset in some terminals),
5
+ // a new client draws non-ASCII output such as λ or é as underscores.
6
+ return insideTmux ? ["switch-client", "-t", sessionName] : ["-u", "attach", "-t", sessionName];
7
+ }
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { readFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
+ import { attachArgs } from "./attach.js";
9
10
  import { hostContext, loadPiRepl } from "./host.js";
10
11
  import { runMcpServer } from "./mcp.js";
11
12
  import { forAgentRepl } from "./text.js";
@@ -55,9 +56,7 @@ export function attachTarget(messages) {
55
56
  return names.length === 1 ? names[0] : undefined;
56
57
  }
57
58
  function attachTerminal(sessionName) {
58
- // Inside tmux, switch this client to the session instead of nesting tmux.
59
- const args = process.env.TMUX ? ["switch-client", "-t", sessionName] : ["attach", "-t", sessionName];
60
- return spawnSync("tmux", args, { stdio: "inherit" }).status ?? 1;
59
+ return spawnSync("tmux", attachArgs(sessionName, Boolean(process.env.TMUX)), { stdio: "inherit" }).status ?? 1;
61
60
  }
62
61
  async function attach(runtime, cwd) {
63
62
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
package/dist/pi-repl.js CHANGED
@@ -1,4 +1,4 @@
1
- // GENERATED by scripts/sync-from-pi-repl.mjs from pi-repl 0.8.4 (6d9234a). 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";
@@ -890,7 +890,9 @@ function buildClojureDisplayStatements(display, indent = "") {
890
890
  // Prints the pane's cursor column, or exits non-zero; tmux gets 500 ms. A
891
891
  // wrapper runs it with Node and ends a line before its footer unless it
892
892
  // prints 0. Only the wrapper reads its output, so it never reaches the pane.
893
- const CURSOR_COLUMN_QUERY = 'try { process.stdout.write(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()); } catch { process.exitCode = 1; }';
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);';
894
896
  function buildPythonFooter(display) {
895
897
  if (!display.enabled)
896
898
  return [];
@@ -954,12 +956,32 @@ function buildJuliaFooter(display) {
954
956
  const query = `Cmd([${juliaStringLiteral(process.execPath)}, "-e", ${juliaStringLiteral(CURSOR_COLUMN_QUERY)}])`;
955
957
  return [
956
958
  ' local __pi_repl_column = ""',
957
- // read throws if the query fails; catch also takes an interrupt here.
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.
958
965
  " try",
959
966
  " flush(stdout)",
960
967
  " flush(stderr)",
961
- ` __pi_repl_column = strip(read(pipeline(${query}, stdin=devnull, stderr=devnull), String))`,
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)))",
962
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",
963
985
  " end",
964
986
  ' __pi_repl_column == "0" || Base.println()',
965
987
  ...display.suffixLines.map((line) => ` Base.println(${juliaStringLiteral(line)})`),
@@ -987,9 +1009,10 @@ function buildRFooter(display) {
987
1009
  return [];
988
1010
  return [
989
1011
  " try({ flush(stdout()); flush(stderr()) }, silent = TRUE)",
990
- // system2 quotes the command but not its arguments. A failed query
991
- // sets a status; an error or Ctrl-C here gives NULL, not a lost done file.
992
- ` .__pi_repl_column <- tryCatch(suppressWarnings(system2(${JSON.stringify(process.execPath)}, c("-e", ${JSON.stringify(shellQuote(CURSOR_COLUMN_QUERY))}), stdout = TRUE, stderr = FALSE)), error = function(e) NULL, interrupt = function(e) NULL)`,
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)`,
993
1016
  ' if (!is.null(attr(.__pi_repl_column, "status")) || !identical(trimws(paste(.__pi_repl_column, collapse = "")), "0")) base::cat("\\n")',
994
1017
  ...display.suffixLines.map((line) => ` base::cat(${JSON.stringify(`${line}\n`)})`),
995
1018
  ];
@@ -1126,16 +1149,27 @@ function buildClojureFooter(display) {
1126
1149
  return [
1127
1150
  " (try (.flush *out*) (.flush *err*) (catch Throwable _))",
1128
1151
  // An interrupt left pending by the code would end the wait at once, so
1129
- // it is cleared for the query and restored afterwards, as is one that
1130
- // arrives during the wait. The query is killed on every path.
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.
1131
1155
  " (let [pi-repl-interrupted (volatile! (Thread/interrupted))",
1132
1156
  ` pi-repl-query (try (.start (.redirectError ${builder} java.lang.ProcessBuilder$Redirect/DISCARD)) (catch Throwable _ nil))`,
1157
+ " pi-repl-deadline (+ (System/nanoTime) 5000000000)",
1133
1158
  " pi-repl-column (when pi-repl-query",
1134
1159
  " (try",
1135
1160
  " (.close (.getOutputStream pi-repl-query))",
1136
- " (when (and (.waitFor pi-repl-query 5 java.util.concurrent.TimeUnit/SECONDS) (zero? (.exitValue pi-repl-query)))",
1137
- " (.trim ^String (slurp (.getInputStream pi-repl-query))))",
1138
- " (catch InterruptedException _ (vreset! pi-repl-interrupted true) nil)",
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"))))',
1139
1173
  " (catch Throwable _ nil)",
1140
1174
  " (finally (.destroyForcibly pi-repl-query))))]",
1141
1175
  " (when @pi-repl-interrupted (.interrupt (Thread/currentThread)))",
@@ -1362,7 +1396,7 @@ function prepareReplControlFiles(runtime, code, details) {
1362
1396
  throw error;
1363
1397
  }
1364
1398
  }
1365
- async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted) {
1399
+ async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted, bracketed = false) {
1366
1400
  const bufferName = `pi-repl-${randomUUID()}`;
1367
1401
  const target = await getPaneTarget(pi, sessionTarget, cwd);
1368
1402
  const controlPaths = createPrivateReplControlFiles({ ...REPL_CONTROL_OPTIONS, extension: "txt", buildSource: () => text });
@@ -1374,7 +1408,10 @@ async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted) {
1374
1408
  const reason = loadResult.stderr.trim() || loadResult.stdout.trim() || `exit code ${loadResult.code}`;
1375
1409
  throw new Error(`Failed to load tmux buffer: ${reason}`);
1376
1410
  }
1377
- 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);
1378
1415
  if (pasteResult.code !== 0) {
1379
1416
  const reason = pasteResult.stderr.trim() || pasteResult.stdout.trim() || `exit code ${pasteResult.code}`;
1380
1417
  throw new Error(`Failed to paste tmux buffer: ${reason}`);
@@ -1674,7 +1711,7 @@ async function runReplCode(pi, params, ctx, signal, options = {}) {
1674
1711
  await pasteTextToTmuxPane(pi, paneTarget, ctx.cwd, prepared.submissionText, () => {
1675
1712
  submissionStarted = true;
1676
1713
  options.onSubmissionStarted?.(submissionState);
1677
- });
1714
+ }, runtime === "julia");
1678
1715
  await waitForReplDoneFile(pi, sessionName, sessionTarget, paneTarget, ctx.cwd, prepared.controlPaths.doneFile, timeoutMs, signal, { beforeCapture, prepared, state: submissionState });
1679
1716
  submissionState.completionObserved = true;
1680
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.3",
3
+ "version": "0.1.6",
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",