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.
- package/dist/index.js +599 -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.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
|
|
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,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
|
|
3198
|
+
return path3.join(CONFIG_DIR, `agent-${hostId}.pid`);
|
|
2684
3199
|
}
|
|
2685
3200
|
function readPidFile(filePath) {
|
|
2686
3201
|
try {
|
|
2687
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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:
|
|
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 ||
|
|
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
|
|
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 =
|
|
3086
|
-
checks.push({ name: "platform", ok: platformOk, detail:
|
|
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
|
|
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 (
|
|
3293
|
-
|
|
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 =
|
|
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`);
|