ragent-cli 1.4.4 → 1.5.0

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.
Files changed (2) hide show
  1. package/dist/index.js +555 -46
  2. package/package.json +1 -1
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.4.4",
34
+ version: "1.5.0",
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: {
@@ -102,7 +102,7 @@ var require_package = __commonJS({
102
102
  });
103
103
 
104
104
  // src/index.ts
105
- var fs5 = __toESM(require("fs"));
105
+ var fs6 = __toESM(require("fs"));
106
106
  var import_commander = require("commander");
107
107
 
108
108
  // src/constants.ts
@@ -257,12 +257,12 @@ async function maybeWarnUpdate() {
257
257
  }
258
258
 
259
259
  // src/commands/connect.ts
260
- var os7 = __toESM(require("os"));
260
+ var os8 = __toESM(require("os"));
261
261
 
262
262
  // src/agent.ts
263
- var fs3 = __toESM(require("fs"));
264
- var os6 = __toESM(require("os"));
265
- var path2 = __toESM(require("path"));
263
+ var fs4 = __toESM(require("fs"));
264
+ var os7 = __toESM(require("os"));
265
+ var path3 = __toESM(require("path"));
266
266
  var import_ws5 = __toESM(require("ws"));
267
267
 
268
268
  // src/auth.ts
@@ -1121,6 +1121,7 @@ var SessionStreamer = class {
1121
1121
  if (!stream || stream.stopped || stream.streamType !== "tmux-pipe") return false;
1122
1122
  const { paneTarget, cleanEnv } = stream;
1123
1123
  if (!paneTarget || !cleanEnv) return false;
1124
+ stream.initializing = true;
1124
1125
  try {
1125
1126
  try {
1126
1127
  const scrollback = (0, import_node_child_process2.execFileSync)(
@@ -1129,7 +1130,7 @@ var SessionStreamer = class {
1129
1130
  { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
1130
1131
  );
1131
1132
  if (scrollback && scrollback.length > 0) {
1132
- this.sendFn(sessionId, scrollback);
1133
+ this.sendFn(sessionId, scrollback.replace(/\n/g, "\r\n"));
1133
1134
  }
1134
1135
  } catch {
1135
1136
  }
@@ -1141,7 +1142,7 @@ var SessionStreamer = class {
1141
1142
  { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
1142
1143
  );
1143
1144
  if (initial) {
1144
- this.sendFn(sessionId, initial);
1145
+ this.sendFn(sessionId, initial.replace(/\n/g, "\r\n").replace(/\r\n$/, ""));
1145
1146
  }
1146
1147
  } catch {
1147
1148
  }
@@ -1161,9 +1162,13 @@ var SessionStreamer = class {
1161
1162
  }
1162
1163
  } catch {
1163
1164
  }
1165
+ stream.initializing = false;
1166
+ stream.initBuffer = [];
1164
1167
  console.log(`[rAgent] Resync capture for: ${sessionId}`);
1165
1168
  return true;
1166
1169
  } catch (error) {
1170
+ stream.initializing = false;
1171
+ stream.initBuffer = [];
1167
1172
  const message = error instanceof Error ? error.message : String(error);
1168
1173
  console.warn(`[rAgent] Failed to resync stream for ${sessionId}: ${message}`);
1169
1174
  return false;
@@ -1250,7 +1255,7 @@ var SessionStreamer = class {
1250
1255
  { env: cleanEnv, timeout: 1e4, encoding: "utf-8" }
1251
1256
  );
1252
1257
  if (scrollback && scrollback.length > 0) {
1253
- this.sendFn(sessionId, scrollback);
1258
+ this.sendFn(sessionId, scrollback.replace(/\n/g, "\r\n"));
1254
1259
  }
1255
1260
  } catch {
1256
1261
  }
@@ -1262,7 +1267,7 @@ var SessionStreamer = class {
1262
1267
  { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
1263
1268
  );
1264
1269
  if (initial) {
1265
- this.sendFn(sessionId, initial);
1270
+ this.sendFn(sessionId, initial.replace(/\n/g, "\r\n").replace(/\r\n$/, ""));
1266
1271
  }
1267
1272
  } catch {
1268
1273
  }
@@ -2404,17 +2409,19 @@ var ControlDispatcher = class {
2404
2409
  streamer;
2405
2410
  inventory;
2406
2411
  connection;
2412
+ transcriptWatcher;
2407
2413
  options;
2408
2414
  /** Set to true when a reconnect was requested (restart-agent, disconnect). */
2409
2415
  reconnectRequested = false;
2410
2416
  /** Set to false to stop the agent. */
2411
2417
  shouldRun = true;
2412
- constructor(shell, streamer, inventory, connection, options) {
2418
+ constructor(shell, streamer, inventory, connection, options, transcriptWatcher) {
2413
2419
  this.shell = shell;
2414
2420
  this.streamer = streamer;
2415
2421
  this.inventory = inventory;
2416
2422
  this.connection = connection;
2417
2423
  this.options = options;
2424
+ this.transcriptWatcher = transcriptWatcher ?? null;
2418
2425
  }
2419
2426
  /** Update options (e.g., after token refresh). */
2420
2427
  updateOptions(options) {
@@ -2529,6 +2536,15 @@ var ControlDispatcher = class {
2529
2536
  case "stop-stream":
2530
2537
  if (sessionId) this.streamer.stopStream(sessionId);
2531
2538
  return;
2539
+ case "prefer-markdown":
2540
+ this.handlePreferMarkdown(sessionId, payload);
2541
+ return;
2542
+ case "sync-markdown":
2543
+ if (sessionId && this.transcriptWatcher) {
2544
+ const fromSeq = typeof payload.fromSeq === "number" ? payload.fromSeq : void 0;
2545
+ this.transcriptWatcher.handleSyncRequest(sessionId, fromSeq);
2546
+ }
2547
+ return;
2532
2548
  default:
2533
2549
  }
2534
2550
  }
@@ -2543,32 +2559,14 @@ var ControlDispatcher = class {
2543
2559
  this.streamer.writeInput(sessionId, data);
2544
2560
  }
2545
2561
  }
2546
- /** Handle resize routing. */
2562
+ /** Handle resize routing (PTY and screen/zellij only — tmux panes are never resized by the portal). */
2547
2563
  handleResize(cols, rows, sessionId) {
2548
2564
  if (!sessionId || sessionId.startsWith("pty:")) {
2549
2565
  this.shell.resize(cols, rows);
2550
- } else if (sessionId.startsWith("tmux:")) {
2551
- this.resizeTmuxPane(sessionId, cols, rows);
2552
2566
  } else if (sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
2553
2567
  this.streamer.resize(sessionId, cols, rows);
2554
2568
  }
2555
2569
  }
2556
- /** Resize a tmux pane to match the viewer's terminal dimensions. */
2557
- resizeTmuxPane(sessionId, cols, rows) {
2558
- const paneTarget = sessionId.slice("tmux:".length);
2559
- if (!paneTarget) return;
2560
- const cleanEnv = { ...process.env };
2561
- delete cleanEnv.TMUX;
2562
- delete cleanEnv.TMUX_PANE;
2563
- try {
2564
- (0, import_child_process4.execFileSync)("tmux", ["resize-pane", "-t", paneTarget, "-x", String(cols), "-y", String(rows)], {
2565
- env: cleanEnv,
2566
- timeout: 5e3,
2567
- stdio: "ignore"
2568
- });
2569
- } catch {
2570
- }
2571
- }
2572
2570
  /** Handle provision request from dashboard. */
2573
2571
  async handleProvision(payload) {
2574
2572
  const provReq = payload;
@@ -2596,6 +2594,23 @@ var ControlDispatcher = class {
2596
2594
  });
2597
2595
  }
2598
2596
  }
2597
+ handlePreferMarkdown(sessionId, payload) {
2598
+ if (!sessionId || !this.transcriptWatcher) return;
2599
+ const enabled = payload.enabled === true;
2600
+ if (enabled) {
2601
+ const agentType = typeof payload.agentType === "string" ? payload.agentType : void 0;
2602
+ const success = this.transcriptWatcher.enableMarkdown(sessionId, agentType);
2603
+ if (!success) {
2604
+ console.log(`[rAgent] Could not enable markdown for ${sessionId}`);
2605
+ }
2606
+ } else {
2607
+ this.transcriptWatcher.disableMarkdown(sessionId);
2608
+ }
2609
+ }
2610
+ /** Stop all transcript watchers (called on disconnect/cleanup). */
2611
+ stopTranscriptWatchers() {
2612
+ this.transcriptWatcher?.stopAll();
2613
+ }
2599
2614
  async syncInventory(force = false) {
2600
2615
  await this.inventory.syncInventory(
2601
2616
  this.connection.activeSocket,
@@ -2640,6 +2655,30 @@ var ControlDispatcher = class {
2640
2655
  }
2641
2656
  await this.syncInventory(true);
2642
2657
  }
2658
+ /** Query the actual cols/rows of a tmux pane (non-critical). */
2659
+ queryPaneDimensions(sessionId) {
2660
+ if (!sessionId.startsWith("tmux:")) return {};
2661
+ const paneTarget = sessionId.slice("tmux:".length);
2662
+ if (!paneTarget) return {};
2663
+ const cleanEnv = { ...process.env };
2664
+ delete cleanEnv.TMUX;
2665
+ delete cleanEnv.TMUX_PANE;
2666
+ try {
2667
+ const info = (0, import_child_process4.execFileSync)(
2668
+ "tmux",
2669
+ ["display-message", "-t", paneTarget, "-p", "#{pane_width} #{pane_height}"],
2670
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
2671
+ ).trim();
2672
+ const parts = info.split(" ");
2673
+ if (parts.length === 2) {
2674
+ const cols = parseInt(parts[0], 10);
2675
+ const rows = parseInt(parts[1], 10);
2676
+ if (cols > 0 && rows > 0) return { cols, rows };
2677
+ }
2678
+ } catch {
2679
+ }
2680
+ return {};
2681
+ }
2643
2682
  handleStreamSession(sessionId) {
2644
2683
  if (!sessionId) return;
2645
2684
  const ws = this.connection.activeSocket;
@@ -2652,7 +2691,8 @@ var ControlDispatcher = class {
2652
2691
  if (alreadyStreaming) {
2653
2692
  this.streamer.resyncStream(sessionId);
2654
2693
  }
2655
- sendToGroup(ws, group, { type: "stream-started", sessionId });
2694
+ const dims = this.queryPaneDimensions(sessionId);
2695
+ sendToGroup(ws, group, { type: "stream-started", sessionId, ...dims });
2656
2696
  } else {
2657
2697
  sendToGroup(ws, group, {
2658
2698
  type: "stream-error",
@@ -2673,13 +2713,440 @@ var ControlDispatcher = class {
2673
2713
  }
2674
2714
  };
2675
2715
 
2716
+ // src/transcript-watcher.ts
2717
+ var fs3 = __toESM(require("fs"));
2718
+ var path2 = __toESM(require("path"));
2719
+ var os6 = __toESM(require("os"));
2720
+ var import_child_process5 = require("child_process");
2721
+ var ClaudeCodeParser = class {
2722
+ name = "claude-code";
2723
+ parseLine(line) {
2724
+ let obj;
2725
+ try {
2726
+ obj = JSON.parse(line);
2727
+ } catch {
2728
+ return null;
2729
+ }
2730
+ if (obj.type !== "assistant" || !obj.message?.content) return null;
2731
+ const content = obj.message.content;
2732
+ const textBlocks = [];
2733
+ const tools = [];
2734
+ for (const block of content) {
2735
+ if (block.type === "text" && typeof block.text === "string") {
2736
+ textBlocks.push(block.text);
2737
+ } else if (block.type === "tool_use" && block.name) {
2738
+ tools.push({
2739
+ name: block.name,
2740
+ input: block.input,
2741
+ status: "started"
2742
+ });
2743
+ }
2744
+ }
2745
+ const text = textBlocks.join("\n").trim();
2746
+ if (!text && tools.length === 0) return null;
2747
+ return {
2748
+ turnId: obj.uuid ?? `turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2749
+ role: "assistant",
2750
+ content: text,
2751
+ tools: tools.length > 0 ? tools : void 0,
2752
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2753
+ };
2754
+ }
2755
+ };
2756
+ var CodexCliParser = class {
2757
+ name = "codex-cli";
2758
+ parseLine(line) {
2759
+ let obj;
2760
+ try {
2761
+ obj = JSON.parse(line);
2762
+ } catch {
2763
+ return null;
2764
+ }
2765
+ const item = obj.response_item;
2766
+ if (!item) return null;
2767
+ if (item.type === "response.output_item.done" && item.item?.type === "message") {
2768
+ const textParts = (item.item.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text);
2769
+ const text = textParts.join("\n").trim();
2770
+ if (!text) return null;
2771
+ return {
2772
+ turnId: item.id ?? `codex-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2773
+ role: "assistant",
2774
+ content: text,
2775
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2776
+ };
2777
+ }
2778
+ return null;
2779
+ }
2780
+ };
2781
+ function discoverViaProc(panePid) {
2782
+ try {
2783
+ const children = (0, import_child_process5.execFileSync)("pgrep", ["-P", String(panePid)], {
2784
+ encoding: "utf-8",
2785
+ timeout: 3e3
2786
+ }).trim().split("\n").filter(Boolean);
2787
+ for (const childPid of children) {
2788
+ const fdDir = `/proc/${childPid}/fd`;
2789
+ try {
2790
+ const fds = fs3.readdirSync(fdDir);
2791
+ for (const fd of fds) {
2792
+ try {
2793
+ const target = fs3.readlinkSync(path2.join(fdDir, fd));
2794
+ if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
2795
+ return target;
2796
+ }
2797
+ } catch {
2798
+ }
2799
+ }
2800
+ } catch {
2801
+ }
2802
+ try {
2803
+ const grandchildren = (0, import_child_process5.execFileSync)("pgrep", ["-P", childPid], {
2804
+ encoding: "utf-8",
2805
+ timeout: 3e3
2806
+ }).trim().split("\n").filter(Boolean);
2807
+ for (const gcPid of grandchildren) {
2808
+ const gcFdDir = `/proc/${gcPid}/fd`;
2809
+ try {
2810
+ const fds = fs3.readdirSync(gcFdDir);
2811
+ for (const fd of fds) {
2812
+ try {
2813
+ const target = fs3.readlinkSync(path2.join(gcFdDir, fd));
2814
+ if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
2815
+ return target;
2816
+ }
2817
+ } catch {
2818
+ }
2819
+ }
2820
+ } catch {
2821
+ }
2822
+ }
2823
+ } catch {
2824
+ }
2825
+ }
2826
+ } catch {
2827
+ }
2828
+ return null;
2829
+ }
2830
+ function discoverViaCwd(paneCwd) {
2831
+ const claudeProjectsDir = path2.join(os6.homedir(), ".claude", "projects");
2832
+ if (!fs3.existsSync(claudeProjectsDir)) return null;
2833
+ const resolvedCwd = fs3.realpathSync(paneCwd);
2834
+ const expectedDirName = resolvedCwd.replace(/\//g, "-");
2835
+ const projectDir = path2.join(claudeProjectsDir, expectedDirName);
2836
+ if (!fs3.existsSync(projectDir)) return null;
2837
+ try {
2838
+ const jsonlFiles = fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
2839
+ const fullPath = path2.join(projectDir, f);
2840
+ const stat = fs3.statSync(fullPath);
2841
+ return { path: fullPath, mtime: stat.mtimeMs };
2842
+ }).sort((a, b) => b.mtime - a.mtime);
2843
+ return jsonlFiles.length > 0 ? jsonlFiles[0].path : null;
2844
+ } catch {
2845
+ return null;
2846
+ }
2847
+ }
2848
+ function discoverCodexTranscript() {
2849
+ const codexSessionsDir = path2.join(os6.homedir(), ".codex", "sessions");
2850
+ if (!fs3.existsSync(codexSessionsDir)) return null;
2851
+ try {
2852
+ const dateDirs = fs3.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
2853
+ for (const dateDir of dateDirs.slice(0, 3)) {
2854
+ const fullDir = path2.join(codexSessionsDir, dateDir);
2855
+ const jsonlFiles = fs3.readdirSync(fullDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")).map((f) => {
2856
+ const fp = path2.join(fullDir, f);
2857
+ const stat = fs3.statSync(fp);
2858
+ return { path: fp, mtime: stat.mtimeMs };
2859
+ }).sort((a, b) => b.mtime - a.mtime);
2860
+ if (jsonlFiles.length > 0) return jsonlFiles[0].path;
2861
+ }
2862
+ } catch {
2863
+ }
2864
+ return null;
2865
+ }
2866
+ function discoverTranscriptFile(sessionId, agentType) {
2867
+ if (agentType === "Codex CLI") {
2868
+ return discoverCodexTranscript();
2869
+ }
2870
+ if (sessionId.startsWith("tmux:")) {
2871
+ const paneTarget = sessionId.slice("tmux:".length);
2872
+ if (!paneTarget) return null;
2873
+ const cleanEnv = { ...process.env };
2874
+ delete cleanEnv.TMUX;
2875
+ delete cleanEnv.TMUX_PANE;
2876
+ try {
2877
+ const pidStr = (0, import_child_process5.execFileSync)(
2878
+ "tmux",
2879
+ ["display-message", "-t", paneTarget, "-p", "#{pane_pid}"],
2880
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
2881
+ ).trim();
2882
+ const panePid = parseInt(pidStr, 10);
2883
+ if (panePid > 0) {
2884
+ const procResult = discoverViaProc(panePid);
2885
+ if (procResult) return procResult;
2886
+ }
2887
+ } catch {
2888
+ }
2889
+ try {
2890
+ const paneCwd = (0, import_child_process5.execFileSync)(
2891
+ "tmux",
2892
+ ["display-message", "-t", paneTarget, "-p", "#{pane_current_path}"],
2893
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
2894
+ ).trim();
2895
+ if (paneCwd) return discoverViaCwd(paneCwd);
2896
+ } catch {
2897
+ }
2898
+ }
2899
+ return null;
2900
+ }
2901
+ var MAX_PARTIAL_BUFFER = 256 * 1024;
2902
+ var MAX_REPLAY_TURNS = 200;
2903
+ var POLL_INTERVAL_MS = 800;
2904
+ var TranscriptWatcher = class {
2905
+ filePath;
2906
+ parser;
2907
+ callbacks;
2908
+ offset = 0;
2909
+ inode = 0;
2910
+ partialLine = "";
2911
+ seq = 0;
2912
+ turns = [];
2913
+ watcher = null;
2914
+ pollTimer = null;
2915
+ stopped = false;
2916
+ subscriberCount = 0;
2917
+ constructor(filePath, parser, callbacks) {
2918
+ this.filePath = filePath;
2919
+ this.parser = parser;
2920
+ this.callbacks = callbacks;
2921
+ }
2922
+ /** Start watching the file. Returns false if file doesn't exist. */
2923
+ start() {
2924
+ try {
2925
+ const stat = fs3.statSync(this.filePath);
2926
+ this.inode = stat.ino;
2927
+ this.offset = stat.size;
2928
+ } catch {
2929
+ return false;
2930
+ }
2931
+ this.stopped = false;
2932
+ this.subscriberCount++;
2933
+ if (this.subscriberCount === 1) {
2934
+ this.startWatching();
2935
+ }
2936
+ return true;
2937
+ }
2938
+ /** Add a subscriber (for concurrent viewers). */
2939
+ addSubscriber() {
2940
+ this.subscriberCount++;
2941
+ }
2942
+ /** Remove a subscriber. Stops watching when last subscriber leaves. */
2943
+ removeSubscriber() {
2944
+ this.subscriberCount = Math.max(0, this.subscriberCount - 1);
2945
+ if (this.subscriberCount === 0) {
2946
+ this.stop();
2947
+ }
2948
+ }
2949
+ /** Stop watching. */
2950
+ stop() {
2951
+ this.stopped = true;
2952
+ if (this.watcher) {
2953
+ this.watcher.close();
2954
+ this.watcher = null;
2955
+ }
2956
+ if (this.pollTimer) {
2957
+ clearInterval(this.pollTimer);
2958
+ this.pollTimer = null;
2959
+ }
2960
+ }
2961
+ /** Get accumulated turns for replay (up to MAX_REPLAY_TURNS). */
2962
+ getReplayTurns(fromSeq) {
2963
+ if (fromSeq !== void 0) {
2964
+ return this.turns.filter((_, i) => i >= fromSeq).slice(-MAX_REPLAY_TURNS);
2965
+ }
2966
+ return this.turns.slice(-MAX_REPLAY_TURNS);
2967
+ }
2968
+ /** Get current sequence number. */
2969
+ get currentSeq() {
2970
+ return this.seq;
2971
+ }
2972
+ /** Read and replay the full transcript from the start of the file. */
2973
+ replayFromStart() {
2974
+ const savedOffset = this.offset;
2975
+ this.offset = 0;
2976
+ this.partialLine = "";
2977
+ this.readNewData();
2978
+ if (this.offset < savedOffset) {
2979
+ this.offset = savedOffset;
2980
+ }
2981
+ }
2982
+ startWatching() {
2983
+ try {
2984
+ this.watcher = fs3.watch(this.filePath, () => {
2985
+ if (!this.stopped) this.readNewData();
2986
+ });
2987
+ this.watcher.on("error", () => {
2988
+ this.watcher?.close();
2989
+ this.watcher = null;
2990
+ });
2991
+ } catch {
2992
+ }
2993
+ this.pollTimer = setInterval(() => {
2994
+ if (!this.stopped) {
2995
+ this.checkForRotation();
2996
+ this.readNewData();
2997
+ }
2998
+ }, POLL_INTERVAL_MS);
2999
+ }
3000
+ checkForRotation() {
3001
+ try {
3002
+ const stat = fs3.statSync(this.filePath);
3003
+ if (stat.ino !== this.inode) {
3004
+ this.inode = stat.ino;
3005
+ this.offset = 0;
3006
+ this.partialLine = "";
3007
+ } else if (stat.size < this.offset) {
3008
+ this.offset = 0;
3009
+ this.partialLine = "";
3010
+ }
3011
+ } catch {
3012
+ }
3013
+ }
3014
+ readNewData() {
3015
+ let fd;
3016
+ try {
3017
+ fd = fs3.openSync(this.filePath, "r");
3018
+ } catch {
3019
+ return;
3020
+ }
3021
+ try {
3022
+ const stat = fs3.fstatSync(fd);
3023
+ if (stat.size <= this.offset) return;
3024
+ const readSize = Math.min(stat.size - this.offset, 256 * 1024);
3025
+ const buffer = Buffer.alloc(readSize);
3026
+ const bytesRead = fs3.readSync(fd, buffer, 0, readSize, this.offset);
3027
+ if (bytesRead === 0) return;
3028
+ this.offset += bytesRead;
3029
+ const chunk = buffer.subarray(0, bytesRead).toString("utf-8");
3030
+ this.processChunk(chunk);
3031
+ } finally {
3032
+ fs3.closeSync(fd);
3033
+ }
3034
+ }
3035
+ processChunk(chunk) {
3036
+ const combined = this.partialLine + chunk;
3037
+ const lines = combined.split("\n");
3038
+ this.partialLine = lines.pop() ?? "";
3039
+ if (this.partialLine.length > MAX_PARTIAL_BUFFER) {
3040
+ this.callbacks.onError?.("Partial line buffer exceeded limit, discarding");
3041
+ this.partialLine = "";
3042
+ }
3043
+ for (const line of lines) {
3044
+ const trimmed = line.trim();
3045
+ if (!trimmed) continue;
3046
+ const turn = this.parser.parseLine(trimmed);
3047
+ if (turn) {
3048
+ this.seq++;
3049
+ this.turns.push({
3050
+ turnId: turn.turnId,
3051
+ role: turn.role,
3052
+ content: turn.content,
3053
+ tools: turn.tools,
3054
+ timestamp: turn.timestamp
3055
+ });
3056
+ if (this.turns.length > MAX_REPLAY_TURNS * 2) {
3057
+ this.turns = this.turns.slice(-MAX_REPLAY_TURNS);
3058
+ }
3059
+ this.callbacks.onTurn(turn, this.seq);
3060
+ }
3061
+ }
3062
+ }
3063
+ };
3064
+ var PARSERS = {
3065
+ "Claude Code": new ClaudeCodeParser(),
3066
+ "Codex CLI": new CodexCliParser()
3067
+ };
3068
+ function getParser(agentType) {
3069
+ if (!agentType) return void 0;
3070
+ return PARSERS[agentType];
3071
+ }
3072
+ var TranscriptWatcherManager = class {
3073
+ active = /* @__PURE__ */ new Map();
3074
+ sendFn;
3075
+ sendSnapshotFn;
3076
+ constructor(sendFn, sendSnapshotFn) {
3077
+ this.sendFn = sendFn;
3078
+ this.sendSnapshotFn = sendSnapshotFn;
3079
+ }
3080
+ /** Enable markdown streaming for a session. */
3081
+ enableMarkdown(sessionId, agentType) {
3082
+ const existing = this.active.get(sessionId);
3083
+ if (existing) {
3084
+ existing.watcher.addSubscriber();
3085
+ return true;
3086
+ }
3087
+ const parser = getParser(agentType);
3088
+ if (!parser) {
3089
+ console.log(`[rAgent] No transcript parser for agent type "${agentType}"`);
3090
+ return false;
3091
+ }
3092
+ const filePath = discoverTranscriptFile(sessionId, agentType);
3093
+ if (!filePath) {
3094
+ console.log(`[rAgent] No transcript file found for session ${sessionId}`);
3095
+ return false;
3096
+ }
3097
+ console.log(`[rAgent] Watching transcript: ${filePath} (${parser.name})`);
3098
+ const watcher = new TranscriptWatcher(filePath, parser, {
3099
+ onTurn: (turn, seq) => {
3100
+ this.sendFn(sessionId, turn, seq);
3101
+ },
3102
+ onError: (error) => {
3103
+ console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3104
+ }
3105
+ });
3106
+ if (!watcher.start()) {
3107
+ console.warn(`[rAgent] Failed to start watching ${filePath}`);
3108
+ return false;
3109
+ }
3110
+ this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
3111
+ watcher.replayFromStart();
3112
+ return true;
3113
+ }
3114
+ /** Disable markdown streaming for a session. */
3115
+ disableMarkdown(sessionId) {
3116
+ const session = this.active.get(sessionId);
3117
+ if (!session) return;
3118
+ session.watcher.removeSubscriber();
3119
+ this.active.delete(sessionId);
3120
+ console.log(`[rAgent] Stopped watching transcript for ${sessionId}`);
3121
+ }
3122
+ /** Handle sync-markdown request — send replay snapshot. */
3123
+ handleSyncRequest(sessionId, fromSeq) {
3124
+ const session = this.active.get(sessionId);
3125
+ if (!session) return;
3126
+ const turns = session.watcher.getReplayTurns(fromSeq);
3127
+ this.sendSnapshotFn(sessionId, turns, session.watcher.currentSeq);
3128
+ }
3129
+ /** Stop all watchers. */
3130
+ stopAll() {
3131
+ for (const [sessionId, session] of this.active) {
3132
+ session.watcher.stop();
3133
+ console.log(`[rAgent] Stopped transcript watcher for ${sessionId}`);
3134
+ }
3135
+ this.active.clear();
3136
+ }
3137
+ /** Check if a session has an active watcher. */
3138
+ isWatching(sessionId) {
3139
+ return this.active.has(sessionId);
3140
+ }
3141
+ };
3142
+
2676
3143
  // src/agent.ts
2677
3144
  function pidFilePath(hostId) {
2678
- return path2.join(CONFIG_DIR, `agent-${hostId}.pid`);
3145
+ return path3.join(CONFIG_DIR, `agent-${hostId}.pid`);
2679
3146
  }
2680
3147
  function readPidFile(filePath) {
2681
3148
  try {
2682
- const raw = fs3.readFileSync(filePath, "utf8").trim();
3149
+ const raw = fs4.readFileSync(filePath, "utf8").trim();
2683
3150
  const pid = Number.parseInt(raw, 10);
2684
3151
  return Number.isInteger(pid) && pid > 0 ? pid : null;
2685
3152
  } catch {
@@ -2704,7 +3171,7 @@ function acquirePidLock(hostId) {
2704
3171
  Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
2705
3172
  );
2706
3173
  }
2707
- fs3.writeFileSync(lockPath, `${process.pid}
3174
+ fs4.writeFileSync(lockPath, `${process.pid}
2708
3175
  `, "utf8");
2709
3176
  return lockPath;
2710
3177
  }
@@ -2712,7 +3179,7 @@ function releasePidLock(lockPath) {
2712
3179
  try {
2713
3180
  const currentPid = readPidFile(lockPath);
2714
3181
  if (currentPid === process.pid) {
2715
- fs3.unlinkSync(lockPath);
3182
+ fs4.unlinkSync(lockPath);
2716
3183
  }
2717
3184
  } catch {
2718
3185
  }
@@ -2721,7 +3188,7 @@ function resolveRunOptions(commandOptions) {
2721
3188
  const config = loadConfig();
2722
3189
  const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
2723
3190
  const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
2724
- const hostName = commandOptions.name || config.hostName || os6.hostname();
3191
+ const hostName = commandOptions.name || config.hostName || os7.hostname();
2725
3192
  const command = commandOptions.command || config.command || "bash";
2726
3193
  const agentToken = commandOptions.agentToken || config.agentToken || "";
2727
3194
  return { portal, hostId, hostName, command, agentToken };
@@ -2772,12 +3239,53 @@ async function runAgent(rawOptions) {
2772
3239
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: chunk, sessionId: ptySessionId });
2773
3240
  }
2774
3241
  };
3242
+ const transcriptWatcher = new TranscriptWatcherManager(
3243
+ (sessionId, turn, seq) => {
3244
+ if (!conn.isReady()) return;
3245
+ const payload = {
3246
+ type: "markdown",
3247
+ subtype: "delta",
3248
+ sessionId,
3249
+ seq,
3250
+ turnId: turn.turnId,
3251
+ role: turn.role,
3252
+ content: turn.content,
3253
+ tools: turn.tools,
3254
+ timestamp: turn.timestamp
3255
+ };
3256
+ if (conn.sessionKey) {
3257
+ const contentStr = JSON.stringify(payload);
3258
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
3259
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
3260
+ } else {
3261
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3262
+ }
3263
+ },
3264
+ (sessionId, turns, seq) => {
3265
+ if (!conn.isReady()) return;
3266
+ const payload = {
3267
+ type: "markdown",
3268
+ subtype: "snapshot",
3269
+ sessionId,
3270
+ seq,
3271
+ turns
3272
+ };
3273
+ if (conn.sessionKey) {
3274
+ const contentStr = JSON.stringify(payload);
3275
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
3276
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
3277
+ } else {
3278
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3279
+ }
3280
+ }
3281
+ );
2775
3282
  const shell = new ShellManager(options.command, sendOutput);
2776
3283
  const inventory = new InventoryManager(options);
2777
- const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options);
3284
+ const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options, transcriptWatcher);
2778
3285
  shell.spawn();
2779
3286
  const onSignal = () => {
2780
3287
  dispatcher.shouldRun = false;
3288
+ dispatcher.stopTranscriptWatchers();
2781
3289
  conn.cleanup({ stopStreams: true });
2782
3290
  shell.stop();
2783
3291
  releasePidLock(lockPath);
@@ -2813,7 +3321,7 @@ async function runAgent(rawOptions) {
2813
3321
  sendToGroup(ws, groups.privateGroup, {
2814
3322
  type: "register",
2815
3323
  hostName: options.hostName,
2816
- environment: os6.platform()
3324
+ environment: os7.platform()
2817
3325
  });
2818
3326
  conn.replayBufferedOutput((chunk) => {
2819
3327
  sendToGroup(ws, groups.privateGroup, {
@@ -2903,6 +3411,7 @@ async function runAgent(rawOptions) {
2903
3411
  conn.reconnectDelay = Math.min(conn.reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
2904
3412
  }
2905
3413
  } finally {
3414
+ dispatcher.stopTranscriptWatchers();
2906
3415
  conn.cleanup({ stopStreams: true });
2907
3416
  shell.stop();
2908
3417
  releasePidLock(lockPath);
@@ -2999,7 +3508,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
2999
3508
  async function connectMachine(opts) {
3000
3509
  const portal = opts.portal || DEFAULT_PORTAL;
3001
3510
  const hostId = sanitizeHostId(opts.id || inferHostId());
3002
- const hostName = opts.name || os7.hostname();
3511
+ const hostName = opts.name || os8.hostname();
3003
3512
  const command = opts.command || "bash";
3004
3513
  if (!opts.token) {
3005
3514
  throw new Error("Connection token is required.");
@@ -3073,12 +3582,12 @@ function registerRunCommand(program2) {
3073
3582
  }
3074
3583
 
3075
3584
  // src/commands/doctor.ts
3076
- var os8 = __toESM(require("os"));
3585
+ var os9 = __toESM(require("os"));
3077
3586
  async function runDoctor(opts) {
3078
3587
  const options = resolveRunOptions(opts);
3079
3588
  const checks = [];
3080
- const platformOk = os8.platform() === "linux";
3081
- checks.push({ name: "platform", ok: platformOk, detail: os8.platform() });
3589
+ const platformOk = os9.platform() === "linux";
3590
+ checks.push({ name: "platform", ok: platformOk, detail: os9.platform() });
3082
3591
  checks.push({
3083
3592
  name: "node",
3084
3593
  ok: Number(process.versions.node.split(".")[0]) >= 20,
@@ -3253,7 +3762,7 @@ function registerServiceCommand(program2) {
3253
3762
  }
3254
3763
 
3255
3764
  // src/commands/uninstall.ts
3256
- var fs4 = __toESM(require("fs"));
3765
+ var fs5 = __toESM(require("fs"));
3257
3766
  async function uninstallAgent(opts) {
3258
3767
  const config = loadConfig();
3259
3768
  const hostName = config.hostName || config.hostId || "this machine";
@@ -3284,8 +3793,8 @@ async function uninstallAgent(opts) {
3284
3793
  }
3285
3794
  console.log("[rAgent] Stopping and removing service...");
3286
3795
  await uninstallService().catch(() => void 0);
3287
- if (fs4.existsSync(CONFIG_DIR)) {
3288
- fs4.rmSync(CONFIG_DIR, { recursive: true, force: true });
3796
+ if (fs5.existsSync(CONFIG_DIR)) {
3797
+ fs5.rmSync(CONFIG_DIR, { recursive: true, force: true });
3289
3798
  console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
3290
3799
  }
3291
3800
  try {
@@ -3334,7 +3843,7 @@ function showStatus() {
3334
3843
  ragent v${CURRENT_VERSION}
3335
3844
  `);
3336
3845
  try {
3337
- const raw = fs5.readFileSync(CONFIG_FILE, "utf8");
3846
+ const raw = fs6.readFileSync(CONFIG_FILE, "utf8");
3338
3847
  const config = JSON.parse(raw);
3339
3848
  if (config.portal && config.agentToken) {
3340
3849
  console.log(` Status: Connected`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
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": {