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 +21 -0
- package/dist/attach.js +7 -0
- package/dist/cli.js +2 -3
- package/dist/pi-repl.js +52 -15
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
991
|
-
// sets a status; an error or Ctrl-C here gives
|
|
992
|
-
|
|
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
|
|
1130
|
-
//
|
|
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 (
|
|
1137
|
-
"
|
|
1138
|
-
"
|
|
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
|
-
|
|
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