ragent-cli 1.4.5 → 1.5.1

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 +599 -42
  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.5",
34
+ version: "1.5.1",
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
@@ -2409,17 +2409,19 @@ var ControlDispatcher = class {
2409
2409
  streamer;
2410
2410
  inventory;
2411
2411
  connection;
2412
+ transcriptWatcher;
2412
2413
  options;
2413
2414
  /** Set to true when a reconnect was requested (restart-agent, disconnect). */
2414
2415
  reconnectRequested = false;
2415
2416
  /** Set to false to stop the agent. */
2416
2417
  shouldRun = true;
2417
- constructor(shell, streamer, inventory, connection, options) {
2418
+ constructor(shell, streamer, inventory, connection, options, transcriptWatcher) {
2418
2419
  this.shell = shell;
2419
2420
  this.streamer = streamer;
2420
2421
  this.inventory = inventory;
2421
2422
  this.connection = connection;
2422
2423
  this.options = options;
2424
+ this.transcriptWatcher = transcriptWatcher ?? null;
2423
2425
  }
2424
2426
  /** Update options (e.g., after token refresh). */
2425
2427
  updateOptions(options) {
@@ -2534,6 +2536,15 @@ var ControlDispatcher = class {
2534
2536
  case "stop-stream":
2535
2537
  if (sessionId) this.streamer.stopStream(sessionId);
2536
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;
2537
2548
  default:
2538
2549
  }
2539
2550
  }
@@ -2548,32 +2559,14 @@ var ControlDispatcher = class {
2548
2559
  this.streamer.writeInput(sessionId, data);
2549
2560
  }
2550
2561
  }
2551
- /** Handle resize routing. */
2562
+ /** Handle resize routing (PTY and screen/zellij only — tmux panes are never resized by the portal). */
2552
2563
  handleResize(cols, rows, sessionId) {
2553
2564
  if (!sessionId || sessionId.startsWith("pty:")) {
2554
2565
  this.shell.resize(cols, rows);
2555
- } else if (sessionId.startsWith("tmux:")) {
2556
- this.resizeTmuxPane(sessionId, cols, rows);
2557
2566
  } else if (sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
2558
2567
  this.streamer.resize(sessionId, cols, rows);
2559
2568
  }
2560
2569
  }
2561
- /** Resize a tmux pane to match the viewer's terminal dimensions. */
2562
- resizeTmuxPane(sessionId, cols, rows) {
2563
- const paneTarget = sessionId.slice("tmux:".length);
2564
- if (!paneTarget) return;
2565
- const cleanEnv = { ...process.env };
2566
- delete cleanEnv.TMUX;
2567
- delete cleanEnv.TMUX_PANE;
2568
- try {
2569
- (0, import_child_process4.execFileSync)("tmux", ["resize-pane", "-t", paneTarget, "-x", String(cols), "-y", String(rows)], {
2570
- env: cleanEnv,
2571
- timeout: 5e3,
2572
- stdio: "ignore"
2573
- });
2574
- } catch {
2575
- }
2576
- }
2577
2570
  /** Handle provision request from dashboard. */
2578
2571
  async handleProvision(payload) {
2579
2572
  const provReq = payload;
@@ -2601,6 +2594,23 @@ var ControlDispatcher = class {
2601
2594
  });
2602
2595
  }
