ragent-cli 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +139 -0
  2. package/dist/index.js +2206 -0
  3. package/package.json +60 -0
package/dist/index.js ADDED
@@ -0,0 +1,2206 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+
29
+ // package.json
30
+ var require_package = __commonJS({
31
+ "package.json"(exports2, module2) {
32
+ module2.exports = {
33
+ name: "ragent-cli",
34
+ version: "1.1.0",
35
+ description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
36
+ main: "dist/index.js",
37
+ bin: {
38
+ ragent: "./dist/index.js"
39
+ },
40
+ scripts: {
41
+ build: "tsup",
42
+ dev: "tsup --watch",
43
+ test: "vitest run",
44
+ typecheck: "tsc --noEmit"
45
+ },
46
+ keywords: [
47
+ "terminal",
48
+ "remote",
49
+ "agent",
50
+ "cli",
51
+ "tmux",
52
+ "devops",
53
+ "ssh",
54
+ "control-plane"
55
+ ],
56
+ author: "Intellimetrics <support@intellimetrics.net>",
57
+ license: "MIT",
58
+ homepage: "https://ragent.live",
59
+ repository: {
60
+ type: "git",
61
+ url: "git+https://github.com/chadlindell/ragent-live.git",
62
+ directory: "cli"
63
+ },
64
+ bugs: {
65
+ url: "https://github.com/chadlindell/ragent-live/issues"
66
+ },
67
+ engines: {
68
+ node: ">=20"
69
+ },
70
+ os: [
71
+ "linux"
72
+ ],
73
+ files: [
74
+ "dist"
75
+ ],
76
+ dependencies: {
77
+ "@azure/web-pubsub-client": "^1.0.2",
78
+ commander: "^14.0.3",
79
+ figlet: "^1.9.3",
80
+ "node-pty": "^1.1.0",
81
+ ws: "^8.19.0"
82
+ },
83
+ devDependencies: {
84
+ "@types/figlet": "^1.7.0",
85
+ "@types/node": "^20.17.0",
86
+ "@types/ws": "^8.5.13",
87
+ tsup: "^8.4.0",
88
+ typescript: "^5.7.0",
89
+ vitest: "^3.0.0"
90
+ }
91
+ };
92
+ }
93
+ });
94
+
95
+ // src/index.ts
96
+ var fs5 = __toESM(require("fs"));
97
+ var import_commander = require("commander");
98
+
99
+ // src/constants.ts
100
+ var os = __toESM(require("os"));
101
+ var path = __toESM(require("path"));
102
+ var packageInfo = require_package();
103
+ var DEFAULT_PORTAL = process.env.RAGENT_PORTAL || "http://localhost:3000";
104
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "ragent");
105
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
106
+ var SERVICE_NAME = "ragent.service";
107
+ var SERVICE_DIR = path.join(os.homedir(), ".config", "systemd", "user");
108
+ var SERVICE_FILE = path.join(SERVICE_DIR, SERVICE_NAME);
109
+ var FALLBACK_PID_FILE = path.join(CONFIG_DIR, "ragent.pid");
110
+ var FALLBACK_LOG_FILE = path.join(CONFIG_DIR, "ragent.log");
111
+ var WS_HEARTBEAT_MS = 15e3;
112
+ var HTTP_HEARTBEAT_MS = 5 * 60 * 1e3;
113
+ var PACKAGE_NAME = packageInfo.name || "ragent-cli";
114
+ var CURRENT_VERSION = packageInfo.version || "0.0.0";
115
+ var SHOW_ART = process.stdout.isTTY && process.env.RAGENT_NO_ART !== "true";
116
+ var DEFAULT_RECONNECT_DELAY_MS = 2500;
117
+ var MAX_RECONNECT_DELAY_MS = 3e4;
118
+ var OUTPUT_BUFFER_MAX_BYTES = 100 * 1024;
119
+
120
+ // src/config.ts
121
+ var fs = __toESM(require("fs"));
122
+ var os2 = __toESM(require("os"));
123
+ function ensureConfigDir() {
124
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
125
+ }
126
+ function loadConfig() {
127
+ try {
128
+ const stat = fs.statSync(CONFIG_FILE);
129
+ const mode = stat.mode & 511;
130
+ if (mode & 63) {
131
+ console.warn(
132
+ `[rAgent] WARNING: Config file ${CONFIG_FILE} has overly permissive permissions (${mode.toString(8)}). Fixing to 0600.`
133
+ );
134
+ fs.chmodSync(CONFIG_FILE, 384);
135
+ }
136
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
137
+ } catch {
138
+ return {};
139
+ }
140
+ }
141
+ function saveConfig(config) {
142
+ ensureConfigDir();
143
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
144
+ }
145
+ function saveConfigPatch(patch) {
146
+ const existing = loadConfig();
147
+ saveConfig({
148
+ ...existing,
149
+ ...patch,
150
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
151
+ });
152
+ }
153
+ function sanitizeHostId(value) {
154
+ return String(value || "").toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+/, "").slice(0, 64);
155
+ }
156
+ function inferHostId() {
157
+ return sanitizeHostId(os2.hostname().split(".")[0] || "linux-host");
158
+ }
159
+
160
+ // src/version.ts
161
+ function parseSemver(version) {
162
+ const match = String(version || "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
163
+ if (!match) return null;
164
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
165
+ }
166
+ function isVersionNewer(latest, current) {
167
+ const latestParts = parseSemver(latest);
168
+ const currentParts = parseSemver(current);
169
+ if (!latestParts || !currentParts) return false;
170
+ for (let i = 0; i < 3; i += 1) {
171
+ if (latestParts[i] > currentParts[i]) return true;
172
+ if (latestParts[i] < currentParts[i]) return false;
173
+ }
174
+ return false;
175
+ }
176
+ async function fetchLatestVersion(timeoutMs = 1800) {
177
+ const controller = new AbortController();
178
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
179
+ try {
180
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
181
+ signal: controller.signal
182
+ });
183
+ if (!response.ok) return null;
184
+ const data = await response.json();
185
+ return typeof data.version === "string" ? data.version : null;
186
+ } catch {
187
+ return null;
188
+ } finally {
189
+ clearTimeout(timeout);
190
+ }
191
+ }
192
+ async function checkForUpdate(opts = {}) {
193
+ const force = Boolean(opts.force);
194
+ const config = loadConfig();
195
+ const lastChecked = config.updateCheckedAt ? new Date(config.updateCheckedAt).getTime() : 0;
196
+ const intervalMs = 12 * 60 * 60 * 1e3;
197
+ const now = Date.now();
198
+ let latestVersion = null;
199
+ if (!force && lastChecked > 0 && now - lastChecked < intervalMs) {
200
+ latestVersion = config.latestKnownVersion || null;
201
+ } else {
202
+ latestVersion = await fetchLatestVersion();
203
+ if (latestVersion) {
204
+ saveConfigPatch({
205
+ updateCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
206
+ latestKnownVersion: latestVersion
207
+ });
208
+ }
209
+ }
210
+ if (!latestVersion) return null;
211
+ return isVersionNewer(latestVersion, CURRENT_VERSION) ? latestVersion : null;
212
+ }
213
+ async function maybeWarnUpdate() {
214
+ if (process.env.RAGENT_DISABLE_UPDATE_CHECK === "true") return;
215
+ const latestVersion = await checkForUpdate({ force: false });
216
+ if (!latestVersion) return;
217
+ console.log(
218
+ `[rAgent] Update available: ${CURRENT_VERSION} -> ${latestVersion}. Run: ragent update`
219
+ );
220
+ }
221
+
222
+ // src/commands/connect.ts
223
+ var os6 = __toESM(require("os"));
224
+
225
+ // src/agent.ts
226
+ var fs3 = __toESM(require("fs"));
227
+ var os5 = __toESM(require("os"));
228
+ var path2 = __toESM(require("path"));
229
+ var import_ws2 = __toESM(require("ws"));
230
+
231
+ // src/auth.ts
232
+ var os3 = __toESM(require("os"));
233
+
234
+ // src/system.ts
235
+ var import_node_child_process = require("child_process");
236
+ var readline = __toESM(require("readline/promises"));
237
+ function wait(ms) {
238
+ return new Promise((resolve) => setTimeout(resolve, ms));
239
+ }
240
+ function execAsync(command, options = {}) {
241
+ const timeout = options.timeout ?? 5e3;
242
+ const maxBuffer = options.maxBuffer ?? 1024 * 1024;
243
+ return new Promise((resolve, reject) => {
244
+ (0, import_node_child_process.exec)(command, { timeout, maxBuffer }, (error, stdout, stderr) => {
245
+ if (error) {
246
+ reject(new Error((stderr || error.message || "").trim()));
247
+ return;
248
+ }
249
+ resolve(stdout);
250
+ });
251
+ });
252
+ }
253
+ function shellQuote(value) {
254
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
255
+ }
256
+ async function commandExists(command) {
257
+ try {
258
+ await execAsync(`command -v ${command}`);
259
+ return true;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+ function detectTmuxInstallRecipe() {
265
+ return [
266
+ { manager: "apt-get", command: "sudo apt-get update && sudo apt-get install -y tmux" },
267
+ { manager: "dnf", command: "sudo dnf install -y tmux" },
268
+ { manager: "yum", command: "sudo yum install -y tmux" },
269
+ { manager: "pacman", command: "sudo pacman -Sy --noconfirm tmux" },
270
+ { manager: "zypper", command: "sudo zypper --non-interactive install tmux" }
271
+ ];
272
+ }
273
+ async function chooseTmuxInstallCommand() {
274
+ const recipes = detectTmuxInstallRecipe();
275
+ for (const recipe of recipes) {
276
+ if (await commandExists(recipe.manager)) {
277
+ return recipe;
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ async function askYesNo(question, defaultYes = true) {
283
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
284
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
285
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
286
+ try {
287
+ const answer = (await rl.question(`${question} ${hint} `)).trim().toLowerCase();
288
+ if (!answer) return defaultYes;
289
+ return answer === "y" || answer === "yes";
290
+ } finally {
291
+ rl.close();
292
+ }
293
+ }
294
+ async function installTmuxInteractively() {
295
+ const recipe = await chooseTmuxInstallCommand();
296
+ if (!recipe) {
297
+ console.log("[rAgent] No supported package manager was detected for tmux auto-install.");
298
+ console.log("[rAgent] Install tmux manually, then rerun: ragent doctor --fix");
299
+ return false;
300
+ }
301
+ const approved = await askYesNo(
302
+ `[rAgent] tmux is missing. Install it now using ${recipe.manager}?`,
303
+ true
304
+ );
305
+ if (!approved) return false;
306
+ await new Promise((resolve, reject) => {
307
+ const child = (0, import_node_child_process.spawn)("bash", ["-lc", recipe.command], { stdio: "inherit" });
308
+ child.on("exit", (code) => {
309
+ if (code === 0) resolve();
310
+ else reject(new Error(`tmux install command exited with code ${code}`));
311
+ });
312
+ child.on("error", reject);
313
+ });
314
+ return commandExists("tmux");
315
+ }
316
+
317
+ // src/sessions.ts
318
+ async function collectTmuxSessions() {
319
+ try {
320
+ await execAsync("tmux -V");
321
+ } catch {
322
+ return [];
323
+ }
324
+ try {
325
+ const raw = await execAsync(
326
+ "tmux list-panes -a -F '#{session_name}|#{window_index}|#{pane_index}|#{pane_current_command}|#{pane_active}|#{pane_last}|#{pane_pid}'"
327
+ );
328
+ const rows = raw.split("\n").map((line) => line.trim()).filter(Boolean);
329
+ return rows.map((row) => {
330
+ const [sessionName, windowIndex, paneIndex, command, activeFlag, lastEpoch, panePid] = row.split("|");
331
+ const id = `tmux:${sessionName}:${windowIndex}.${paneIndex}`;
332
+ const lastActivityAt = Number(lastEpoch) > 0 ? new Date(Number(lastEpoch) * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
333
+ const pids = [];
334
+ const pid = Number(panePid);
335
+ if (pid > 0) pids.push(pid);
336
+ return {
337
+ id,
338
+ type: "tmux",
339
+ name: `${sessionName}:${windowIndex}.${paneIndex}`,
340
+ status: activeFlag === "1" ? "active" : "detached",
341
+ command,
342
+ agentType: detectAgentType(command),
343
+ lastActivityAt,
344
+ pids
345
+ };
346
+ });
347
+ } catch {
348
+ return [];
349
+ }
350
+ }
351
+ async function collectScreenSessions() {
352
+ try {
353
+ await execAsync("screen -v");
354
+ } catch {
355
+ return [];
356
+ }
357
+ try {
358
+ let raw;
359
+ try {
360
+ raw = await execAsync("screen -ls");
361
+ } catch (e) {
362
+ raw = e instanceof Error ? e.message : "";
363
+ }
364
+ const sessionPattern = /^\s*(\d+)\.(\S+)\s+\((Detached|Attached)\)/;
365
+ const sessions = [];
366
+ for (const line of raw.split("\n")) {
367
+ const match = line.match(sessionPattern);
368
+ if (!match) continue;
369
+ const [, screenPid, sessionName, state] = match;
370
+ const pid = Number(screenPid);
371
+ const childInfo = await getChildAgentInfo(pid);
372
+ if (!childInfo) continue;
373
+ const id = `screen:${sessionName}:${screenPid}`;
374
+ sessions.push({
375
+ id,
376
+ type: "screen",
377
+ name: `${sessionName}`,
378
+ status: state === "Attached" ? "active" : "detached",
379
+ command: childInfo.command,
380
+ agentType: childInfo.agentType,
381
+ lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
382
+ pids: [pid, ...childInfo.childPids]
383
+ });
384
+ }
385
+ return sessions;
386
+ } catch {
387
+ return [];
388
+ }
389
+ }
390
+ async function collectZellijSessions() {
391
+ try {
392
+ await execAsync("zellij --version");
393
+ } catch {
394
+ return [];
395
+ }
396
+ try {
397
+ const raw = await execAsync("zellij list-sessions");
398
+ const sessions = [];
399
+ for (const line of raw.split("\n")) {
400
+ const sessionName = line.trim();
401
+ if (!sessionName) continue;
402
+ let serverPids;
403
+ try {
404
+ const psOut = await execAsync(
405
+ `ps axo pid,args --no-headers | grep 'zellij.*--session ${sessionName}' | grep -v grep`
406
+ );
407
+ serverPids = psOut.split("\n").map((l) => Number(l.trim().split(/\s+/)[0])).filter((p) => p > 0);
408
+ } catch {
409
+ serverPids = [];
410
+ }
411
+ const allChildPids = [];
412
+ let foundAgent = null;
413
+ for (const spid of serverPids) {
414
+ const childInfo = await getChildAgentInfo(spid);
415
+ if (childInfo) {
416
+ foundAgent = childInfo;
417
+ allChildPids.push(...childInfo.childPids);
418
+ }
419
+ }
420
+ if (!foundAgent) continue;
421
+ const id = `zellij:${sessionName}`;
422
+ sessions.push({
423
+ id,
424
+ type: "zellij",
425
+ name: sessionName,
426
+ status: "active",
427
+ command: foundAgent.command,
428
+ agentType: foundAgent.agentType,
429
+ lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
430
+ pids: [...serverPids, ...allChildPids]
431
+ });
432
+ }
433
+ return sessions;
434
+ } catch {
435
+ return [];
436
+ }
437
+ }
438
+ async function collectBareAgentProcesses(excludePids) {
439
+ try {
440
+ const raw = await execAsync("ps axo pid,ppid,comm,args --no-headers");
441
+ const sessions = [];
442
+ const seen = /* @__PURE__ */ new Set();
443
+ for (const line of raw.split("\n")) {
444
+ const trimmed = line.trim();
445
+ if (!trimmed) continue;
446
+ const parts = trimmed.split(/\s+/);
447
+ if (parts.length < 4) continue;
448
+ const pid = Number(parts[0]);
449
+ const args = parts.slice(3).join(" ");
450
+ const agentType = detectAgentType(args);
451
+ if (!agentType) continue;
452
+ if (excludePids?.has(pid)) continue;
453
+ if (seen.has(pid)) continue;
454
+ seen.add(pid);
455
+ const id = `process:${pid}`;
456
+ sessions.push({
457
+ id,
458
+ type: "process",
459
+ name: `${agentType} (pid ${pid})`,
460
+ status: "active",
461
+ command: args,
462
+ agentType,
463
+ lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
464
+ pids: [pid]
465
+ });
466
+ }
467
+ return sessions;
468
+ } catch {
469
+ return [];
470
+ }
471
+ }
472
+ async function collectSessionInventory(hostId, command) {
473
+ const [tmux, screen, zellij] = await Promise.all([
474
+ collectTmuxSessions(),
475
+ collectScreenSessions(),
476
+ collectZellijSessions()
477
+ ]);
478
+ const multiplexerPids = /* @__PURE__ */ new Set();
479
+ for (const s of [...tmux, ...screen, ...zellij]) {
480
+ if (s.pids) s.pids.forEach((p) => multiplexerPids.add(p));
481
+ }
482
+ const bare = await collectBareAgentProcesses(multiplexerPids);
483
+ const ptySession = {
484
+ id: `pty:${hostId}`,
485
+ type: "pty",
486
+ name: command,
487
+ status: "active",
488
+ command,
489
+ agentType: detectAgentType(command),
490
+ lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
491
+ };
492
+ return [...tmux, ...screen, ...zellij, ...bare, ptySession];
493
+ }
494
+ function detectAgentType(command) {
495
+ if (!command) return void 0;
496
+ const cmd = command.toLowerCase();
497
+ if (cmd.includes("claude")) return "Claude Code";
498
+ if (cmd.includes("codex")) return "Codex CLI";
499
+ if (cmd.includes("aider")) return "aider";
500
+ if (cmd.includes("cursor")) return "Cursor";
501
+ if (cmd.includes("windsurf")) return "Windsurf";
502
+ if (cmd.includes("gemini")) return "Gemini CLI";
503
+ if (cmd.includes("amazon-q") || cmd.includes("amazon_q")) return "Amazon Q";
504
+ if (cmd.includes("copilot")) return "Copilot CLI";
505
+ return void 0;
506
+ }
507
+ function sessionInventoryFingerprint(sessions) {
508
+ const sorted = [...sessions].sort((a, b) => a.id.localeCompare(b.id));
509
+ return sorted.map((s) => `${s.id}|${s.type}|${s.name}|${s.status}|${s.command || ""}`).join("\n");
510
+ }
511
+ async function getChildAgentInfo(parentPid) {
512
+ try {
513
+ const raw = await execAsync(`ps --ppid ${parentPid} -o pid,args --no-headers`);
514
+ const childPids = [];
515
+ for (const line of raw.split("\n")) {
516
+ const trimmed = line.trim();
517
+ if (!trimmed) continue;
518
+ const spaceIdx = trimmed.indexOf(" ");
519
+ if (spaceIdx < 0) continue;
520
+ const pid = Number(trimmed.slice(0, spaceIdx));
521
+ const args = trimmed.slice(spaceIdx + 1).trim();
522
+ if (pid > 0) childPids.push(pid);
523
+ const agentType = detectAgentType(args);
524
+ if (agentType) {
525
+ return { command: args, agentType, childPids };
526
+ }
527
+ const deeper = await getChildAgentInfo(pid);
528
+ if (deeper) {
529
+ return { ...deeper, childPids: [...childPids, ...deeper.childPids] };
530
+ }
531
+ }
532
+ return null;
533
+ } catch {
534
+ return null;
535
+ }
536
+ }
537
+
538
+ // src/auth.ts
539
+ var AuthError = class extends Error {
540
+ constructor(message) {
541
+ super(message);
542
+ this.name = "AuthError";
543
+ }
544
+ };
545
+ function decodeJwtExp(token) {
546
+ try {
547
+ const parts = token.split(".");
548
+ if (parts.length !== 3) return null;
549
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
550
+ return typeof payload.exp === "number" ? payload.exp : null;
551
+ } catch {
552
+ return null;
553
+ }
554
+ }
555
+ async function refreshTokenIfNeeded(params) {
556
+ const config = loadConfig();
557
+ const refreshToken = config.refreshToken;
558
+ const exp = decodeJwtExp(params.agentToken);
559
+ if (!exp) return params.agentToken;
560
+ const remainingSeconds = exp - Math.floor(Date.now() / 1e3);
561
+ const threshold = remainingSeconds < 3600 ? 120 : 3600;
562
+ if (remainingSeconds > threshold) return params.agentToken;
563
+ console.log(
564
+ `[rAgent] Token expires in ${Math.round(remainingSeconds / 60)}m \u2014 refreshing...`
565
+ );
566
+ try {
567
+ const headers = { "Content-Type": "application/json" };
568
+ let body = {};
569
+ if (refreshToken) {
570
+ body = { refreshToken };
571
+ } else {
572
+ headers["Authorization"] = `Bearer ${params.agentToken}`;
573
+ }
574
+ const response = await fetch(`${params.portal}/api/agent/refresh`, {
575
+ method: "POST",
576
+ headers,
577
+ body: JSON.stringify(body)
578
+ });
579
+ if (!response.ok) {
580
+ const data2 = await response.json().catch(() => ({}));
581
+ console.warn(
582
+ `[rAgent] Token refresh failed: ${data2.error || response.status}`
583
+ );
584
+ return params.agentToken;
585
+ }
586
+ const data = await response.json();
587
+ if (!data.agentToken) return params.agentToken;
588
+ const patch = {
589
+ agentToken: data.agentToken,
590
+ tokenExpiresAt: data.expiresAt || ""
591
+ };
592
+ if (data.refreshToken) {
593
+ patch.refreshToken = data.refreshToken;
594
+ patch.refreshExpiresAt = data.refreshExpiresAt || "";
595
+ }
596
+ saveConfigPatch(patch);
597
+ console.log("[rAgent] Token refreshed successfully.");
598
+ return data.agentToken;
599
+ } catch (error) {
600
+ const message = error instanceof Error ? error.message : String(error);
601
+ console.warn(`[rAgent] Token refresh error: ${message}`);
602
+ return params.agentToken;
603
+ }
604
+ }
605
+ async function claimHost(params) {
606
+ const sessions = await collectSessionInventory(params.hostId, params.command);
607
+ const response = await fetch(`${params.portal}/api/agent/claim`, {
608
+ method: "POST",
609
+ headers: { "Content-Type": "application/json" },
610
+ body: JSON.stringify({
611
+ connectionToken: params.connectionToken,
612
+ hostId: params.hostId,
613
+ hostName: params.hostName,
614
+ environment: os3.platform(),
615
+ sessions,
616
+ agentVersion: CURRENT_VERSION
617
+ })
618
+ });
619
+ const data = await response.json();
620
+ if (!response.ok) {
621
+ throw new Error(data.error || "Failed to connect machine");
622
+ }
623
+ if (!data.agentToken) {
624
+ throw new Error("Missing connector token in claim response");
625
+ }
626
+ return data;
627
+ }
628
+ async function negotiateAgent(params) {
629
+ const response = await fetch(`${params.portal}/api/agent/negotiate`, {
630
+ method: "POST",
631
+ headers: {
632
+ "Content-Type": "application/json",
633
+ Authorization: `Bearer ${params.agentToken}`
634
+ },
635
+ body: JSON.stringify({})
636
+ });
637
+ const data = await response.json();
638
+ if (response.status === 401 || response.status === 403) {
639
+ throw new AuthError(
640
+ data.error || "Connector token rejected (401/403). Re-connect this machine."
641
+ );
642
+ }
643
+ if (!response.ok) {
644
+ throw new Error(data.error || "Failed to negotiate websocket access");
645
+ }
646
+ if (!data.url || !data.groups?.privateGroup || !data.groups?.registryGroup) {
647
+ throw new Error("Negotiate response missing websocket URL or group info");
648
+ }
649
+ return data;
650
+ }
651
+ async function deregisterHost(params) {
652
+ const response = await fetch(`${params.portal}/api/agent/deregister`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ Authorization: `Bearer ${params.agentToken}`
657
+ },
658
+ body: JSON.stringify({})
659
+ });
660
+ if (response.status === 401 || response.status === 403) {
661
+ console.warn("[rAgent] Token expired \u2014 could not deregister from server. Host may remain visible in dashboard until manually removed.");
662
+ return false;
663
+ }
664
+ return response.ok;
665
+ }
666
+ async function postHeartbeat(params) {
667
+ const sessions = await collectSessionInventory(params.hostId, params.command);
668
+ try {
669
+ const response = await fetch(`${params.portal}/api/agent/heartbeat`, {
670
+ method: "POST",
671
+ headers: {
672
+ "Content-Type": "application/json",
673
+ Authorization: `Bearer ${params.agentToken}`
674
+ },
675
+ body: JSON.stringify({
676
+ environment: os3.platform(),
677
+ hostName: params.hostName,
678
+ sessions,
679
+ agentVersion: CURRENT_VERSION
680
+ })
681
+ });
682
+ if (response.status === 401 || response.status === 403) {
683
+ throw new AuthError(
684
+ "Heartbeat rejected (401/403). Token may be expired or revoked."
685
+ );
686
+ }
687
+ } catch (error) {
688
+ if (error instanceof AuthError) throw error;
689
+ }
690
+ return sessions;
691
+ }
692
+
693
+ // src/output-buffer.ts
694
+ var OutputBuffer = class {
695
+ maxBytes;
696
+ chunks;
697
+ totalBytes;
698
+ constructor(maxBytes = OUTPUT_BUFFER_MAX_BYTES) {
699
+ this.maxBytes = maxBytes;
700
+ this.chunks = [];
701
+ this.totalBytes = 0;
702
+ }
703
+ push(chunk) {
704
+ const size = Buffer.byteLength(chunk, "utf8");
705
+ if (size > this.maxBytes) {
706
+ this.chunks = [chunk.slice(-this.maxBytes)];
707
+ this.totalBytes = Buffer.byteLength(this.chunks[0], "utf8");
708
+ return;
709
+ }
710
+ this.chunks.push(chunk);
711
+ this.totalBytes += size;
712
+ while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {
713
+ const dropped = this.chunks.shift();
714
+ this.totalBytes -= Buffer.byteLength(dropped, "utf8");
715
+ }
716
+ }
717
+ drain() {
718
+ const result = this.chunks;
719
+ this.chunks = [];
720
+ this.totalBytes = 0;
721
+ return result;
722
+ }
723
+ get length() {
724
+ return this.chunks.length;
725
+ }
726
+ get byteLength() {
727
+ return this.totalBytes;
728
+ }
729
+ };
730
+
731
+ // src/pty.ts
732
+ var pty = __toESM(require("node-pty"));
733
+ function isInteractiveShell(command) {
734
+ const trimmed = String(command).trim();
735
+ return ["bash", "sh", "zsh", "fish"].includes(trimmed);
736
+ }
737
+ function spawnConnectorShell(command, onData, onExit) {
738
+ const shell = "bash";
739
+ const args = isInteractiveShell(command) ? [] : ["-lc", command];
740
+ const processName = isInteractiveShell(command) ? command : shell;
741
+ const ptyProcess = pty.spawn(processName, args, {
742
+ name: "xterm-color",
743
+ cols: 80,
744
+ rows: 30,
745
+ cwd: process.cwd(),
746
+ env: process.env
747
+ });
748
+ ptyProcess.onData(onData);
749
+ ptyProcess.onExit(onExit);
750
+ return ptyProcess;
751
+ }
752
+ async function stopTmuxPaneBySessionId(sessionId) {
753
+ if (!sessionId.startsWith("tmux:")) return false;
754
+ const paneTarget = sessionId.slice("tmux:".length).trim();
755
+ if (!paneTarget) return false;
756
+ await execAsync(`tmux kill-pane -t ${shellQuote(paneTarget)}`, { timeout: 5e3 });
757
+ return true;
758
+ }
759
+ async function stopAllDetachedTmuxSessions() {
760
+ try {
761
+ const raw = await execAsync(
762
+ "tmux list-sessions -F '#{session_name}|#{session_attached}'",
763
+ { timeout: 5e3 }
764
+ );
765
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
766
+ let killed = 0;
767
+ for (const line of lines) {
768
+ const [name, attached] = line.split("|");
769
+ if (attached === "0" && name) {
770
+ try {
771
+ await execAsync(`tmux kill-session -t ${shellQuote(name)}`, { timeout: 5e3 });
772
+ killed++;
773
+ } catch {
774
+ }
775
+ }
776
+ }
777
+ return killed;
778
+ } catch {
779
+ return 0;
780
+ }
781
+ }
782
+
783
+ // src/service.ts
784
+ var import_child_process = require("child_process");
785
+ var fs2 = __toESM(require("fs"));
786
+ var os4 = __toESM(require("os"));
787
+ function assertConfiguredAgentToken() {
788
+ const config = loadConfig();
789
+ if (!config.agentToken) {
790
+ throw new Error("No saved connector token. Run `ragent connect --token <token>` first.");
791
+ }
792
+ }
793
+ function getConfiguredServiceBackend() {
794
+ const config = loadConfig();
795
+ if (config.serviceBackend === "systemd" || config.serviceBackend === "pidfile") {
796
+ return config.serviceBackend;
797
+ }
798
+ if (fs2.existsSync(SERVICE_FILE)) return "systemd";
799
+ if (fs2.existsSync(FALLBACK_PID_FILE)) return "pidfile";
800
+ return null;
801
+ }
802
+ async function canUseSystemdUser() {
803
+ if (os4.platform() !== "linux") return false;
804
+ try {
805
+ await execAsync("systemctl --user --version", { timeout: 4e3 });
806
+ await execAsync("systemctl --user show-environment", { timeout: 4e3 });
807
+ return true;
808
+ } catch {
809
+ return false;
810
+ }
811
+ }
812
+ async function runSystemctlUser(args, options = {}) {
813
+ const command = `systemctl --user ${args.map(shellQuote).join(" ")}`;
814
+ return execAsync(command, { timeout: options.timeout ?? 1e4 });
815
+ }
816
+ function buildSystemdUnit() {
817
+ return `[Unit]
818
+ Description=rAgent Live connector
819
+ After=network-online.target
820
+ Wants=network-online.target
821
+
822
+ [Service]
823
+ Type=simple
824
+ ExecStart=${process.execPath} ${__filename} run
825
+ Restart=always
826
+ RestartSec=3
827
+ Environment=NODE_ENV=production
828
+ NoNewPrivileges=true
829
+ PrivateTmp=true
830
+ ProtectSystem=strict
831
+ ProtectHome=read-only
832
+ ReadWritePaths=%h/.config/ragent
833
+
834
+ [Install]
835
+ WantedBy=default.target
836
+ `;
837
+ }
838
+ async function installSystemdService(opts = {}) {
839
+ assertConfiguredAgentToken();
840
+ fs2.mkdirSync(SERVICE_DIR, { recursive: true });
841
+ fs2.writeFileSync(SERVICE_FILE, buildSystemdUnit(), "utf8");
842
+ await runSystemctlUser(["daemon-reload"]);
843
+ if (opts.enable !== false) {
844
+ await runSystemctlUser(["enable", SERVICE_NAME]);
845
+ }
846
+ if (opts.start) {
847
+ await runSystemctlUser(["restart", SERVICE_NAME]);
848
+ }
849
+ saveConfigPatch({ serviceBackend: "systemd" });
850
+ console.log(`[rAgent] Installed systemd user service at ${SERVICE_FILE}`);
851
+ }
852
+ function readFallbackPid() {
853
+ try {
854
+ const raw = fs2.readFileSync(FALLBACK_PID_FILE, "utf8").trim();
855
+ const pid = Number.parseInt(raw, 10);
856
+ return Number.isInteger(pid) ? pid : null;
857
+ } catch {
858
+ return null;
859
+ }
860
+ }
861
+ function isProcessRunning(pid) {
862
+ if (!pid || !Number.isInteger(pid)) return false;
863
+ try {
864
+ process.kill(pid, 0);
865
+ return true;
866
+ } catch {
867
+ return false;
868
+ }
869
+ }
870
+ async function startPidfileService() {
871
+ assertConfiguredAgentToken();
872
+ const existingPid = readFallbackPid();
873
+ if (existingPid && isProcessRunning(existingPid)) {
874
+ console.log(`[rAgent] Service already running (pid ${existingPid})`);
875
+ return;
876
+ }
877
+ ensureConfigDir();
878
+ const logFd = fs2.openSync(FALLBACK_LOG_FILE, "a");
879
+ const child = (0, import_child_process.spawn)(process.execPath, [__filename, "run"], {
880
+ detached: true,
881
+ stdio: ["ignore", logFd, logFd],
882
+ cwd: os4.homedir(),
883
+ env: process.env
884
+ });
885
+ child.unref();
886
+ fs2.closeSync(logFd);
887
+ fs2.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
888
+ `, "utf8");
889
+ saveConfigPatch({ serviceBackend: "pidfile" });
890
+ console.log(`[rAgent] Started fallback background service (pid ${child.pid})`);
891
+ console.log(`[rAgent] Logs: ${FALLBACK_LOG_FILE}`);
892
+ }
893
+ async function stopPidfileService() {
894
+ const pid = readFallbackPid();
895
+ if (!pid || !isProcessRunning(pid)) {
896
+ try {
897
+ fs2.unlinkSync(FALLBACK_PID_FILE);
898
+ } catch {
899
+ }
900
+ console.log("[rAgent] Service is not running.");
901
+ return;
902
+ }
903
+ process.kill(pid, "SIGTERM");
904
+ for (let i = 0; i < 20; i += 1) {
905
+ if (!isProcessRunning(pid)) break;
906
+ await wait(150);
907
+ }
908
+ if (isProcessRunning(pid)) {
909
+ process.kill(pid, "SIGKILL");
910
+ }
911
+ try {
912
+ fs2.unlinkSync(FALLBACK_PID_FILE);
913
+ } catch {
914
+ }
915
+ console.log(`[rAgent] Stopped fallback background service (pid ${pid})`);
916
+ }
917
+ async function ensureServiceInstalled(opts = {}) {
918
+ const wantsSystemd = await canUseSystemdUser();
919
+ if (wantsSystemd) {
920
+ await installSystemdService(opts);
921
+ return "systemd";
922
+ }
923
+ saveConfigPatch({ serviceBackend: "pidfile" });
924
+ if (opts.start) {
925
+ await startPidfileService();
926
+ } else {
927
+ console.log(
928
+ "[rAgent] systemd user manager unavailable; using fallback pidfile backend."
929
+ );
930
+ }
931
+ return "pidfile";
932
+ }
933
+ async function startService() {
934
+ const backend = getConfiguredServiceBackend();
935
+ if (backend === "systemd") {
936
+ await runSystemctlUser(["start", SERVICE_NAME]);
937
+ console.log("[rAgent] Started service via systemd.");
938
+ return;
939
+ }
940
+ if (backend === "pidfile") {
941
+ await startPidfileService();
942
+ return;
943
+ }
944
+ await ensureServiceInstalled({ start: true, enable: true });
945
+ }
946
+ async function stopService() {
947
+ const backend = getConfiguredServiceBackend();
948
+ if (backend === "systemd") {
949
+ await runSystemctlUser(["stop", SERVICE_NAME]);
950
+ console.log("[rAgent] Stopped service via systemd.");
951
+ return;
952
+ }
953
+ await stopPidfileService();
954
+ }
955
+ async function restartService() {
956
+ const backend = getConfiguredServiceBackend();
957
+ if (backend === "systemd") {
958
+ await runSystemctlUser(["restart", SERVICE_NAME]);
959
+ console.log("[rAgent] Restarted service via systemd.");
960
+ return;
961
+ }
962
+ await stopPidfileService();
963
+ await startPidfileService();
964
+ }
965
+ async function printServiceStatus() {
966
+ const backend = getConfiguredServiceBackend();
967
+ if (backend === "systemd") {
968
+ const status = await execAsync(
969
+ `systemctl --user status ${shellQuote(SERVICE_NAME)} --no-pager --lines=20`,
970
+ { timeout: 1e4 }
971
+ ).catch((error) => {
972
+ console.log(error.message);
973
+ return "";
974
+ });
975
+ if (status) {
976
+ process.stdout.write(status);
977
+ }
978
+ return;
979
+ }
980
+ const pid = readFallbackPid();
981
+ if (pid && isProcessRunning(pid)) {
982
+ console.log(`[rAgent] fallback service running (pid ${pid})`);
983
+ console.log(`[rAgent] logs: ${FALLBACK_LOG_FILE}`);
984
+ return;
985
+ }
986
+ console.log("[rAgent] service is not running.");
987
+ }
988
+ async function printServiceLogs(opts) {
989
+ const lines = Number.parseInt(String(opts.lines || 100), 10) || 100;
990
+ const follow = Boolean(opts.follow);
991
+ const backend = getConfiguredServiceBackend();
992
+ if (backend === "systemd") {
993
+ if (follow) {
994
+ await new Promise((resolve) => {
995
+ const child = (0, import_child_process.spawn)(
996
+ "journalctl",
997
+ ["--user", "-u", SERVICE_NAME, "-f", "-n", String(lines)],
998
+ { stdio: "inherit" }
999
+ );
1000
+ child.on("exit", () => resolve());
1001
+ });
1002
+ return;
1003
+ }
1004
+ const output = await execAsync(
1005
+ `journalctl --user -u ${shellQuote(SERVICE_NAME)} -n ${lines} --no-pager`,
1006
+ { timeout: 12e3 }
1007
+ );
1008
+ process.stdout.write(output);
1009
+ return;
1010
+ }
1011
+ if (!fs2.existsSync(FALLBACK_LOG_FILE)) {
1012
+ console.log(`[rAgent] No log file found at ${FALLBACK_LOG_FILE}`);
1013
+ return;
1014
+ }
1015
+ if (follow) {
1016
+ await new Promise((resolve) => {
1017
+ const child = (0, import_child_process.spawn)("tail", ["-n", String(lines), "-f", FALLBACK_LOG_FILE], {
1018
+ stdio: "inherit"
1019
+ });
1020
+ child.on("exit", () => resolve());
1021
+ });
1022
+ return;
1023
+ }
1024
+ const content = fs2.readFileSync(FALLBACK_LOG_FILE, "utf8");
1025
+ const tail = content.split("\n").slice(-lines).join("\n");
1026
+ process.stdout.write(`${tail}${tail.endsWith("\n") ? "" : "\n"}`);
1027
+ }
1028
+ async function uninstallService() {
1029
+ const backend = getConfiguredServiceBackend();
1030
+ if (backend === "systemd") {
1031
+ await runSystemctlUser(["stop", SERVICE_NAME]).catch(() => void 0);
1032
+ await runSystemctlUser(["disable", SERVICE_NAME]).catch(() => void 0);
1033
+ try {
1034
+ fs2.unlinkSync(SERVICE_FILE);
1035
+ } catch {
1036
+ }
1037
+ await runSystemctlUser(["daemon-reload"]).catch(() => void 0);
1038
+ } else {
1039
+ await stopPidfileService();
1040
+ }
1041
+ const config = loadConfig();
1042
+ delete config.serviceBackend;
1043
+ saveConfig(config);
1044
+ console.log("[rAgent] Service uninstalled.");
1045
+ }
1046
+ function requestStopSelfService() {
1047
+ const backend = getConfiguredServiceBackend();
1048
+ if (backend === "systemd") {
1049
+ const child = (0, import_child_process.spawn)("systemctl", ["--user", "stop", SERVICE_NAME], {
1050
+ detached: true,
1051
+ stdio: "ignore"
1052
+ });
1053
+ child.unref();
1054
+ return;
1055
+ }
1056
+ if (backend === "pidfile") {
1057
+ try {
1058
+ fs2.unlinkSync(FALLBACK_PID_FILE);
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ // src/websocket.ts
1065
+ var import_ws = __toESM(require("ws"));
1066
+ function sendToGroup(ws, group, data) {
1067
+ if (!group || ws.readyState !== import_ws.default.OPEN) return;
1068
+ ws.send(
1069
+ JSON.stringify({
1070
+ type: "sendToGroup",
1071
+ group,
1072
+ dataType: "json",
1073
+ data,
1074
+ noEcho: true
1075
+ })
1076
+ );
1077
+ }
1078
+
1079
+ // src/provisioner.ts
1080
+ var import_child_process2 = require("child_process");
1081
+ var DANGEROUS_PATTERN = /rm\s+-r[fe]*\s+\/|mkfs|dd\s+if=|:()\s*\{|>\s*\/dev\/sd/i;
1082
+ function shellQuote2(s) {
1083
+ return `'${s.replace(/'/g, "'\\''")}'`;
1084
+ }
1085
+ var SESSION_NAME_RE = /^[a-zA-Z0-9_-]+$/;
1086
+ var MAX_SESSION_NAME = 128;
1087
+ function runCommand(cmd, timeout = 12e4) {
1088
+ return (0, import_child_process2.execSync)(cmd, {
1089
+ encoding: "utf8",
1090
+ timeout,
1091
+ maxBuffer: 10 * 1024 * 1024,
1092
+ // 10 MB
1093
+ stdio: ["pipe", "pipe", "pipe"]
1094
+ }).trim();
1095
+ }
1096
+ function commandExists2(cmd) {
1097
+ if (!/^[a-zA-Z0-9._+-]+$/.test(cmd)) return false;
1098
+ try {
1099
+ (0, import_child_process2.execFileSync)("sh", ["-c", `command -v -- ${cmd} >/dev/null 2>&1`], { stdio: "ignore" });
1100
+ return true;
1101
+ } catch {
1102
+ return false;
1103
+ }
1104
+ }
1105
+ function getVersion(cmd) {
1106
+ try {
1107
+ const output = runCommand(`${cmd} --version`, 1e4);
1108
+ const match = output.match(/(\d+(?:\.\d+)*)/);
1109
+ return match ? match[1] : null;
1110
+ } catch {
1111
+ return null;
1112
+ }
1113
+ }
1114
+ function majorVersion(version) {
1115
+ return parseInt(version.split(".")[0], 10);
1116
+ }
1117
+ function checkPrerequisites(manifest, onProgress) {
1118
+ onProgress({
1119
+ type: "provision-progress",
1120
+ provisionId: "",
1121
+ step: "checking-prereqs",
1122
+ message: `Checking prerequisites for ${manifest.name}...`
1123
+ });
1124
+ if (!manifest.prerequisites || manifest.prerequisites.runtime === "none") {
1125
+ return true;
1126
+ }
1127
+ const { runtime, minVersion } = manifest.prerequisites;
1128
+ const runtimeCmd = runtime === "node" ? "node" : "python3";
1129
+ if (!commandExists2(runtimeCmd)) {
1130
+ onProgress({
1131
+ type: "provision-progress",
1132
+ provisionId: "",
1133
+ step: "error",
1134
+ message: `${runtimeCmd} is not installed. Please install ${runtime === "node" ? "Node.js" : "Python"} first.`
1135
+ });
1136
+ return false;
1137
+ }
1138
+ if (minVersion) {
1139
+ const currentVersion = getVersion(runtimeCmd);
1140
+ if (!currentVersion) {
1141
+ onProgress({
1142
+ type: "provision-progress",
1143
+ provisionId: "",
1144
+ step: "error",
1145
+ message: `Could not determine ${runtimeCmd} version.`
1146
+ });
1147
+ return false;
1148
+ }
1149
+ if (majorVersion(currentVersion) < majorVersion(minVersion)) {
1150
+ onProgress({
1151
+ type: "provision-progress",
1152
+ provisionId: "",
1153
+ step: "error",
1154
+ message: `${runtimeCmd} version ${currentVersion} is too old. Minimum required: ${minVersion}.`
1155
+ });
1156
+ return false;
1157
+ }
1158
+ }
1159
+ onProgress({
1160
+ type: "provision-progress",
1161
+ provisionId: "",
1162
+ step: "checking-prereqs",
1163
+ message: `Prerequisites satisfied.`
1164
+ });
1165
+ return true;
1166
+ }
1167
+ function checkInstalled(manifest, onProgress) {
1168
+ if (!manifest.checkCommand) return false;
1169
+ try {
1170
+ runCommand(manifest.checkCommand, 1e4);
1171
+ onProgress({
1172
+ type: "provision-progress",
1173
+ provisionId: "",
1174
+ step: "checking-prereqs",
1175
+ message: `${manifest.name} is already installed.`
1176
+ });
1177
+ return true;
1178
+ } catch {
1179
+ return false;
1180
+ }
1181
+ }
1182
+ function installAgent(manifest, onProgress) {
1183
+ if (!manifest.installCommand || manifest.installMethod === "custom") {
1184
+ return true;
1185
+ }
1186
+ onProgress({
1187
+ type: "provision-progress",
1188
+ provisionId: "",
1189
+ step: "installing",
1190
+ message: `Installing ${manifest.name}: ${manifest.installCommand}`
1191
+ });
1192
+ try {
1193
+ runCommand(manifest.installCommand, 3e5);
1194
+ onProgress({
1195
+ type: "provision-progress",
1196
+ provisionId: "",
1197
+ step: "installing",
1198
+ message: `${manifest.name} installed successfully.`
1199
+ });
1200
+ return true;
1201
+ } catch (error) {
1202
+ const message = error instanceof Error ? error.message : String(error);
1203
+ onProgress({
1204
+ type: "provision-progress",
1205
+ provisionId: "",
1206
+ step: "error",
1207
+ message: `Failed to install ${manifest.name}: ${message}`
1208
+ });
1209
+ return false;
1210
+ }
1211
+ }
1212
+ function startAgent(request, onProgress) {
1213
+ if (!request.sessionName || request.sessionName.length > MAX_SESSION_NAME || !SESSION_NAME_RE.test(request.sessionName)) {
1214
+ onProgress({
1215
+ type: "provision-progress",
1216
+ provisionId: request.provisionId,
1217
+ step: "error",
1218
+ message: "Invalid session name. Must be alphanumeric with hyphens/underscores, max 128 chars."
1219
+ });
1220
+ return false;
1221
+ }
1222
+ if (DANGEROUS_PATTERN.test(request.command)) {
1223
+ onProgress({
1224
+ type: "provision-progress",
1225
+ provisionId: request.provisionId,
1226
+ step: "error",
1227
+ message: "Rejected: command matches dangerous pattern."
1228
+ });
1229
+ return false;
1230
+ }
1231
+ onProgress({
1232
+ type: "provision-progress",
1233
+ provisionId: request.provisionId,
1234
+ step: "starting",
1235
+ message: `Starting ${request.manifest.name} in tmux session "${request.sessionName}"...`
1236
+ });
1237
+ const tmuxArgs = ["new-session", "-d", "-s", request.sessionName];
1238
+ if (request.workingDir) {
1239
+ tmuxArgs.push("-c", request.workingDir);
1240
+ }
1241
+ const envEntries = request.envVars ? Object.entries(request.envVars).filter(([k, v]) => /^[A-Z_][A-Z0-9_]*$/i.test(k) && typeof v === "string") : [];
1242
+ let fullCmd = request.command;
1243
+ if (envEntries.length > 0) {
1244
+ const envPrefix = envEntries.map(([k, v]) => `${k}=${shellQuote2(v)}`).join(" ");
1245
+ fullCmd = `${envPrefix} ${request.command}`;
1246
+ }
1247
+ tmuxArgs.push(fullCmd);
1248
+ try {
1249
+ (0, import_child_process2.execFileSync)("tmux", tmuxArgs, { stdio: "ignore" });
1250
+ onProgress({
1251
+ type: "provision-progress",
1252
+ provisionId: request.provisionId,
1253
+ step: "complete",
1254
+ message: `${request.manifest.name} is running in session "${request.sessionName}".`
1255
+ });
1256
+ return true;
1257
+ } catch (error) {
1258
+ const message = error instanceof Error ? error.message : String(error);
1259
+ onProgress({
1260
+ type: "provision-progress",
1261
+ provisionId: request.provisionId,
1262
+ step: "error",
1263
+ message: `Failed to start tmux session: ${message}`
1264
+ });
1265
+ return false;
1266
+ }
1267
+ }
1268
+ async function executeProvision(request, onProgress) {
1269
+ const emit = (progress) => {
1270
+ onProgress({ ...progress, provisionId: request.provisionId });
1271
+ };
1272
+ if (!checkPrerequisites(request.manifest, emit)) {
1273
+ return false;
1274
+ }
1275
+ const alreadyInstalled = checkInstalled(request.manifest, emit);
1276
+ if (!alreadyInstalled) {
1277
+ if (!installAgent(request.manifest, emit)) {
1278
+ return false;
1279
+ }
1280
+ }
1281
+ return startAgent(request, emit);
1282
+ }
1283
+
1284
+ // src/agent.ts
1285
+ function pidFilePath(hostId) {
1286
+ return path2.join(CONFIG_DIR, `agent-${hostId}.pid`);
1287
+ }
1288
+ function readPidFile(filePath) {
1289
+ try {
1290
+ const raw = fs3.readFileSync(filePath, "utf8").trim();
1291
+ const pid = Number.parseInt(raw, 10);
1292
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
1293
+ } catch {
1294
+ return null;
1295
+ }
1296
+ }
1297
+ function isProcessAlive(pid) {
1298
+ try {
1299
+ process.kill(pid, 0);
1300
+ return true;
1301
+ } catch {
1302
+ return false;
1303
+ }
1304
+ }
1305
+ function acquirePidLock(hostId) {
1306
+ ensureConfigDir();
1307
+ const lockPath = pidFilePath(hostId);
1308
+ const existingPid = readPidFile(lockPath);
1309
+ if (existingPid && existingPid !== process.pid && isProcessAlive(existingPid)) {
1310
+ throw new Error(
1311
+ `Another agent for host "${hostId}" is already running (pid ${existingPid}).
1312
+ Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
1313
+ );
1314
+ }
1315
+ fs3.writeFileSync(lockPath, `${process.pid}
1316
+ `, "utf8");
1317
+ return lockPath;
1318
+ }
1319
+ function releasePidLock(lockPath) {
1320
+ try {
1321
+ const currentPid = readPidFile(lockPath);
1322
+ if (currentPid === process.pid) {
1323
+ fs3.unlinkSync(lockPath);
1324
+ }
1325
+ } catch {
1326
+ }
1327
+ }
1328
+ function resolveRunOptions(commandOptions) {
1329
+ const config = loadConfig();
1330
+ const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
1331
+ const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
1332
+ const hostName = commandOptions.name || config.hostName || os5.hostname();
1333
+ const command = commandOptions.command || config.command || "bash";
1334
+ const agentToken = commandOptions.agentToken || config.agentToken || "";
1335
+ return { portal, hostId, hostName, command, agentToken };
1336
+ }
1337
+ async function runAgent(rawOptions) {
1338
+ const options = resolveRunOptions(rawOptions);
1339
+ if (!options.agentToken) {
1340
+ throw new Error("No agent token found. Run `ragent connect` first.");
1341
+ }
1342
+ const lockPath = acquirePidLock(options.hostId);
1343
+ console.log(`[rAgent] Connector started for ${options.hostName} (${options.hostId})`);
1344
+ console.log(`[rAgent] Portal: ${options.portal}`);
1345
+ try {
1346
+ const existingSessions = await collectSessionInventory(options.hostId, options.command);
1347
+ const tmuxCount = existingSessions.filter((s) => s.type === "tmux").length;
1348
+ if (tmuxCount > 0) {
1349
+ console.log(`[rAgent] Found ${tmuxCount} existing tmux session(s) on this machine.`);
1350
+ }
1351
+ } catch {
1352
+ }
1353
+ let shouldRun = true;
1354
+ let reconnectRequested = false;
1355
+ let reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
1356
+ let activeSocket = null;
1357
+ let activeGroups = { privateGroup: "", registryGroup: "" };
1358
+ let wsHeartbeatTimer = null;
1359
+ let httpHeartbeatTimer = null;
1360
+ let suppressNextShellRespawn = false;
1361
+ let lastSentFingerprint = "";
1362
+ let lastHttpHeartbeatAt = 0;
1363
+ const outputBuffer = new OutputBuffer();
1364
+ let ptyProcess = null;
1365
+ const sendOutput = (chunk) => {
1366
+ const ws = activeSocket;
1367
+ if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.privateGroup) {
1368
+ outputBuffer.push(chunk);
1369
+ return;
1370
+ }
1371
+ sendToGroup(ws, activeGroups.privateGroup, { type: "output", data: chunk });
1372
+ };
1373
+ const killCurrentShell = () => {
1374
+ if (!ptyProcess) return;
1375
+ suppressNextShellRespawn = true;
1376
+ try {
1377
+ ptyProcess.kill();
1378
+ } catch {
1379
+ }
1380
+ ptyProcess = null;
1381
+ };
1382
+ const spawnOrRespawnShell = () => {
1383
+ ptyProcess = spawnConnectorShell(options.command, sendOutput, () => {
1384
+ if (suppressNextShellRespawn) {
1385
+ suppressNextShellRespawn = false;
1386
+ return;
1387
+ }
1388
+ if (!shouldRun) return;
1389
+ console.warn("[rAgent] Shell exited. Restarting shell process.");
1390
+ setTimeout(() => {
1391
+ if (shouldRun) spawnOrRespawnShell();
1392
+ }, 200);
1393
+ });
1394
+ };
1395
+ const restartLocalShell = () => {
1396
+ killCurrentShell();
1397
+ spawnOrRespawnShell();
1398
+ };
1399
+ spawnOrRespawnShell();
1400
+ const cleanupSocket = () => {
1401
+ if (wsHeartbeatTimer) {
1402
+ clearInterval(wsHeartbeatTimer);
1403
+ wsHeartbeatTimer = null;
1404
+ }
1405
+ if (httpHeartbeatTimer) {
1406
+ clearInterval(httpHeartbeatTimer);
1407
+ httpHeartbeatTimer = null;
1408
+ }
1409
+ if (activeSocket) {
1410
+ activeSocket.removeAllListeners();
1411
+ try {
1412
+ activeSocket.close();
1413
+ } catch {
1414
+ }
1415
+ activeSocket = null;
1416
+ }
1417
+ activeGroups = { privateGroup: "", registryGroup: "" };
1418
+ };
1419
+ const collectVitals = () => {
1420
+ const cpus2 = os5.cpus();
1421
+ let totalIdle = 0;
1422
+ let totalTick = 0;
1423
+ for (const cpu of cpus2) {
1424
+ for (const type of Object.keys(cpu.times)) {
1425
+ totalTick += cpu.times[type];
1426
+ }
1427
+ totalIdle += cpu.times.idle;
1428
+ }
1429
+ const cpuUsage = totalTick > 0 ? Math.round((totalTick - totalIdle) / totalTick * 100) : 0;
1430
+ const totalMem = os5.totalmem();
1431
+ const freeMem = os5.freemem();
1432
+ const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
1433
+ let diskUsedPct = 0;
1434
+ try {
1435
+ const { execSync: execSync2 } = require("child_process");
1436
+ const dfOutput = execSync2("df -P / | tail -1", { encoding: "utf8", timeout: 5e3 });
1437
+ const parts = dfOutput.trim().split(/\s+/);
1438
+ if (parts.length >= 5) {
1439
+ diskUsedPct = parseInt(parts[4].replace("%", ""), 10) || 0;
1440
+ }
1441
+ } catch {
1442
+ }
1443
+ return { cpu: cpuUsage, memUsedPct, diskUsedPct };
1444
+ };
1445
+ const announceToRegistry = async (type = "heartbeat") => {
1446
+ const ws = activeSocket;
1447
+ if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.registryGroup) return;
1448
+ const sessions = await collectSessionInventory(options.hostId, options.command);
1449
+ const vitals = collectVitals();
1450
+ sendToGroup(ws, activeGroups.registryGroup, {
1451
+ type,
1452
+ hostId: options.hostId,
1453
+ hostName: options.hostName,
1454
+ environment: os5.hostname(),
1455
+ sessions,
1456
+ vitals,
1457
+ agentVersion: CURRENT_VERSION,
1458
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
1459
+ });
1460
+ };
1461
+ const syncInventory = async (force = false) => {
1462
+ const sessions = await collectSessionInventory(options.hostId, options.command);
1463
+ const fingerprint = sessionInventoryFingerprint(sessions);
1464
+ const changed = fingerprint !== lastSentFingerprint;
1465
+ const checkpointDue = Date.now() - lastHttpHeartbeatAt > HTTP_HEARTBEAT_MS;
1466
+ if (changed || force) {
1467
+ await announceToRegistry("inventory");
1468
+ lastSentFingerprint = fingerprint;
1469
+ }
1470
+ if (changed || checkpointDue || force) {
1471
+ await postHeartbeat({
1472
+ portal: options.portal,
1473
+ agentToken: options.agentToken,
1474
+ hostId: options.hostId,
1475
+ hostName: options.hostName,
1476
+ command: options.command
1477
+ });
1478
+ lastHttpHeartbeatAt = Date.now();
1479
+ }
1480
+ };
1481
+ const handleControlAction = async (payload) => {
1482
+ const action = typeof payload?.action === "string" ? payload.action : "";
1483
+ const sessionId = typeof payload?.sessionId === "string" && payload.sessionId.trim().length > 0 ? payload.sessionId.trim() : null;
1484
+ switch (action) {
1485
+ case "restart-shell":
1486
+ restartLocalShell();
1487
+ await syncInventory();
1488
+ return;
1489
+ case "restart-agent":
1490
+ case "disconnect":
1491
+ reconnectRequested = true;
1492
+ if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1493
+ activeSocket.close();
1494
+ }
1495
+ return;
1496
+ case "stop-agent":
1497
+ shouldRun = false;
1498
+ requestStopSelfService();
1499
+ if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1500
+ activeSocket.close();
1501
+ }
1502
+ return;
1503
+ case "stop-session":
1504
+ if (!sessionId) return;
1505
+ if (sessionId.startsWith("pty:")) {
1506
+ restartLocalShell();
1507
+ await syncInventory();
1508
+ return;
1509
+ }
1510
+ if (sessionId.startsWith("tmux:")) {
1511
+ try {
1512
+ await stopTmuxPaneBySessionId(sessionId);
1513
+ console.log(`[rAgent] Closed remote session ${sessionId}.`);
1514
+ } catch (error) {
1515
+ const message = error instanceof Error ? error.message : String(error);
1516
+ console.warn(
1517
+ `[rAgent] Failed to close ${sessionId}: ${message}`
1518
+ );
1519
+ }
1520
+ await syncInventory();
1521
+ }
1522
+ return;
1523
+ case "stop-detached": {
1524
+ const killed = await stopAllDetachedTmuxSessions();
1525
+ console.log(`[rAgent] Killed ${killed} detached tmux session(s).`);
1526
+ await syncInventory();
1527
+ return;
1528
+ }
1529
+ case "start-agent": {
1530
+ const sessionName = typeof payload?.sessionName === "string" && payload.sessionName.trim().length > 0 ? payload.sessionName.trim() : `agent-${Date.now().toString(36)}`;
1531
+ const cmd = typeof payload?.command === "string" && payload.command.trim().length > 0 ? payload.command.trim() : null;
1532
+ if (!cmd) {
1533
+ console.warn("[rAgent] start-agent: no command provided, ignoring.");
1534
+ return;
1535
+ }
1536
+ if (sessionName.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
1537
+ console.warn("[rAgent] start-agent: invalid session name, ignoring.");
1538
+ return;
1539
+ }
1540
+ const dangerous = /rm\s+-r[fe]*\s+\/|mkfs|dd\s+if=|:()\s*\{|>\s*\/dev\/sd/i;
1541
+ if (dangerous.test(cmd)) {
1542
+ console.warn(`[rAgent] start-agent: rejected dangerous command: ${cmd}`);
1543
+ return;
1544
+ }
1545
+ const workingDir = typeof payload?.workingDir === "string" && payload.workingDir.trim().length > 0 ? payload.workingDir.trim() : void 0;
1546
+ const envVars = payload?.envVars && typeof payload.envVars === "object" ? payload.envVars : void 0;
1547
+ const tmuxArgs = ["new-session", "-d", "-s", sessionName];
1548
+ if (workingDir) {
1549
+ tmuxArgs.push("-c", workingDir);
1550
+ }
1551
+ let fullCmd = cmd;
1552
+ if (envVars) {
1553
+ const entries = Object.entries(envVars).filter(([k, v]) => /^[A-Z_][A-Z0-9_]*$/i.test(k) && typeof v === "string").map(([k, v]) => `${k}='${v.replace(/'/g, "'\\''")}'`).join(" ");
1554
+ if (entries) fullCmd = `${entries} ${cmd}`;
1555
+ }
1556
+ tmuxArgs.push(fullCmd);
1557
+ try {
1558
+ const { execFileSync: execFileSync2 } = await import("child_process");
1559
+ execFileSync2("tmux", tmuxArgs, { stdio: "ignore" });
1560
+ console.log(`[rAgent] Started agent session "${sessionName}": ${cmd}`);
1561
+ } catch (error) {
1562
+ const message = error instanceof Error ? error.message : String(error);
1563
+ console.error(`[rAgent] Failed to start agent session "${sessionName}": ${message}`);
1564
+ }
1565
+ await syncInventory(true);
1566
+ return;
1567
+ }
1568
+ default:
1569
+ }
1570
+ };
1571
+ const onSignal = () => {
1572
+ shouldRun = false;
1573
+ cleanupSocket();
1574
+ killCurrentShell();
1575
+ releasePidLock(lockPath);
1576
+ };
1577
+ process.once("SIGTERM", onSignal);
1578
+ process.once("SIGINT", onSignal);
1579
+ try {
1580
+ while (shouldRun) {
1581
+ reconnectRequested = false;
1582
+ try {
1583
+ options.agentToken = await refreshTokenIfNeeded({
1584
+ portal: options.portal,
1585
+ agentToken: options.agentToken
1586
+ });
1587
+ const negotiated = await negotiateAgent({
1588
+ portal: options.portal,
1589
+ agentToken: options.agentToken
1590
+ });
1591
+ activeGroups = {
1592
+ privateGroup: negotiated.groups.privateGroup,
1593
+ registryGroup: negotiated.groups.registryGroup
1594
+ };
1595
+ await new Promise((resolve) => {
1596
+ const ws = new import_ws2.default(
1597
+ negotiated.url,
1598
+ "json.webpubsub.azure.v1"
1599
+ );
1600
+ activeSocket = ws;
1601
+ ws.on("open", async () => {
1602
+ console.log("[rAgent] Connector connected to relay.");
1603
+ reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
1604
+ ws.send(
1605
+ JSON.stringify({
1606
+ type: "joinGroup",
1607
+ group: activeGroups.privateGroup
1608
+ })
1609
+ );
1610
+ ws.send(
1611
+ JSON.stringify({
1612
+ type: "joinGroup",
1613
+ group: activeGroups.registryGroup
1614
+ })
1615
+ );
1616
+ sendToGroup(ws, activeGroups.privateGroup, {
1617
+ type: "register",
1618
+ hostName: options.hostName,
1619
+ environment: os5.platform()
1620
+ });
1621
+ const buffered = outputBuffer.drain();
1622
+ if (buffered.length > 0) {
1623
+ console.log(
1624
+ `[rAgent] Replaying ${buffered.length} buffered output chunks (${buffered.reduce((sum, c) => sum + Buffer.byteLength(c, "utf8"), 0)} bytes)`
1625
+ );
1626
+ for (const chunk of buffered) {
1627
+ sendToGroup(ws, activeGroups.privateGroup, {
1628
+ type: "output",
1629
+ data: chunk
1630
+ });
1631
+ }
1632
+ }
1633
+ await syncInventory(true);
1634
+ wsHeartbeatTimer = setInterval(async () => {
1635
+ if (!activeSocket || activeSocket.readyState !== import_ws2.default.OPEN)
1636
+ return;
1637
+ await announceToRegistry("heartbeat");
1638
+ }, WS_HEARTBEAT_MS);
1639
+ httpHeartbeatTimer = setInterval(async () => {
1640
+ if (!activeSocket || activeSocket.readyState !== import_ws2.default.OPEN)
1641
+ return;
1642
+ await syncInventory();
1643
+ }, HTTP_HEARTBEAT_MS);
1644
+ });
1645
+ ws.on("message", async (data) => {
1646
+ let msg;
1647
+ try {
1648
+ msg = JSON.parse(data.toString());
1649
+ } catch {
1650
+ return;
1651
+ }
1652
+ if (msg.type === "message" && msg.group === activeGroups.privateGroup) {
1653
+ const payload = msg.data || {};
1654
+ if (payload.type === "input" && typeof payload.data === "string") {
1655
+ if (ptyProcess) ptyProcess.write(payload.data);
1656
+ } else if (payload.type === "resize" && Number.isInteger(payload.cols) && Number.isInteger(payload.rows)) {
1657
+ try {
1658
+ if (ptyProcess)
1659
+ ptyProcess.resize(
1660
+ payload.cols,
1661
+ payload.rows
1662
+ );
1663
+ } catch (error) {
1664
+ const message = error instanceof Error ? error.message : String(error);
1665
+ if (!message.includes("EBADF")) {
1666
+ console.warn(`[rAgent] Resize failed: ${message}`);
1667
+ }
1668
+ }
1669
+ } else if (payload.type === "control" && typeof payload.action === "string") {
1670
+ await handleControlAction(payload);
1671
+ } else if (payload.type === "start-agent") {
1672
+ await handleControlAction({ ...payload, action: "start-agent" });
1673
+ } else if (payload.type === "provision") {
1674
+ const provReq = payload;
1675
+ if (provReq.provisionId && provReq.manifest) {
1676
+ console.log(`[rAgent] Provision request: ${provReq.manifest.name} (${provReq.provisionId})`);
1677
+ const sendProgress = (progress) => {
1678
+ const currentWs = activeSocket;
1679
+ if (currentWs && currentWs.readyState === import_ws2.default.OPEN && activeGroups.registryGroup) {
1680
+ sendToGroup(currentWs, activeGroups.registryGroup, {
1681
+ ...progress,
1682
+ hostId: options.hostId
1683
+ });
1684
+ }
1685
+ };
1686
+ try {
1687
+ await executeProvision(provReq, sendProgress);
1688
+ await syncInventory(true);
1689
+ } catch (error) {
1690
+ const errMsg = error instanceof Error ? error.message : String(error);
1691
+ sendProgress({
1692
+ type: "provision-progress",
1693
+ provisionId: provReq.provisionId,
1694
+ step: "error",
1695
+ message: `Provision failed: ${errMsg}`
1696
+ });
1697
+ }
1698
+ }
1699
+ }
1700
+ }
1701
+ if (msg.type === "message" && msg.group === activeGroups.registryGroup) {
1702
+ const payload = msg.data || {};
1703
+ if (payload.type === "ping") {
1704
+ await announceToRegistry("announce");
1705
+ }
1706
+ }
1707
+ });
1708
+ ws.on("error", (error) => {
1709
+ console.error("[rAgent] WebSocket error:", error.message);
1710
+ });
1711
+ ws.on("close", () => {
1712
+ console.log(
1713
+ "[rAgent] Relay disconnected. Output will be buffered until reconnect."
1714
+ );
1715
+ cleanupSocket();
1716
+ resolve();
1717
+ });
1718
+ });
1719
+ } catch (error) {
1720
+ if (error instanceof AuthError) {
1721
+ console.error(`[rAgent] ${error.message}`);
1722
+ console.error(
1723
+ "[rAgent] Connector token is invalid or revoked. Stopping. Re-connect with: ragent connect --token <token>"
1724
+ );
1725
+ shouldRun = false;
1726
+ break;
1727
+ }
1728
+ const message = error instanceof Error ? error.message : String(error);
1729
+ console.error(`[rAgent] Relay connect failed: ${message}`);
1730
+ }
1731
+ if (!shouldRun) break;
1732
+ if (reconnectRequested) {
1733
+ console.log("[rAgent] Reconnecting to relay...");
1734
+ await wait(300);
1735
+ continue;
1736
+ }
1737
+ console.log(
1738
+ `[rAgent] Disconnected. Reconnecting in ${Math.round(reconnectDelay / 1e3)}s...`
1739
+ );
1740
+ await wait(reconnectDelay);
1741
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
1742
+ }
1743
+ } finally {
1744
+ cleanupSocket();
1745
+ killCurrentShell();
1746
+ releasePidLock(lockPath);
1747
+ process.removeListener("SIGTERM", onSignal);
1748
+ process.removeListener("SIGINT", onSignal);
1749
+ }
1750
+ }
1751
+
1752
+ // src/brand.ts
1753
+ var figlet = null;
1754
+ try {
1755
+ figlet = require("figlet");
1756
+ } catch {
1757
+ figlet = null;
1758
+ }
1759
+ var ANSI = {
1760
+ reset: "\x1B[0m",
1761
+ dim: "\x1B[2m",
1762
+ bold: "\x1B[1m"
1763
+ };
1764
+ function supportsColor() {
1765
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR !== "1";
1766
+ }
1767
+ function color(text, ansiCode = "") {
1768
+ if (!supportsColor()) return text;
1769
+ return `${ansiCode}${text}${ANSI.reset}`;
1770
+ }
1771
+ function rgb(r, g, b) {
1772
+ return `\x1B[38;2;${r};${g};${b}m`;
1773
+ }
1774
+ function gradientCharColor(position, total) {
1775
+ const safeTotal = Math.max(total - 1, 1);
1776
+ const t = position / safeTotal;
1777
+ const start = { r: 35, g: 198, b: 255 };
1778
+ const end = { r: 78, g: 129, b: 255 };
1779
+ const r = Math.round(start.r + (end.r - start.r) * t);
1780
+ const g = Math.round(start.g + (end.g - start.g) * t);
1781
+ const b = Math.round(start.b + (end.b - start.b) * t);
1782
+ return rgb(r, g, b);
1783
+ }
1784
+ function colorizeGradientLine(line) {
1785
+ if (!supportsColor()) return line;
1786
+ const chars = [...line];
1787
+ return chars.map((char, i) => `${gradientCharColor(i, chars.length)}${char}`).join("").concat(ANSI.reset);
1788
+ }
1789
+ function generateBrandArt() {
1790
+ const fallback = ["rAgent Live", "remote agent terminal control"];
1791
+ if (!figlet) return fallback;
1792
+ const cols = process.stdout.columns || 120;
1793
+ const tryRender = (text, font) => {
1794
+ try {
1795
+ const rendered = figlet.textSync(text, {
1796
+ font,
1797
+ horizontalLayout: "default",
1798
+ verticalLayout: "default"
1799
+ });
1800
+ const lines = rendered.split("\n");
1801
+ const width = Math.max(...lines.map((line) => line.length), 0);
1802
+ if (width > cols && text.includes(" ")) return null;
1803
+ return lines;
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ };
1808
+ const coderMini = tryRender("rAgent Live", "Coder Mini");
1809
+ if (coderMini) return coderMini;
1810
+ const variants = [
1811
+ ["rAgent Live", "ANSI Shadow"],
1812
+ ["rAgent Live", "Small"],
1813
+ ["rAgent Live", "Standard"],
1814
+ ["rAgent Live", "Slant"],
1815
+ ["rAgent Live", "Mini"],
1816
+ ["rAgent", "Standard"]
1817
+ ];
1818
+ for (const [text, font] of variants) {
1819
+ const rendered = tryRender(text, font);
1820
+ if (rendered) return rendered;
1821
+ }
1822
+ return fallback;
1823
+ }
1824
+ function printCommandArt(title, subtitle = "remote agent control") {
1825
+ if (!SHOW_ART) return;
1826
+ const art = generateBrandArt();
1827
+ art.forEach((line) => {
1828
+ console.log(colorizeGradientLine(line));
1829
+ });
1830
+ console.log(color(" rAgent Live", ANSI.bold));
1831
+ console.log(color(` > ${title}`, ANSI.bold));
1832
+ console.log(color(` > ${subtitle}`, ANSI.dim));
1833
+ console.log("");
1834
+ }
1835
+
1836
+ // src/commands/connect.ts
1837
+ async function connectMachine(opts) {
1838
+ const portal = opts.portal || DEFAULT_PORTAL;
1839
+ const hostId = sanitizeHostId(opts.id || inferHostId());
1840
+ const hostName = opts.name || os6.hostname();
1841
+ const command = opts.command || "bash";
1842
+ if (!opts.token) {
1843
+ throw new Error("Connection token is required.");
1844
+ }
1845
+ const claimed = await claimHost({
1846
+ portal,
1847
+ connectionToken: opts.token,
1848
+ hostId,
1849
+ hostName,
1850
+ command
1851
+ });
1852
+ const config = loadConfig();
1853
+ const nextConfig = {
1854
+ ...config,
1855
+ portal,
1856
+ hostId,
1857
+ hostName,
1858
+ command,
1859
+ agentToken: claimed.agentToken,
1860
+ tokenExpiresAt: claimed.expiresAt,
1861
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1862
+ };
1863
+ if (claimed.refreshToken) {
1864
+ nextConfig.refreshToken = claimed.refreshToken;
1865
+ nextConfig.refreshExpiresAt = claimed.refreshExpiresAt;
1866
+ }
1867
+ saveConfig(nextConfig);
1868
+ console.log(`[rAgent] Machine connected: ${hostName} (${hostId})`);
1869
+ console.log(`[rAgent] Machine name: ${hostName} (override with --name <friendly-name>)`);
1870
+ console.log(`[rAgent] Config saved to ${CONFIG_FILE}`);
1871
+ if (opts.asService) {
1872
+ await ensureServiceInstalled({ enable: true, start: true });
1873
+ console.log("[rAgent] Service mode enabled.");
1874
+ return;
1875
+ }
1876
+ if (opts.run !== false) {
1877
+ await runAgent({
1878
+ portal,
1879
+ id: hostId,
1880
+ name: hostName,
1881
+ command,
1882
+ agentToken: claimed.agentToken
1883
+ });
1884
+ }
1885
+ }
1886
+ function registerConnectCommand(program2) {
1887
+ program2.command("connect").description("Connect a machine to rAgent Live using a short-lived token").requiredOption("--token <token>", "Connection token from portal").option("--portal <url>", "Portal base URL", DEFAULT_PORTAL).option("-i, --id <id>", "Machine ID").option("-n, --name <name>", "Machine name").option("-c, --command <command>", "CLI command to run", "bash").option("--as-service", "Install/start background service after connecting").option("--no-run", "Connect only; do not immediately run connector").action(async (opts) => {
1888
+ try {
1889
+ printCommandArt("Connect Machine", "connecting machine + saving connector credentials");
1890
+ await connectMachine(opts);
1891
+ } catch (error) {
1892
+ const message = error instanceof Error ? error.message : String(error);
1893
+ console.error(`[rAgent] Connect failed: ${message}`);
1894
+ process.exit(1);
1895
+ }
1896
+ });
1897
+ }
1898
+
1899
+ // src/commands/run.ts
1900
+ function registerRunCommand(program2) {
1901
+ program2.command("run").description("Run connector for a connected machine").option("--portal <url>", "Portal base URL").option("--agent-token <token>", "Connector token").option("-i, --id <id>", "Machine ID").option("-n, --name <name>", "Machine name").option("-c, --command <command>", "CLI command to run").action(async (opts) => {
1902
+ try {
1903
+ printCommandArt("Run Connector", "streaming terminal session to portal");
1904
+ await runAgent(opts);
1905
+ } catch (error) {
1906
+ const message = error instanceof Error ? error.message : String(error);
1907
+ console.error(`[rAgent] Run failed: ${message}`);
1908
+ process.exit(1);
1909
+ }
1910
+ });
1911
+ }
1912
+
1913
+ // src/commands/doctor.ts
1914
+ var os7 = __toESM(require("os"));
1915
+ async function runDoctor(opts) {
1916
+ const options = resolveRunOptions(opts);
1917
+ const checks = [];
1918
+ const platformOk = os7.platform() === "linux";
1919
+ checks.push({ name: "platform", ok: platformOk, detail: os7.platform() });
1920
+ checks.push({
1921
+ name: "node",
1922
+ ok: Number(process.versions.node.split(".")[0]) >= 20,
1923
+ detail: process.versions.node
1924
+ });
1925
+ checks.push({ name: "portal", ok: Boolean(options.portal), detail: options.portal });
1926
+ let tmuxOk = false;
1927
+ try {
1928
+ await execAsync("tmux -V");
1929
+ tmuxOk = true;
1930
+ checks.push({ name: "tmux", ok: true, detail: "installed" });
1931
+ } catch (error) {
1932
+ const message = error instanceof Error ? error.message : String(error);
1933
+ checks.push({ name: "tmux", ok: false, detail: message });
1934
+ }
1935
+ try {
1936
+ const response = await fetch(`${options.portal}/api/bootstrap/linux.sh`);
1937
+ checks.push({
1938
+ name: "portal-reachability",
1939
+ ok: response.ok,
1940
+ detail: String(response.status)
1941
+ });
1942
+ } catch (error) {
1943
+ const message = error instanceof Error ? error.message : String(error);
1944
+ checks.push({ name: "portal-reachability", ok: false, detail: message });
1945
+ }
1946
+ checks.forEach((check) => {
1947
+ console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`);
1948
+ });
1949
+ if (!platformOk) {
1950
+ console.log("[rAgent] Linux is required for the connector.");
1951
+ }
1952
+ if (!tmuxOk) {
1953
+ const recipe = await chooseTmuxInstallCommand();
1954
+ if (recipe) {
1955
+ console.log(`[rAgent] tmux install suggestion: ${recipe.command}`);
1956
+ }
1957
+ if (opts.fix) {
1958
+ try {
1959
+ const installed = await installTmuxInteractively();
1960
+ if (installed) {
1961
+ console.log("[rAgent] tmux installation complete.");
1962
+ } else {
1963
+ console.log("[rAgent] tmux installation skipped or incomplete.");
1964
+ }
1965
+ } catch (error) {
1966
+ const message = error instanceof Error ? error.message : String(error);
1967
+ console.error(`[rAgent] Failed to install tmux: ${message}`);
1968
+ }
1969
+ } else {
1970
+ console.log(
1971
+ "[rAgent] Run `ragent doctor --fix` to install missing dependencies interactively."
1972
+ );
1973
+ }
1974
+ }
1975
+ }
1976
+ function registerDoctorCommand(program2) {
1977
+ program2.command("doctor").description("Run Linux environment and connectivity checks").option("--portal <url>", "Portal base URL").option("--fix", "Attempt interactive fixes for missing dependencies").action(async (opts) => {
1978
+ printCommandArt("Doctor", "checking local runtime + portal connectivity");
1979
+ await runDoctor(opts);
1980
+ });
1981
+ }
1982
+
1983
+ // src/commands/update.ts
1984
+ function registerUpdateCommand(program2) {
1985
+ program2.command("update").description("Update ragent CLI from npm").option("--check", "Check for updates only; do not install").action(async (opts) => {
1986
+ printCommandArt("Update", "checking npm registry for newer ragent-cli versions");
1987
+ const latestVersion = await checkForUpdate({ force: true });
1988
+ if (!latestVersion) {
1989
+ console.log(`[rAgent] You are up to date (${CURRENT_VERSION}).`);
1990
+ return;
1991
+ }
1992
+ console.log(`[rAgent] Update available: ${CURRENT_VERSION} -> ${latestVersion}`);
1993
+ if (opts.check) return;
1994
+ await execAsync(`npm install -g ${shellQuote(PACKAGE_NAME)}`, {
1995
+ timeout: 5 * 60 * 1e3,
1996
+ maxBuffer: 10 * 1024 * 1024
1997
+ });
1998
+ console.log(
1999
+ "[rAgent] Update command completed. Restart any running ragent service."
2000
+ );
2001
+ });
2002
+ }
2003
+
2004
+ // src/commands/sessions.ts
2005
+ async function listSessions() {
2006
+ const sessions = await collectTmuxSessions();
2007
+ if (sessions.length === 0) {
2008
+ console.log("No tmux sessions discovered.");
2009
+ return;
2010
+ }
2011
+ sessions.forEach((session) => {
2012
+ console.log(`${session.id} ${session.status} ${session.command || ""}`);
2013
+ });
2014
+ }
2015
+ function registerSessionsCommand(program2) {
2016
+ const sessions = program2.command("sessions").description("Session tooling");
2017
+ sessions.command("list").description("List tmux sessions/windows/panes on this host").action(async () => {
2018
+ await listSessions();
2019
+ });
2020
+ }
2021
+
2022
+ // src/commands/service.ts
2023
+ function registerServiceCommand(program2) {
2024
+ const service = program2.command("service").description("Manage background connector service");
2025
+ service.command("install").description("Install service (systemd user unit when available)").option("--start", "Start service immediately").option("--no-enable", "Do not enable autostart in systemd").action(async (opts) => {
2026
+ try {
2027
+ await ensureServiceInstalled({
2028
+ enable: opts.enable,
2029
+ start: Boolean(opts.start)
2030
+ });
2031
+ } catch (error) {
2032
+ const message = error instanceof Error ? error.message : String(error);
2033
+ console.error(`[rAgent] Service install failed: ${message}`);
2034
+ process.exit(1);
2035
+ }
2036
+ });
2037
+ service.command("start").description("Start installed service").action(async () => {
2038
+ try {
2039
+ await startService();
2040
+ } catch (error) {
2041
+ const message = error instanceof Error ? error.message : String(error);
2042
+ console.error(`[rAgent] Service start failed: ${message}`);
2043
+ process.exit(1);
2044
+ }
2045
+ });
2046
+ service.command("stop").description("Stop installed service").action(async () => {
2047
+ try {
2048
+ await stopService();
2049
+ } catch (error) {
2050
+ const message = error instanceof Error ? error.message : String(error);
2051
+ console.error(`[rAgent] Service stop failed: ${message}`);
2052
+ process.exit(1);
2053
+ }
2054
+ });
2055
+ service.command("restart").description("Restart installed service").action(async () => {
2056
+ try {
2057
+ await restartService();
2058
+ } catch (error) {
2059
+ const message = error instanceof Error ? error.message : String(error);
2060
+ console.error(`[rAgent] Service restart failed: ${message}`);
2061
+ process.exit(1);
2062
+ }
2063
+ });
2064
+ service.command("status").description("Show service status").action(async () => {
2065
+ try {
2066
+ await printServiceStatus();
2067
+ } catch (error) {
2068
+ const message = error instanceof Error ? error.message : String(error);
2069
+ console.error(`[rAgent] Service status failed: ${message}`);
2070
+ process.exit(1);
2071
+ }
2072
+ });
2073
+ service.command("logs").description("Show service logs").option("-n, --lines <lines>", "Number of log lines", "100").option("-f, --follow", "Follow logs").action(async (opts) => {
2074
+ try {
2075
+ await printServiceLogs(opts);
2076
+ } catch (error) {
2077
+ const message = error instanceof Error ? error.message : String(error);
2078
+ console.error(`[rAgent] Service logs failed: ${message}`);
2079
+ process.exit(1);
2080
+ }
2081
+ });
2082
+ service.command("uninstall").description("Remove installed service").action(async () => {
2083
+ try {
2084
+ await uninstallService();
2085
+ } catch (error) {
2086
+ const message = error instanceof Error ? error.message : String(error);
2087
+ console.error(`[rAgent] Service uninstall failed: ${message}`);
2088
+ process.exit(1);
2089
+ }
2090
+ });
2091
+ }
2092
+
2093
+ // src/commands/uninstall.ts
2094
+ var fs4 = __toESM(require("fs"));
2095
+ async function uninstallAgent(opts) {
2096
+ const config = loadConfig();
2097
+ const hostName = config.hostName || config.hostId || "this machine";
2098
+ if (!opts.yes) {
2099
+ const confirmed = await askYesNo(
2100
+ `[rAgent] This will deregister "${hostName}" from the server, stop the service, remove config, and unlink the CLI. Continue?`,
2101
+ false
2102
+ );
2103
+ if (!confirmed) {
2104
+ console.log("[rAgent] Uninstall cancelled.");
2105
+ return;
2106
+ }
2107
+ }
2108
+ if (config.portal && config.agentToken) {
2109
+ console.log("[rAgent] Deregistering from server...");
2110
+ try {
2111
+ const token = await refreshTokenIfNeeded({
2112
+ portal: config.portal,
2113
+ agentToken: config.agentToken
2114
+ });
2115
+ const ok = await deregisterHost({ portal: config.portal, agentToken: token });
2116
+ if (ok) {
2117
+ console.log("[rAgent] Host removed from server.");
2118
+ }
2119
+ } catch {
2120
+ console.warn("[rAgent] Could not reach server \u2014 host may need manual removal from dashboard.");
2121
+ }
2122
+ }
2123
+ console.log("[rAgent] Stopping and removing service...");
2124
+ await uninstallService().catch(() => void 0);
2125
+ if (fs4.existsSync(CONFIG_DIR)) {
2126
+ fs4.rmSync(CONFIG_DIR, { recursive: true, force: true });
2127
+ console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
2128
+ }
2129
+ try {
2130
+ await execAsync(`npm unlink -g ${PACKAGE_NAME}`, { timeout: 15e3 });
2131
+ console.log(`[rAgent] Unlinked global package: ${PACKAGE_NAME}`);
2132
+ } catch {
2133
+ }
2134
+ console.log("[rAgent] Uninstall complete. rAgent has been removed from this machine.");
2135
+ }
2136
+ function registerUninstallCommand(parent) {
2137
+ parent.command("uninstall").description("Remove rAgent from this machine (stops service, deletes config, unlinks CLI)").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
2138
+ try {
2139
+ printCommandArt("Uninstall", "removing rAgent from this machine");
2140
+ await uninstallAgent(opts);
2141
+ } catch (error) {
2142
+ const message = error instanceof Error ? error.message : String(error);
2143
+ console.error(`[rAgent] Uninstall failed: ${message}`);
2144
+ process.exit(1);
2145
+ }
2146
+ });
2147
+ }
2148
+
2149
+ // src/index.ts
2150
+ import_commander.program.name("ragent").description("Connect machines to rAgent Live").version(CURRENT_VERSION);
2151
+ registerConnectCommand(import_commander.program);
2152
+ registerRunCommand(import_commander.program);
2153
+ registerDoctorCommand(import_commander.program);
2154
+ registerUpdateCommand(import_commander.program);
2155
+ registerSessionsCommand(import_commander.program);
2156
+ registerServiceCommand(import_commander.program);
2157
+ registerUninstallCommand(import_commander.program);
2158
+ import_commander.program.hook("preAction", async (_thisCommand, actionCommand) => {
2159
+ if (actionCommand.name() === "update") return;
2160
+ await maybeWarnUpdate();
2161
+ });
2162
+ import_commander.program.action(() => {
2163
+ showStatus();
2164
+ });
2165
+ import_commander.program.parseAsync(process.argv);
2166
+ function showStatus() {
2167
+ console.log(`
2168
+ ragent v${CURRENT_VERSION}
2169
+ `);
2170
+ try {
2171
+ const raw = fs5.readFileSync(CONFIG_FILE, "utf8");
2172
+ const config = JSON.parse(raw);
2173
+ if (config.portal && config.agentToken) {
2174
+ console.log(` Status: Connected`);
2175
+ console.log(` Machine: ${config.hostName ?? config.hostId ?? "unknown"}`);
2176
+ console.log(` Portal: ${config.portal}`);
2177
+ console.log(` Config: ${CONFIG_FILE}`);
2178
+ if (config.updatedAt) {
2179
+ console.log(` Last: ${config.updatedAt}`);
2180
+ }
2181
+ console.log("");
2182
+ console.log(" Commands:");
2183
+ console.log(" ragent service status \u2014 Check if connector is running");
2184
+ console.log(" ragent service logs -f \u2014 Follow live logs");
2185
+ console.log(" ragent service restart \u2014 Restart the connector");
2186
+ console.log(" ragent sessions \u2014 List tmux sessions");
2187
+ console.log(" ragent doctor \u2014 Run diagnostics");
2188
+ console.log(" ragent --help \u2014 All available commands");
2189
+ console.log("");
2190
+ } else {
2191
+ showNotConnected();
2192
+ }
2193
+ } catch {
2194
+ showNotConnected();
2195
+ }
2196
+ }
2197
+ function showNotConnected() {
2198
+ console.log(" Status: Not connected\n");
2199
+ console.log(" To connect this machine:");
2200
+ console.log(" 1. Open the rAgent dashboard in your browser");
2201
+ console.log(" 2. Click '+ connect' to generate a connection token");
2202
+ console.log(" 3. Run: ragent connect --portal <url> --token <token>");
2203
+ console.log("");
2204
+ console.log(" Run `ragent --help` for all available commands.");
2205
+ console.log("");
2206
+ }