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.
- package/dist/index.js +555 -46
- 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.
|
|
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
|
|
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
|
|
260
|
+
var os8 = __toESM(require("os"));
|
|
261
261
|
|
|
262
262
|
// src/agent.ts
|
|
263
|
-
var
|
|
264
|
-
var
|
|
265
|
-
var
|
|
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
|
-
|
|
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
|
|
3145
|
+
return path3.join(CONFIG_DIR, `agent-${hostId}.pid`);
|
|
2679
3146
|
}
|
|
2680
3147
|
function readPidFile(filePath) {
|
|
2681
3148
|
try {
|
|
2682
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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:
|
|
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 ||
|
|
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
|
|
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 =
|
|
3081
|
-
checks.push({ name: "platform", ok: platformOk, detail:
|
|
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
|
|
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 (
|
|
3288
|
-
|
|
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 =
|
|
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`);
|