2603
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
+ }
2604
2614
  async syncInventory(force = false) {
2605
2615
  await this.inventory.syncInventory(
2606
2616
  this.connection.activeSocket,
@@ -2645,6 +2655,30 @@ var ControlDispatcher = class {
2645
2655
  }
2646
2656
  await this.syncInventory(true);
2647
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
+ }
2648
2682
  handleStreamSession(sessionId) {
2649
2683
  if (!sessionId) return;
2650
2684
  const ws = this.connection.activeSocket;
@@ -2657,7 +2691,8 @@ var ControlDispatcher = class {
2657
2691
  if (alreadyStreaming) {
2658
2692
  this.streamer.resyncStream(sessionId);
2659
2693
  }
2660
- sendToGroup(ws, group, { type: "stream-started", sessionId });
2694
+ const dims = this.queryPaneDimensions(sessionId);
2695
+ sendToGroup(ws, group, { type: "stream-started", sessionId, ...dims });
2661
2696
  } else {
2662
2697
  sendToGroup(ws, group, {
2663
2698
  type: "stream-error",
@@ -2678,13 +2713,493 @@ var ControlDispatcher = class {
2678
2713
  }
2679
2714
  };
2680
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
+ /** Maps tool_use id → tool name for matching results back to tools. */
2724
+ pendingTools = /* @__PURE__ */ new Map();
2725
+ parseLine(line) {
2726
+ let obj;
2727
+ try {
2728
+ obj = JSON.parse(line);
2729
+ } catch {
2730
+ return null;
2731
+ }
2732
+ if (!obj.message?.content) return null;
2733
+ if (obj.type === "assistant") {
2734
+ return this.parseAssistant(obj);
2735
+ }
2736
+ if (obj.type === "user") {
2737
+ return this.parseUserToolResults(obj);
2738
+ }
2739
+ return null;
2740
+ }
2741
+ parseAssistant(obj) {
2742
+ const content = obj.message.content;
2743
+ const textBlocks = [];
2744
+ const tools = [];
2745
+ for (const block of content) {
2746
+ if (block.type === "text" && typeof block.text === "string") {
2747
+ textBlocks.push(block.text);
2748
+ } else if (block.type === "tool_use" && block.name) {
2749
+ if (block.id) {
2750
+ this.pendingTools.set(block.id, block.name);
2751
+ }
2752
+ tools.push({
2753
+ name: block.name,
2754
+ input: block.input,
2755
+ status: "started"
2756
+ });
2757
+ }
2758
+ }
2759
+ if (this.pendingTools.size > 500) {
2760
+ const keys = [...this.pendingTools.keys()];
2761
+ for (let i = 0; i < 250; i++) {
2762
+ this.pendingTools.delete(keys[i]);
2763
+ }
2764
+ }
2765
+ const text = textBlocks.join("\n").trim();
2766
+ if (!text && tools.length === 0) return null;
2767
+ return {
2768
+ turnId: obj.uuid ?? `turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2769
+ role: "assistant",
2770
+ content: text,
2771
+ tools: tools.length > 0 ? tools : void 0,
2772
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2773
+ };
2774
+ }
2775
+ parseUserToolResults(obj) {
2776
+ const content = obj.message.content;
2777
+ const tools = [];
2778
+ for (const block of content) {
2779
+ if (block.type !== "tool_result") continue;
2780
+ const toolName = (block.tool_use_id && this.pendingTools.get(block.tool_use_id)) ?? "Tool";
2781
+ const resultText = this.extractToolResultText(block);
2782
+ if (block.tool_use_id) {
2783
+ this.pendingTools.delete(block.tool_use_id);
2784
+ }
2785
+ tools.push({
2786
+ name: toolName,
2787
+ status: block.is_error ? "error" : "completed",
2788
+ result: resultText || void 0
2789
+ });
2790
+ }
2791
+ if (tools.length === 0) return null;
2792
+ return {
2793
+ turnId: obj.uuid ?? `result-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2794
+ role: "user",
2795
+ content: "",
2796
+ tools,
2797
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2798
+ };
2799
+ }
2800
+ extractToolResultText(block) {
2801
+ const rc = block.content;
2802
+ if (typeof rc === "string") return rc;
2803
+ if (Array.isArray(rc)) {
2804
+ return rc.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n");
2805
+ }
2806
+ return "";
2807
+ }
2808
+ };
2809
+ var CodexCliParser = class {
2810
+ name = "codex-cli";
2811
+ parseLine(line) {
2812
+ let obj;
2813
+ try {
2814
+ obj = JSON.parse(line);
2815
+ } catch {
2816
+ return null;
2817
+ }
2818
+ const item = obj.response_item;
2819
+ if (!item) return null;
2820
+ if (item.type === "response.output_item.done" && item.item?.type === "message") {
2821
+ const textParts = (item.item.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text);
2822
+ const text = textParts.join("\n").trim();
2823
+ if (!text) return null;
2824
+ return {
2825
+ turnId: item.id ?? `codex-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2826
+ role: "assistant",
2827
+ content: text,
2828
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2829
+ };
2830
+ }
2831
+ return null;
2832
+ }
2833
+ };
2834
+ function discoverViaProc(panePid) {
2835
+ try {
2836
+ const children = (0, import_child_process5.execFileSync)("pgrep", ["-P", String(panePid)], {
2837
+ encoding: "utf-8",
2838
+ timeout: 3e3
2839
+ }).trim().split("\n").filter(Boolean);
2840
+ for (const childPid of children) {
2841
+ const fdDir = `/proc/${childPid}/fd`;
2842
+ try {
2843
+ const fds = fs3.readdirSync(fdDir);
2844
+ for (const fd of fds) {
2845
+ try {
2846
+ const target = fs3.readlinkSync(path2.join(fdDir, fd));
2847
+ if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
2848
+ return target;
2849
+ }
2850
+ } catch {
2851
+ }
2852
+ }
2853
+ } catch {
2854
+ }
2855
+ try {
2856
+ const grandchildren = (0, import_child_process5.execFileSync)("pgrep", ["-P", childPid], {
2857
+ encoding: "utf-8",
2858
+ timeout: 3e3
2859
+ }).trim().split("\n").filter(Boolean);
2860
+ for (const gcPid of grandchildren) {
2861
+ const gcFdDir = `/proc/${gcPid}/fd`;
2862
+ try {
2863
+ const fds = fs3.readdirSync(gcFdDir);
2864
+ for (const fd of fds) {
2865
+ try {
2866
+ const target = fs3.readlinkSync(path2.join(gcFdDir, fd));
2867
+ if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
2868
+ return target;
2869
+ }
2870
+ } catch {
2871
+ }
2872
+ }
2873
+ } catch {
2874
+ }
2875
+ }
2876
+ } catch {
2877
+ }
2878
+ }
2879
+ } catch {
2880
+ }
2881
+ return null;
2882
+ }
2883
+ function discoverViaCwd(paneCwd) {
2884
+ const claudeProjectsDir = path2.join(os6.homedir(), ".claude", "projects");
2885
+ if (!fs3.existsSync(claudeProjectsDir)) return null;
2886
+ const resolvedCwd = fs3.realpathSync(paneCwd);
2887
+ const expectedDirName = resolvedCwd.replace(/\//g, "-");
2888
+ const projectDir = path2.join(claudeProjectsDir, expectedDirName);
2889
+ if (!fs3.existsSync(projectDir)) return null;
2890
+ try {
2891
+ const jsonlFiles = fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
2892
+ const fullPath = path2.join(projectDir, f);
2893
+ const stat = fs3.statSync(fullPath);
2894
+ return { path: fullPath, mtime: stat.mtimeMs };
2895
+ }).sort((a, b) => b.mtime - a.mtime);
2896
+ return jsonlFiles.length > 0 ? jsonlFiles[0].path : null;
2897
+ } catch {
2898
+ return null;
2899
+ }
2900
+ }
2901
+ function discoverCodexTranscript() {
2902
+ const codexSessionsDir = path2.join(os6.homedir(), ".codex", "sessions");
2903
+ if (!fs3.existsSync(codexSessionsDir)) return null;
2904
+ try {
2905
+ const dateDirs = fs3.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
2906
+ for (const dateDir of dateDirs.slice(0, 3)) {
2907
+ const fullDir = path2.join(codexSessionsDir, dateDir);
2908
+ const jsonlFiles = fs3.readdirSync(fullDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")).map((f) => {
2909
+ const fp = path2.join(fullDir, f);
2910
+ const stat = fs3.statSync(fp);
2911
+ return { path: fp, mtime: stat.mtimeMs };
2912
+ }).sort((a, b) => b.mtime - a.mtime);
2913
+ if (jsonlFiles.length > 0) return jsonlFiles[0].path;
2914
+ }
2915
+ } catch {
2916
+ }
2917
+ return null;
2918
+ }
2919
+ function discoverTranscriptFile(sessionId, agentType) {
2920
+ if (agentType === "Codex CLI") {
2921
+ return discoverCodexTranscript();
2922
+ }
2923
+ if (sessionId.startsWith("tmux:")) {
2924
+ const paneTarget = sessionId.slice("tmux:".length);
2925
+ if (!paneTarget) return null;
2926
+ const cleanEnv = { ...process.env };
2927
+ delete cleanEnv.TMUX;
2928
+ delete cleanEnv.TMUX_PANE;
2929
+ try {
2930
+ const pidStr = (0, import_child_process5.execFileSync)(
2931
+ "tmux",
2932
+ ["display-message", "-t", paneTarget, "-p", "#{pane_pid}"],
2933
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
2934
+ ).trim();
2935
+ const panePid = parseInt(pidStr, 10);
2936
+ if (panePid > 0) {
2937
+ const procResult = discoverViaProc(panePid);
2938
+ if (procResult) return procResult;
2939
+ }
2940
+ } catch {
2941
+ }
2942
+ try {
2943
+ const paneCwd = (0, import_child_process5.execFileSync)(
2944
+ "tmux",
2945
+ ["display-message", "-t", paneTarget, "-p", "#{pane_current_path}"],
2946
+ { env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
2947
+ ).trim();
2948
+ if (paneCwd) return discoverViaCwd(paneCwd);
2949
+ } catch {
2950
+ }
2951
+ }
2952
+ return null;
2953
+ }
2954
+ var MAX_PARTIAL_BUFFER = 256 * 1024;
2955
+ var MAX_REPLAY_TURNS = 200;
2956
+ var POLL_INTERVAL_MS = 800;
2957
+ var TranscriptWatcher = class {
2958
+ filePath;
2959
+ parser;
2960
+ callbacks;
2961
+ offset = 0;
2962
+ inode = 0;
2963
+ partialLine = "";
2964
+ seq = 0;
2965
+ turns = [];
2966
+ watcher = null;
2967
+ pollTimer = null;
2968
+ stopped = false;
2969
+ subscriberCount = 0;
2970
+ constructor(filePath, parser, callbacks) {
2971
+ this.filePath = filePath;
2972
+ this.parser = parser;
2973
+ this.callbacks = callbacks;
2974
+ }
2975
+ /** Start watching the file. Returns false if file doesn't exist. */
2976
+ start() {
2977
+ try {
2978
+ const stat = fs3.statSync(this.filePath);
2979
+ this.inode = stat.ino;
2980
+ this.offset = stat.size;
2981
+ } catch {
2982
+ return false;
2983
+ }
2984
+ this.stopped = false;
2985
+ this.subscriberCount++;
2986
+ if (this.subscriberCount === 1) {
2987
+ this.startWatching();
2988
+ }
2989
+ return true;
2990
+ }
2991
+ /** Add a subscriber (for concurrent viewers). */
2992
+ addSubscriber() {
2993
+ this.subscriberCount++;
2994
+ }
2995
+ /** Remove a subscriber. Stops watching when last subscriber leaves. */
2996
+ removeSubscriber() {
2997
+ this.subscriberCount = Math.max(0, this.subscriberCount - 1);
2998
+ if (this.subscriberCount === 0) {
2999
+ this.stop();
3000
+ }
3001
+ }
3002
+ /** Stop watching. */
3003
+ stop() {
3004
+ this.stopped = true;
3005
+ if (this.watcher) {
3006
+ this.watcher.close();
3007
+ this.watcher = null;
3008
+ }
3009
+ if (this.pollTimer) {
3010
+ clearInterval(this.pollTimer);
3011
+ this.pollTimer = null;
3012
+ }
3013
+ }
3014
+ /** Get accumulated turns for replay (up to MAX_REPLAY_TURNS). */
3015
+ getReplayTurns(fromSeq) {
3016
+ if (fromSeq !== void 0) {
3017
+ return this.turns.filter((_, i) => i >= fromSeq).slice(-MAX_REPLAY_TURNS);
3018
+ }
3019
+ return this.turns.slice(-MAX_REPLAY_TURNS);
3020
+ }
3021
+ /** Get current sequence number. */
3022
+ get currentSeq() {
3023
+ return this.seq;
3024
+ }
3025
+ /** Read and replay the full transcript from the start of the file. */
3026
+ replayFromStart() {
3027
+ const savedOffset = this.offset;
3028
+ this.offset = 0;
3029
+ this.partialLine = "";
3030
+ this.readNewData();
3031
+ if (this.offset < savedOffset) {
3032
+ this.offset = savedOffset;
3033
+ }
3034
+ }
3035
+ startWatching() {
3036
+ try {
3037
+ this.watcher = fs3.watch(this.filePath, () => {
3038
+ if (!this.stopped) this.readNewData();
3039
+ });
3040
+ this.watcher.on("error", () => {
3041
+ this.watcher?.close();
3042
+ this.watcher = null;
3043
+ });
3044
+ } catch {
3045
+ }
3046
+ this.pollTimer = setInterval(() => {
3047
+ if (!this.stopped) {
3048
+ this.checkForRotation();
3049
+ this.readNewData();
3050
+ }
3051
+ }, POLL_INTERVAL_MS);
3052
+ }
3053
+ checkForRotation() {
3054
+ try {
3055
+ const stat = fs3.statSync(this.filePath);
3056
+ if (stat.ino !== this.inode) {
3057
+ this.inode = stat.ino;
3058
+ this.offset = 0;
3059
+ this.partialLine = "";
3060
+ } else if (stat.size < this.offset) {
3061
+ this.offset = 0;
3062
+ this.partialLine = "";
3063
+ }
3064
+ } catch {
3065
+ }
3066
+ }
3067
+ readNewData() {
3068
+ let fd;
3069
+ try {
3070
+ fd = fs3.openSync(this.filePath, "r");
3071
+ } catch {
3072
+ return;
3073
+ }
3074
+ try {
3075
+ const stat = fs3.fstatSync(fd);
3076
+ if (stat.size <= this.offset) return;
3077
+ const readSize = Math.min(stat.size - this.offset, 256 * 1024);
3078
+ const buffer = Buffer.alloc(readSize);
3079
+ const bytesRead = fs3.readSync(fd, buffer, 0, readSize, this.offset);
3080
+ if (bytesRead === 0) return;
3081
+ this.offset += bytesRead;
3082
+ const chunk = buffer.subarray(0, bytesRead).toString("utf-8");
3083
+ this.processChunk(chunk);
3084
+ } finally {
3085
+ fs3.closeSync(fd);
3086
+ }
3087
+ }
3088
+ processChunk(chunk) {
3089
+ const combined = this.partialLine + chunk;
3090
+ const lines = combined.split("\n");
3091
+ this.partialLine = lines.pop() ?? "";
3092
+ if (this.partialLine.length > MAX_PARTIAL_BUFFER) {
3093
+ this.callbacks.onError?.("Partial line buffer exceeded limit, discarding");
3094
+ this.partialLine = "";
3095
+ }
3096
+ for (const line of lines) {
3097
+ const trimmed = line.trim();
3098
+ if (!trimmed) continue;
3099
+ const turn = this.parser.parseLine(trimmed);
3100
+ if (turn) {
3101
+ this.seq++;
3102
+ this.turns.push({
3103
+ turnId: turn.turnId,
3104
+ role: turn.role,
3105
+ content: turn.content,
3106
+ tools: turn.tools,
3107
+ timestamp: turn.timestamp
3108
+ });
3109
+ if (this.turns.length > MAX_REPLAY_TURNS * 2) {
3110
+ this.turns = this.turns.slice(-MAX_REPLAY_TURNS);
3111
+ }
3112
+ this.callbacks.onTurn(turn, this.seq);
3113
+ }
3114
+ }
3115
+ }
3116
+ };
3117
+ var PARSERS = {
3118
+ "Claude Code": new ClaudeCodeParser(),
3119
+ "Codex CLI": new CodexCliParser()
3120
+ };
3121
+ function getParser(agentType) {
3122
+ if (!agentType) return void 0;
3123
+ return PARSERS[agentType];
3124
+ }
3125
+ var TranscriptWatcherManager = class {
3126
+ active = /* @__PURE__ */ new Map();
3127
+ sendFn;
3128
+ sendSnapshotFn;
3129
+ constructor(sendFn, sendSnapshotFn) {
3130
+ this.sendFn = sendFn;
3131
+ this.sendSnapshotFn = sendSnapshotFn;
3132
+ }
3133
+ /** Enable markdown streaming for a session. */
3134
+ enableMarkdown(sessionId, agentType) {
3135
+ const existing = this.active.get(sessionId);
3136
+ if (existing) {
3137
+ existing.watcher.addSubscriber();
3138
+ return true;
3139
+ }
3140
+ const parser = getParser(agentType);
3141
+ if (!parser) {
3142
+ console.log(`[rAgent] No transcript parser for agent type "${agentType}"`);
3143
+ return false;
3144
+ }
3145
+ const filePath = discoverTranscriptFile(sessionId, agentType);
3146
+ if (!filePath) {
3147
+ console.log(`[rAgent] No transcript file found for session ${sessionId}`);
3148
+ return false;
3149
+ }
3150
+ console.log(`[rAgent] Watching transcript: ${filePath} (${parser.name})`);
3151
+ const watcher = new TranscriptWatcher(filePath, parser, {
3152
+ onTurn: (turn, seq) => {
3153
+ this.sendFn(sessionId, turn, seq);
3154
+ },
3155
+ onError: (error) => {
3156
+ console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3157
+ }
3158
+ });
3159
+ if (!watcher.start()) {
3160
+ console.warn(`[rAgent] Failed to start watching ${filePath}`);
3161
+ return false;
3162
+ }
3163
+ this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
3164
+ watcher.replayFromStart();
3165
+ return true;
3166
+ }
3167
+ /** Disable markdown streaming for a session. */
3168
+ disableMarkdown(sessionId) {
3169
+ const session = this.active.get(sessionId);
3170
+ if (!session) return;
3171
+ session.watcher.removeSubscriber();
3172
+ this.active.delete(sessionId);
3173
+ console.log(`[rAgent] Stopped watching transcript for ${sessionId}`);
3174
+ }
3175
+ /** Handle sync-markdown request — send replay snapshot. */
3176
+ handleSyncRequest(sessionId, fromSeq) {
3177
+ const session = this.active.get(sessionId);
3178
+ if (!session) return;
3179
+ const turns = session.watcher.getReplayTurns(fromSeq);
3180
+ this.sendSnapshotFn(sessionId, turns, session.watcher.currentSeq);
3181
+ }
3182
+ /** Stop all watchers. */
3183
+ stopAll() {
3184
+ for (const [sessionId, session] of this.active) {
3185
+ session.watcher.stop();
3186
+ console.log(`[rAgent] Stopped transcript watcher for ${sessionId}`);
3187
+ }
3188
+ this.active.clear();
3189
+ }
3190
+ /** Check if a session has an active watcher. */
3191
+ isWatching(sessionId) {
3192
+ return this.active.has(sessionId);
3193
+ }
3194
+ };
3195
+
2681
3196
  // src/agent.ts
2682
3197
  function pidFilePath(hostId) {
2683
- return path2.join(CONFIG_DIR, `agent-${hostId}.pid`);
3198
+ return path3.join(CONFIG_DIR, `agent-${hostId}.pid`);
2684
3199
  }
2685
3200
  function readPidFile(filePath) {
2686
3201
  try {
2687
- const raw = fs3.readFileSync(filePath, "utf8").trim();
3202
+ const raw = fs4.readFileSync(filePath, "utf8").trim();
2688
3203
  const pid = Number.parseInt(raw, 10);
2689
3204
  return Number.isInteger(pid) && pid > 0 ? pid : null;
2690
3205
  } catch {
@@ -2709,7 +3224,7 @@ function acquirePidLock(hostId) {
2709
3224
  Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
2710
3225
  );
2711
3226
  }
2712
- fs3.writeFileSync(lockPath, `${process.pid}
3227
+ fs4.writeFileSync(lockPath, `${process.pid}
2713
3228
  `, "utf8");
2714
3229
  return lockPath;
2715
3230
  }
@@ -2717,7 +3232,7 @@ function releasePidLock(lockPath) {
2717
3232
  try {
2718
3233
  const currentPid = readPidFile(lockPath);
2719
3234
  if (currentPid === process.pid) {
2720
- fs3.unlinkSync(lockPath);
3235
+ fs4.unlinkSync(lockPath);
2721
3236
  }
2722
3237
  } catch {
2723
3238
  }
@@ -2726,7 +3241,7 @@ function resolveRunOptions(commandOptions) {
2726
3241
  const config = loadConfig();
2727
3242
  const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
2728
3243
  const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
2729
- const hostName = commandOptions.name || config.hostName || os6.hostname();
3244
+ const hostName = commandOptions.name || config.hostName || os7.hostname();
2730
3245
  const command = commandOptions.command || config.command || "bash";
2731
3246
  const agentToken = commandOptions.agentToken || config.agentToken || "";
2732
3247
  return { portal, hostId, hostName, command, agentToken };
@@ -2777,12 +3292,53 @@ async function runAgent(rawOptions) {
2777
3292
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: chunk, sessionId: ptySessionId });
2778
3293
  }
2779
3294
  };
3295
+ const transcriptWatcher = new TranscriptWatcherManager(
3296
+ (sessionId, turn, seq) => {
3297
+ if (!conn.isReady()) return;
3298
+ const payload = {
3299
+ type: "markdown",
3300
+ subtype: "delta",
3301
+ sessionId,
3302
+ seq,
3303
+ turnId: turn.turnId,
3304
+ role: turn.role,
3305
+ content: turn.content,
3306
+ tools: turn.tools,
3307
+ timestamp: turn.timestamp
3308
+ };
3309
+ if (conn.sessionKey) {
3310
+ const contentStr = JSON.stringify(payload);
3311
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
3312
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
3313
+ } else {
3314
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3315
+ }
3316
+ },
3317
+ (sessionId, turns, seq) => {
3318
+ if (!conn.isReady()) return;
3319
+ const payload = {
3320
+ type: "markdown",
3321
+ subtype: "snapshot",
3322
+ sessionId,
3323
+ seq,
3324
+ turns
3325
+ };
3326
+ if (conn.sessionKey) {
3327
+ const contentStr = JSON.stringify(payload);
3328
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
3329
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
3330
+ } else {
3331
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3332
+ }
3333
+ }
3334
+ );
2780
3335
  const shell = new ShellManager(options.command, sendOutput);
2781
3336
  const inventory = new InventoryManager(options);
2782
- const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options);
3337
+ const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options, transcriptWatcher);
2783
3338
  shell.spawn();
2784
3339
  const onSignal = () => {
2785
3340
  dispatcher.shouldRun = false;
3341
+ dispatcher.stopTranscriptWatchers();
2786
3342
  conn.cleanup({ stopStreams: true });
2787
3343
  shell.stop();
2788
3344
  releasePidLock(lockPath);
@@ -2818,7 +3374,7 @@ async function runAgent(rawOptions) {
2818
3374
  sendToGroup(ws, groups.privateGroup, {
2819
3375
  type: "register",
2820
3376
  hostName: options.hostName,
2821
- environment: os6.platform()
3377
+ environment: os7.platform()
2822
3378
  });
2823
3379
  conn.replayBufferedOutput((chunk) => {
2824
3380
  sendToGroup(ws, groups.privateGroup, {
@@ -2908,6 +3464,7 @@ async function runAgent(rawOptions) {
2908
3464
  conn.reconnectDelay = Math.min(conn.reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
2909
3465
  }
2910
3466
  } finally {
3467
+ dispatcher.stopTranscriptWatchers();
2911
3468
  conn.cleanup({ stopStreams: true });
2912
3469
  shell.stop();
2913
3470
  releasePidLock(lockPath);
@@ -3004,7 +3561,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
3004
3561
  async function connectMachine(opts) {
3005
3562
  const portal = opts.portal || DEFAULT_PORTAL;
3006
3563
  const hostId = sanitizeHostId(opts.id || inferHostId());
3007
- const hostName = opts.name || os7.hostname();
3564
+ const hostName = opts.name || os8.hostname();
3008
3565
  const command = opts.command || "bash";
3009
3566
  if (!opts.token) {
3010
3567
  throw new Error("Connection token is required.");
@@ -3078,12 +3635,12 @@ function registerRunCommand(program2) {
3078
3635
  }
3079
3636
 
3080
3637
  // src/commands/doctor.ts
3081
- var os8 = __toESM(require("os"));
3638
+ var os9 = __toESM(require("os"));
3082
3639
  async function runDoctor(opts) {
3083
3640
  const options = resolveRunOptions(opts);
3084
3641
  const checks = [];
3085
- const platformOk = os8.platform() === "linux";
3086
- checks.push({ name: "platform", ok: platformOk, detail: os8.platform() });
3642
+ const platformOk = os9.platform() === "linux";
3643
+ checks.push({ name: "platform", ok: platformOk, detail: os9.platform() });
3087
3644
  checks.push({
3088
3645
  name: "node",
3089
3646
  ok: Number(process.versions.node.split(".")[0]) >= 20,
@@ -3258,7 +3815,7 @@ function registerServiceCommand(program2) {
3258
3815
  }
3259
3816
 
3260
3817
  // src/commands/uninstall.ts
3261
- var fs4 = __toESM(require("fs"));
3818
+ var fs5 = __toESM(require("fs"));
3262
3819
  async function uninstallAgent(opts) {
3263
3820
  const config = loadConfig();
3264
3821
  const hostName = config.hostName || config.hostId || "this machine";
@@ -3289,8 +3846,8 @@ async function uninstallAgent(opts) {
3289
3846
  }
3290
3847
  console.log("[rAgent] Stopping and removing service...");
3291
3848
  await uninstallService().catch(() => void 0);
3292
- if (fs4.existsSync(CONFIG_DIR)) {
3293
- fs4.rmSync(CONFIG_DIR, { recursive: true, force: true });
3849
+ if (fs5.existsSync(CONFIG_DIR)) {
3850
+ fs5.rmSync(CONFIG_DIR, { recursive: true, force: true });
3294
3851
  console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
3295
3852
  }
3296
3853
  try {
@@ -3339,7 +3896,7 @@ function showStatus() {
3339
3896
  ragent v${CURRENT_VERSION}
3340
3897
  `);
3341
3898
  try {
3342
- const raw = fs5.readFileSync(CONFIG_FILE, "utf8");
3899
+ const raw = fs6.readFileSync(CONFIG_FILE, "utf8");
3343
3900
  const config = JSON.parse(raw);
3344
3901
  if (config.portal && config.agentToken) {
3345
3902
  console.log(` Status: Connected`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.4.5",
3
+ "version": "1.5.1",
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": {