ragent-cli 1.4.5 → 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 +546 -42
- 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
|
|
@@ -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
|
-
|
|
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,440 @@ 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
|
+
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
|
+
|
|
2681
3143
|
// src/agent.ts
|
|
2682
3144
|
function pidFilePath(hostId) {
|
|
2683
|
-
return
|
|
3145
|
+
return path3.join(CONFIG_DIR, `agent-${hostId}.pid`);
|
|
2684
3146
|
}
|
|
2685
3147
|
function readPidFile(filePath) {
|
|
2686
3148
|
try {
|
|
2687
|
-
const raw =
|
|
3149
|
+
const raw = fs4.readFileSync(filePath, "utf8").trim();
|
|
2688
3150
|
const pid = Number.parseInt(raw, 10);
|
|
2689
3151
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2690
3152
|
} catch {
|
|
@@ -2709,7 +3171,7 @@ function acquirePidLock(hostId) {
|
|
|
2709
3171
|
Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
|
|
2710
3172
|
);
|
|
2711
3173
|
}
|
|
2712
|
-
|
|
3174
|
+
fs4.writeFileSync(lockPath, `${process.pid}
|
|
2713
3175
|
`, "utf8");
|
|
2714
3176
|
return lockPath;
|
|
2715
3177
|
}
|
|
@@ -2717,7 +3179,7 @@ function releasePidLock(lockPath) {
|
|
|
2717
3179
|
try {
|
|
2718
3180
|
const currentPid = readPidFile(lockPath);
|
|
2719
3181
|
if (currentPid === process.pid) {
|
|
2720
|
-
|
|
3182
|
+
fs4.unlinkSync(lockPath);
|
|
2721
3183
|
}
|
|
2722
3184
|
} catch {
|
|
2723
3185
|
}
|
|
@@ -2726,7 +3188,7 @@ function resolveRunOptions(commandOptions) {
|
|
|
2726
3188
|
const config = loadConfig();
|
|
2727
3189
|
const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
|
|
2728
3190
|
const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
|
|
2729
|
-
const hostName = commandOptions.name || config.hostName ||
|
|
3191
|
+
const hostName = commandOptions.name || config.hostName || os7.hostname();
|
|
2730
3192
|
const command = commandOptions.command || config.command || "bash";
|
|
2731
3193
|
const agentToken = commandOptions.agentToken || config.agentToken || "";
|
|
2732
3194
|
return { portal, hostId, hostName, command, agentToken };
|
|
@@ -2777,12 +3239,53 @@ async function runAgent(rawOptions) {
|
|
|
2777
3239
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: chunk, sessionId: ptySessionId });
|
|
2778
3240
|
}
|
|
2779
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
|
+
);
|
|
2780
3282
|
const shell = new ShellManager(options.command, sendOutput);
|
|
2781
3283
|
const inventory = new InventoryManager(options);
|
|
2782
|
-
const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options);
|
|
3284
|
+
const dispatcher = new ControlDispatcher(shell, sessionStreamer, inventory, conn, options, transcriptWatcher);
|
|
2783
3285
|
shell.spawn();
|
|
2784
3286
|
const onSignal = () => {
|
|
2785
3287
|
dispatcher.shouldRun = false;
|
|
3288
|
+
dispatcher.stopTranscriptWatchers();
|
|
2786
3289
|
conn.cleanup({ stopStreams: true });
|
|
2787
3290
|
shell.stop();
|
|
2788
3291
|
releasePidLock(lockPath);
|
|
@@ -2818,7 +3321,7 @@ async function runAgent(rawOptions) {
|
|
|
2818
3321
|
sendToGroup(ws, groups.privateGroup, {
|
|
2819
3322
|
type: "register",
|
|
2820
3323
|
hostName: options.hostName,
|
|
2821
|
-
environment:
|
|
3324
|
+
environment: os7.platform()
|
|
2822
3325
|
});
|
|
2823
3326
|
conn.replayBufferedOutput((chunk) => {
|
|
2824
3327
|
sendToGroup(ws, groups.privateGroup, {
|
|
@@ -2908,6 +3411,7 @@ async function runAgent(rawOptions) {
|
|
|
2908
3411
|
conn.reconnectDelay = Math.min(conn.reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
|
|
2909
3412
|
}
|
|
2910
3413
|
} finally {
|
|
3414
|
+
dispatcher.stopTranscriptWatchers();
|
|
2911
3415
|
conn.cleanup({ stopStreams: true });
|
|
2912
3416
|
shell.stop();
|
|
2913
3417
|
releasePidLock(lockPath);
|
|
@@ -3004,7 +3508,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
|
|
|
3004
3508
|
async function connectMachine(opts) {
|
|
3005
3509
|
const portal = opts.portal || DEFAULT_PORTAL;
|
|
3006
3510
|
const hostId = sanitizeHostId(opts.id || inferHostId());
|
|
3007
|
-
const hostName = opts.name ||
|
|
3511
|
+
const hostName = opts.name || os8.hostname();
|
|
3008
3512
|
const command = opts.command || "bash";
|
|
3009
3513
|
if (!opts.token) {
|
|
3010
3514
|
throw new Error("Connection token is required.");
|
|
@@ -3078,12 +3582,12 @@ function registerRunCommand(program2) {
|
|
|
3078
3582
|
}
|
|
3079
3583
|
|
|
3080
3584
|
// src/commands/doctor.ts
|
|
3081
|
-
var
|
|
3585
|
+
var os9 = __toESM(require("os"));
|
|
3082
3586
|
async function runDoctor(opts) {
|
|
3083
3587
|
const options = resolveRunOptions(opts);
|
|
3084
3588
|
const checks = [];
|
|
3085
|
-
const platformOk =
|
|
3086
|
-
checks.push({ name: "platform", ok: platformOk, detail:
|
|
3589
|
+
const platformOk = os9.platform() === "linux";
|
|
3590
|
+
checks.push({ name: "platform", ok: platformOk, detail: os9.platform() });
|
|
3087
3591
|
checks.push({
|
|
3088
3592
|
name: "node",
|
|
3089
3593
|
ok: Number(process.versions.node.split(".")[0]) >= 20,
|
|
@@ -3258,7 +3762,7 @@ function registerServiceCommand(program2) {
|
|
|
3258
3762
|
}
|
|
3259
3763
|
|
|
3260
3764
|
// src/commands/uninstall.ts
|
|
3261
|
-
var
|
|
3765
|
+
var fs5 = __toESM(require("fs"));
|
|
3262
3766
|
async function uninstallAgent(opts) {
|
|
3263
3767
|
const config = loadConfig();
|
|
3264
3768
|
const hostName = config.hostName || config.hostId || "this machine";
|
|
@@ -3289,8 +3793,8 @@ async function uninstallAgent(opts) {
|
|
|
3289
3793
|
}
|
|
3290
3794
|
console.log("[rAgent] Stopping and removing service...");
|
|
3291
3795
|
await uninstallService().catch(() => void 0);
|
|
3292
|
-
if (
|
|
3293
|
-
|
|
3796
|
+
if (fs5.existsSync(CONFIG_DIR)) {
|
|
3797
|
+
fs5.rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
3294
3798
|
console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
|
|
3295
3799
|
}
|
|
3296
3800
|
try {
|
|
@@ -3339,7 +3843,7 @@ function showStatus() {
|
|
|
3339
3843
|
ragent v${CURRENT_VERSION}
|
|
3340
3844
|
`);
|
|
3341
3845
|
try {
|
|
3342
|
-
const raw =
|
|
3846
|
+
const raw = fs6.readFileSync(CONFIG_FILE, "utf8");
|
|
3343
3847
|
const config = JSON.parse(raw);
|
|
3344
3848
|
if (config.portal && config.agentToken) {
|
|
3345
3849
|
console.log(` Status: Connected`);
|