ragent-cli 1.11.14 → 1.11.15

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/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "ragent-cli",
34
- version: "1.11.14",
34
+ version: "1.11.15",
35
35
  description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -10738,13 +10738,14 @@ var MAX_CONCURRENT_STREAMS = 20;
10738
10738
  var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
10739
10739
  var PRIVATE_MODE_SEQUENCE_RE = /\x1b\[\?([0-9;]*)([hl])/g;
10740
10740
  var ALT_SCREEN_PARAMS = /* @__PURE__ */ new Set(["47", "1047", "1048", "1049"]);
10741
+ var MOUSE_MODE_PARAMS = /* @__PURE__ */ new Set(["9", "1000", "1002", "1003", "1005", "1006", "1015", "1016"]);
10741
10742
  function stripAlternateScreenModeSwitches(data) {
10742
10743
  return data.replace(
10743
10744
  PRIVATE_MODE_SEQUENCE_RE,
10744
10745
  (full, params, action) => {
10745
10746
  if (!params) return full;
10746
10747
  const parts = params.split(";");
10747
- const filtered = parts.filter((p) => !ALT_SCREEN_PARAMS.has(p));
10748
+ const filtered = parts.filter((p) => !ALT_SCREEN_PARAMS.has(p) && !MOUSE_MODE_PARAMS.has(p));
10748
10749
  if (filtered.length === parts.length) return full;
10749
10750
  if (filtered.length === 0) return "";
10750
10751
  return `\x1B[?${filtered.join(";")}${action}`;
@@ -10769,6 +10770,18 @@ var AlternateScreenModeStripper = class {
10769
10770
  return stripAlternateScreenModeSwitches(pending);
10770
10771
  }
10771
10772
  };
10773
+ function buildRelativeCursorSync(lineCount, x, y) {
10774
+ if (lineCount <= 0) return "";
10775
+ let seq = "";
10776
+ if (y <= lineCount - 1) {
10777
+ const up = lineCount - 1 - y;
10778
+ if (up > 0) seq += `\x1B[${up}A`;
10779
+ } else {
10780
+ seq += "\r\n".repeat(y - (lineCount - 1));
10781
+ }
10782
+ seq += `\x1B[${x + 1}G`;
10783
+ return seq;
10784
+ }
10772
10785
  function parsePaneTarget(sessionId) {
10773
10786
  if (!sessionId.startsWith("tmux:")) return null;
10774
10787
  const rest = sessionId.slice("tmux:".length);
@@ -10824,6 +10837,7 @@ var SessionStreamer = class {
10824
10837
  pendingStops = /* @__PURE__ */ new Map();
10825
10838
  sendFn;
10826
10839
  onStreamStopped;
10840
+ sendResetFn;
10827
10841
  /**
10828
10842
  * Per-session ring buffer for output captured while the WebSocket is disconnected.
10829
10843
  * Each entry is a string chunk; oldest entries are evicted when the total byte
@@ -10837,9 +10851,10 @@ var SessionStreamer = class {
10837
10851
  lastSeenDroppedFrames = 0;
10838
10852
  /** Sessions whose viewers need a capture-pane resync after frame drops. */
10839
10853
  pendingDropResync = /* @__PURE__ */ new Set();
10840
- constructor(sendFn, onStreamStopped) {
10854
+ constructor(sendFn, onStreamStopped, sendResetFn) {
10841
10855
  this.sendFn = sendFn;
10842
10856
  this.onStreamStopped = onStreamStopped;
10857
+ this.sendResetFn = sendResetFn;
10843
10858
  this.cleanupStaleStreams();
10844
10859
  this.startBackpressureResumeTimer();
10845
10860
  }
@@ -11123,84 +11138,141 @@ var SessionStreamer = class {
11123
11138
  }
11124
11139
  /**
11125
11140
  * Re-send capture-pane data for an already-active tmux stream.
11126
- * Used when a viewer missed the initial burst (e.g. joinGroup race)
11127
- * or reconnected ("Retry stream"). Does NOT restart pipe-pane.
11141
+ * Used when a viewer missed the initial burst (e.g. joinGroup race),
11142
+ * reconnected, resized, or after backpressure frame drops corrupted the
11143
+ * stream. Does NOT restart pipe-pane.
11144
+ *
11145
+ * Default is a LEAN repaint: visible pane + cursor only. Replaying
11146
+ * scrollback mid-session shoved thousands of lines of old output into
11147
+ * live viewers ("ghost text") on every resize/heal. Pass
11148
+ * `{ scrollback: true }` only for viewer (re)attach, where the replay is
11149
+ * prefixed with ED3 (`\x1b[3J`) so the viewer's existing scrollback is
11150
+ * cleared first instead of duplicated.
11128
11151
  */
11129
- resyncStream(sessionId, viewerId) {
11152
+ resyncStream(sessionId, viewerId, opts) {
11130
11153
  const stream = this.active.get(sessionId);
11131
11154
  if (!stream || stream.stopped || stream.streamType !== "tmux-pipe") return false;
11132
11155
  const { paneTarget, cleanEnv } = stream;
11133
11156
  if (!paneTarget || !cleanEnv) return false;
11134
11157
  const targetViewerId = viewerId?.trim() || void 0;
11135
- const send = (data) => {
11136
- if (targetViewerId) {
11137
- this.sendFn(sessionId, data, targetViewerId);
11138
- } else {
11139
- this.sendFn(sessionId, data);
11140
- }
11141
- };
11158
+ const withScrollback = opts?.scrollback === true;
11159
+ if (stream.initializing) {
11160
+ const pending = stream.pendingResyncs ??= [];
11161
+ const duplicate = pending.some(
11162
+ (p) => p.viewerId === targetViewerId && p.scrollback === withScrollback
11163
+ );
11164
+ if (!duplicate) pending.push({ viewerId: targetViewerId, scrollback: withScrollback });
11165
+ return true;
11166
+ }
11167
+ stream.initBuffer = [];
11142
11168
  stream.initializing = true;
11143
- try {
11144
- let alternateScreen = false;
11145
- try {
11146
- const altFlag = (0, import_node_child_process2.execFileSync)(
11147
- "tmux",
11148
- ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11149
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11150
- ).trim();
11151
- alternateScreen = altFlag === "1";
11152
- } catch {
11169
+ setImmediate(() => {
11170
+ if (stream.stopped) {
11171
+ stream.initializing = false;
11172
+ stream.initBuffer = [];
11173
+ return;
11153
11174
  }
11175
+ const send = (data) => {
11176
+ if (targetViewerId) {
11177
+ this.sendFn(sessionId, data, targetViewerId);
11178
+ } else {
11179
+ this.sendFn(sessionId, data);
11180
+ }
11181
+ };
11154
11182
  try {
11155
- const scrollback = (0, import_node_child_process2.execFileSync)(
11156
- "tmux",
11157
- ["capture-pane", "-t", paneTarget, "-p", "-e", "-S", "-5000", "-E", "-1"],
11158
- { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
11159
- );
11160
- if (scrollback && scrollback.length > 0) {
11161
- send(scrollback.replace(/\n/g, "\r\n"));
11183
+ this.sendResetFn?.(sessionId, withScrollback ? "full" : "repaint", targetViewerId);
11184
+ const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
11185
+ if (withScrollback) {
11186
+ send("\x1B[3J");
11187
+ try {
11188
+ const scrollback = (0, import_node_child_process2.execFileSync)(
11189
+ "tmux",
11190
+ ["capture-pane", "-t", paneTarget, "-p", "-e", "-S", "-5000", "-E", "-1"],
11191
+ { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
11192
+ );
11193
+ if (scrollback && scrollback.length > 0) {
11194
+ send(scrollback.replace(/\n/g, "\r\n"));
11195
+ }
11196
+ } catch {
11197
+ }
11198
+ }
11199
+ send("\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11200
+ const lineCount = this.sendVisiblePane(send, paneTarget, cleanEnv, alternateScreen);
11201
+ this.sendCursorSync(send, paneTarget, cleanEnv, lineCount);
11202
+ log17.info("resync capture", { sessionId, mode: withScrollback ? "full" : "repaint" });
11203
+ } catch (error) {
11204
+ const message = error instanceof Error ? error.message : String(error);
11205
+ log17.warn("failed to resync stream", { sessionId, error: message });
11206
+ } finally {
11207
+ stream.initializing = false;
11208
+ stream.initBuffer = [];
11209
+ const next = stream.pendingResyncs?.shift();
11210
+ if (next && !stream.stopped) {
11211
+ this.resyncStream(sessionId, next.viewerId, { scrollback: next.scrollback });
11162
11212
  }
11163
- } catch {
11164
11213
  }
11165
- send("\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11166
- try {
11167
- const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11168
- const initial = (0, import_node_child_process2.execFileSync)(
11169
- "tmux",
11170
- captureArgs,
11171
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11214
+ });
11215
+ return true;
11216
+ }
11217
+ /** Detect whether the pane is using the alternate screen buffer. */
11218
+ detectAlternateScreen(paneTarget, cleanEnv) {
11219
+ try {
11220
+ const altFlag = (0, import_node_child_process2.execFileSync)(
11221
+ "tmux",
11222
+ ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11223
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11224
+ ).trim();
11225
+ return altFlag === "1";
11226
+ } catch {
11227
+ return false;
11228
+ }
11229
+ }
11230
+ /**
11231
+ * Capture and send the visible pane content. Converts bare \n to \r\n for
11232
+ * xterm.js and strips the trailing \r\n to prevent an extra scroll that
11233
+ * would shift the viewport by 1 row. Returns the number of lines sent
11234
+ * (0 when the capture failed or was empty) for relative cursor sync.
11235
+ */
11236
+ sendVisiblePane(send, paneTarget, cleanEnv, alternateScreen) {
11237
+ try {
11238
+ const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11239
+ const initial = (0, import_node_child_process2.execFileSync)(
11240
+ "tmux",
11241
+ captureArgs,
11242
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11243
+ );
11244
+ if (initial) {
11245
+ const converted = stripAlternateScreenModeSwitches(
11246
+ initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")
11172
11247
  );
11173
- if (initial) {
11174
- send(stripAlternateScreenModeSwitches(initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")));
11248
+ if (converted) {
11249
+ send(converted);
11250
+ return converted.split("\r\n").length;
11175
11251
  }
11176
- } catch {
11177
11252
  }
11178
- try {
11179
- const cursorInfo = (0, import_node_child_process2.execFileSync)(
11180
- "tmux",
11181
- ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11182
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11183
- ).trim();
11184
- const parts = cursorInfo.split(" ");
11185
- if (parts.length === 2) {
11186
- const x = parseInt(parts[0], 10);
11187
- const y = parseInt(parts[1], 10);
11188
- if (!isNaN(x) && !isNaN(y)) {
11189
- send(`\x1B[${y + 1};${x + 1}H`);
11190
- }
11253
+ } catch {
11254
+ }
11255
+ return 0;
11256
+ }
11257
+ /** Query the tmux cursor position and send a relative cursor sync. */
11258
+ sendCursorSync(send, paneTarget, cleanEnv, lineCount) {
11259
+ if (lineCount <= 0) return;
11260
+ try {
11261
+ const cursorInfo = (0, import_node_child_process2.execFileSync)(
11262
+ "tmux",
11263
+ ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11264
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11265
+ ).trim();
11266
+ const parts = cursorInfo.split(" ");
11267
+ if (parts.length === 2) {
11268
+ const x = parseInt(parts[0], 10);
11269
+ const y = parseInt(parts[1], 10);
11270
+ if (!isNaN(x) && !isNaN(y)) {
11271
+ const seq = buildRelativeCursorSync(lineCount, x, y);
11272
+ if (seq) send(seq);
11191
11273
  }
11192
- } catch {
11193
11274
  }
11194
- stream.initializing = false;
11195
- stream.initBuffer = [];
11196
- log17.info("resync capture", { sessionId });
11197
- return true;
11198
- } catch (error) {
11199
- stream.initializing = false;
11200
- stream.initBuffer = [];
11201
- const message = error instanceof Error ? error.message : String(error);
11202
- log17.warn("failed to resync stream", { sessionId, error: message });
11203
- return false;
11275
+ } catch {
11204
11276
  }
11205
11277
  }
11206
11278
  // ---------------------------------------------------------------------------
@@ -11285,17 +11357,10 @@ var SessionStreamer = class {
11285
11357
  this.onStreamStopped?.(sessionId);
11286
11358
  }
11287
11359
  });
11288
- let alternateScreen = false;
11289
- try {
11290
- const altFlag = (0, import_node_child_process2.execFileSync)(
11291
- "tmux",
11292
- ["display-message", "-t", paneTarget, "-p", "#{alternate_on}"],
11293
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11294
- ).trim();
11295
- alternateScreen = altFlag === "1";
11296
- } catch {
11297
- }
11360
+ this.sendResetFn?.(sessionId, "full");
11361
+ const alternateScreen = this.detectAlternateScreen(paneTarget, cleanEnv);
11298
11362
  stream.alternateScreen = alternateScreen;
11363
+ this.sendFn(sessionId, "\x1B[3J");
11299
11364
  try {
11300
11365
  const scrollback = (0, import_node_child_process2.execFileSync)(
11301
11366
  "tmux",
@@ -11308,34 +11373,9 @@ var SessionStreamer = class {
11308
11373
  } catch {
11309
11374
  }
11310
11375
  this.sendFn(sessionId, "\x1B[?1049l\x1B[0m\x1B[2J\x1B[H");
11311
- try {
11312
- const captureArgs = alternateScreen ? ["capture-pane", "-a", "-t", paneTarget, "-p", "-e"] : ["capture-pane", "-t", paneTarget, "-p", "-e"];
11313
- const initial = (0, import_node_child_process2.execFileSync)(
11314
- "tmux",
11315
- captureArgs,
11316
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11317
- );
11318
- if (initial) {
11319
- this.sendFn(sessionId, stripAlternateScreenModeSwitches(initial.replace(/\n/g, "\r\n").replace(/\r\n$/, "")));
11320
- }
11321
- } catch {
11322
- }
11323
- try {
11324
- const cursorInfo = (0, import_node_child_process2.execFileSync)(
11325
- "tmux",
11326
- ["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
11327
- { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
11328
- ).trim();
11329
- const parts = cursorInfo.split(" ");
11330
- if (parts.length === 2) {
11331
- const x = parseInt(parts[0], 10);
11332
- const y = parseInt(parts[1], 10);
11333
- if (!isNaN(x) && !isNaN(y)) {
11334
- this.sendFn(sessionId, `\x1B[${y + 1};${x + 1}H`);
11335
- }
11336
- }
11337
- } catch {
11338
- }
11376
+ const sendBroadcast = (data) => this.sendFn(sessionId, data);
11377
+ const lineCount = this.sendVisiblePane(sendBroadcast, paneTarget, cleanEnv, alternateScreen);
11378
+ this.sendCursorSync(sendBroadcast, paneTarget, cleanEnv, lineCount);
11339
11379
  stream.initializing = false;
11340
11380
  stream.initBuffer = [];
11341
11381
  log17.info("started streaming (tmux)", { sessionId, pane: paneTarget });
@@ -11697,6 +11737,11 @@ var ANSI_KEY_SEQUENCES = /* @__PURE__ */ new Map([
11697
11737
  ["\x1B[6~", "PageDown"]
11698
11738
  ]);
11699
11739
  var ANSI_KEY_ENTRIES = Array.from(ANSI_KEY_SEQUENCES.entries());
11740
+ var MOUSE_OR_FOCUS_REPORT_RE = /\x1b\[(?:<\d+;\d+;\d+[Mm]|\d+;\d+;\d+M|M[\x20-\xFF]{3}|[IO])/g;
11741
+ function sanitizeRemoteInput(data) {
11742
+ if (!data.includes("\x1B[")) return data;
11743
+ return data.replace(MOUSE_OR_FOCUS_REPORT_RE, "");
11744
+ }
11700
11745
  function controlKeyName(charCode) {
11701
11746
  if (charCode >= 1 && charCode <= 26) {
11702
11747
  return `C-${String.fromCharCode(charCode + 96)}`;
@@ -11727,7 +11772,8 @@ function appendLiteralSegment(segments, value) {
11727
11772
  segments.push({ kind: "literal", value });
11728
11773
  }
11729
11774
  }
11730
- function encodeTmuxInput(data) {
11775
+ function encodeTmuxInput(rawData) {
11776
+ const data = sanitizeRemoteInput(rawData);
11731
11777
  const segments = [];
11732
11778
  let literal = "";
11733
11779
  const flushLiteral = () => {
@@ -13310,7 +13356,9 @@ var ControlDispatcher = class {
13310
13356
  this.handleInput(writtenPath, sessionId);
13311
13357
  }
13312
13358
  /** Handle input routing to the correct target. */
13313
- handleInput(data, sessionId) {
13359
+ handleInput(rawData, sessionId) {
13360
+ const data = sanitizeRemoteInput(rawData);
13361
+ if (!data) return;
13314
13362
  if (!sessionId || sessionId.startsWith("pty:")) {
13315
13363
  this.shell.write(data);
13316
13364
  } else if (sessionId.startsWith("tmux:")) {
@@ -13689,12 +13737,11 @@ var ControlDispatcher = class {
13689
13737
  const viewerKey = viewerId?.trim() || "";
13690
13738
  if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:") || sessionId.startsWith("process:")) {
13691
13739
  const alreadyStreaming = this.streamer.isStreaming(sessionId);
13692
- const viewerAlreadyAttached = alreadyStreaming && this.streamer.hasViewer(sessionId, viewerKey);
13693
13740
  const started = this.streamer.startStream(sessionId, viewerId ?? void 0);
13694
13741
  if (ws && ws.readyState === import_ws4.default.OPEN && group) {
13695
13742
  if (started) {
13696
- if (alreadyStreaming && !viewerAlreadyAttached) {
13697
- this.streamer.resyncStream(sessionId, viewerKey);
13743
+ if (alreadyStreaming) {
13744
+ this.streamer.resyncStream(sessionId, viewerKey, { scrollback: true });
13698
13745
  }
13699
13746
  const dims = this.queryPaneDimensions(sessionId);
13700
13747
  sendToGroup(ws, group, { type: "stream-started", sessionId, ...dims });
@@ -14308,6 +14355,24 @@ async function runAgent(rawOptions) {
14308
14355
  }
14309
14356
  if (!conn.isReady()) return;
14310
14357
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "stream-stopped", sessionId });
14358
+ },
14359
+ (sessionId, mode, viewerId) => {
14360
+ if (!conn.isReady()) return;
14361
+ const tail = flushStreamingRedaction(sessionId);
14362
+ if (tail) {
14363
+ if (conn.sessionKey) {
14364
+ const { enc, iv, seq } = encryptPayload(tail, conn.sessionKey, conn.txSeq.next());
14365
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
14366
+ } else {
14367
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: tail, sessionId });
14368
+ }
14369
+ }
14370
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, {
14371
+ type: "stream-reset",
14372
+ sessionId,
14373
+ mode,
14374
+ ...viewerId ? { viewerId } : {}
14375
+ });
14311
14376
  }
14312
14377
  );
14313
14378
  const conn = new ConnectionManager(sessionStreamer, outputBuffer);
package/dist/sbom.json CHANGED
@@ -1299,8 +1299,8 @@
1299
1299
  {
1300
1300
  "type": "library",
1301
1301
  "name": "ragent-cli",
1302
- "version": "1.11.14",
1303
- "bom-ref": "ragent-live|ragent-cli@1.11.14",
1302
+ "version": "1.11.15",
1303
+ "bom-ref": "ragent-live|ragent-cli@1.11.15",
1304
1304
  "author": "Intellimetrics",
1305
1305
  "description": "CLI agent for rAgent Live — browser-first terminal control plane for AI coding agents",
1306
1306
  "licenses": [
@@ -1311,7 +1311,7 @@
1311
1311
  }
1312
1312
  }
1313
1313
  ],
1314
- "purl": "pkg:npm/ragent-cli@1.11.14",
1314
+ "purl": "pkg:npm/ragent-cli@1.11.15",
1315
1315
  "externalReferences": [
1316
1316
  {
1317
1317
  "url": "https://github.com/chadlindell/ragent-live/issues",
@@ -1436,7 +1436,7 @@
1436
1436
  "ragent-live|@emnapi/wasi-threads@1.2.1",
1437
1437
  "ragent-live|@pkgjs/parseargs@0.11.0",
1438
1438
  "ragent-live|@tybys/wasm-util@0.10.1",
1439
- "ragent-live|ragent-cli@1.11.14"
1439
+ "ragent-live|ragent-cli@1.11.15"
1440
1440
  ]
1441
1441
  },
1442
1442
  {
@@ -1584,7 +1584,7 @@
1584
1584
  ]
1585
1585
  },
1586
1586
  {
1587
- "ref": "ragent-live|ragent-cli@1.11.14",
1587
+ "ref": "ragent-live|ragent-cli@1.11.15",
1588
1588
  "dependsOn": [
1589
1589
  "ragent-live|@azure/web-pubsub-client@1.0.4",
1590
1590
  "ragent-live|@openai/codex-sdk@0.130.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.11.14",
3
+ "version": "1.11.15",
4
4
  "description": "CLI agent for rAgent Live — browser-first terminal control plane for AI coding agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {