echoclaw-relay-agent 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +567 -9
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -203,6 +203,80 @@ async function forwardToBridge(bridgeUrl, request) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// src/service/session.ts
|
|
207
|
+
var import_fs = __toESM(require("fs"));
|
|
208
|
+
var import_path = __toESM(require("path"));
|
|
209
|
+
var import_crypto = require("crypto");
|
|
210
|
+
var CONFIG_DIR = import_path.default.join(
|
|
211
|
+
process.env.HOME || process.env.USERPROFILE || ".",
|
|
212
|
+
".echoclaw-relay"
|
|
213
|
+
);
|
|
214
|
+
var SESSION_FILE = import_path.default.join(CONFIG_DIR, "session.json");
|
|
215
|
+
function ensureDir() {
|
|
216
|
+
if (!import_fs.default.existsSync(CONFIG_DIR)) {
|
|
217
|
+
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function saveSession(sessionId2, sessionKey2, relayUrl, bridgeUrl, pairingCode2) {
|
|
221
|
+
ensureDir();
|
|
222
|
+
const rawKey = await import_crypto.webcrypto.subtle.exportKey("raw", sessionKey2);
|
|
223
|
+
const keyBase64 = Buffer.from(rawKey).toString("base64");
|
|
224
|
+
const data = {
|
|
225
|
+
sessionId: sessionId2,
|
|
226
|
+
sessionKeyBase64: keyBase64,
|
|
227
|
+
relayUrl,
|
|
228
|
+
bridgeUrl,
|
|
229
|
+
pairingCode: pairingCode2,
|
|
230
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
231
|
+
};
|
|
232
|
+
import_fs.default.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), {
|
|
233
|
+
mode: 384
|
|
234
|
+
// Only owner can read/write
|
|
235
|
+
});
|
|
236
|
+
console.log(`[session] Saved to ${SESSION_FILE}`);
|
|
237
|
+
}
|
|
238
|
+
async function loadSession() {
|
|
239
|
+
if (!import_fs.default.existsSync(SESSION_FILE)) return null;
|
|
240
|
+
try {
|
|
241
|
+
const raw = import_fs.default.readFileSync(SESSION_FILE, "utf-8");
|
|
242
|
+
const data = JSON.parse(raw);
|
|
243
|
+
if (!data.sessionId || !data.sessionKeyBase64) return null;
|
|
244
|
+
const keyBytes = Buffer.from(data.sessionKeyBase64, "base64");
|
|
245
|
+
const sessionKey2 = await import_crypto.webcrypto.subtle.importKey(
|
|
246
|
+
"raw",
|
|
247
|
+
keyBytes,
|
|
248
|
+
"AES-GCM",
|
|
249
|
+
true,
|
|
250
|
+
["encrypt", "decrypt"]
|
|
251
|
+
);
|
|
252
|
+
console.log(`[session] Loaded saved session (ID: ${data.sessionId})`);
|
|
253
|
+
return {
|
|
254
|
+
sessionId: data.sessionId,
|
|
255
|
+
sessionKey: sessionKey2,
|
|
256
|
+
relayUrl: data.relayUrl,
|
|
257
|
+
bridgeUrl: data.bridgeUrl,
|
|
258
|
+
pairingCode: data.pairingCode
|
|
259
|
+
};
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.warn(`[session] Failed to load session: ${err.message}`);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function clearSession() {
|
|
266
|
+
if (import_fs.default.existsSync(SESSION_FILE)) {
|
|
267
|
+
import_fs.default.unlinkSync(SESSION_FILE);
|
|
268
|
+
console.log("[session] Cleared saved session");
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function hasSession() {
|
|
274
|
+
return import_fs.default.existsSync(SESSION_FILE);
|
|
275
|
+
}
|
|
276
|
+
function getConfigDir() {
|
|
277
|
+
return CONFIG_DIR;
|
|
278
|
+
}
|
|
279
|
+
|
|
206
280
|
// src/relay/client.ts
|
|
207
281
|
var ws = null;
|
|
208
282
|
var keyPair = null;
|
|
@@ -212,16 +286,31 @@ var pairingCode = null;
|
|
|
212
286
|
var reconnect;
|
|
213
287
|
var config;
|
|
214
288
|
var _stopped = false;
|
|
289
|
+
var _heartbeatTimer = null;
|
|
290
|
+
var HEARTBEAT_INTERVAL_MS = 25e3;
|
|
291
|
+
var _onPaired = null;
|
|
292
|
+
function onPaired(cb) {
|
|
293
|
+
_onPaired = cb;
|
|
294
|
+
}
|
|
215
295
|
async function startRelayClient(cfg) {
|
|
216
296
|
config = cfg;
|
|
217
297
|
_stopped = false;
|
|
218
298
|
reconnect = createReconnectController();
|
|
299
|
+
if (cfg.resumeSessionKey && cfg.sessionId) {
|
|
300
|
+
sessionKey = cfg.resumeSessionKey;
|
|
301
|
+
sessionId = cfg.sessionId;
|
|
302
|
+
console.log(`[relay-agent] Resuming session ${sessionId}`);
|
|
303
|
+
}
|
|
219
304
|
keyPair = await generateKeyPair();
|
|
220
305
|
console.log("[relay-agent] X25519 key pair generated");
|
|
221
306
|
connect();
|
|
222
307
|
}
|
|
223
308
|
function stopRelayClient() {
|
|
224
309
|
_stopped = true;
|
|
310
|
+
if (_heartbeatTimer) {
|
|
311
|
+
clearInterval(_heartbeatTimer);
|
|
312
|
+
_heartbeatTimer = null;
|
|
313
|
+
}
|
|
225
314
|
if (ws) {
|
|
226
315
|
ws.close(1e3, "agent shutdown");
|
|
227
316
|
ws = null;
|
|
@@ -230,14 +319,30 @@ function stopRelayClient() {
|
|
|
230
319
|
keyPair = null;
|
|
231
320
|
reconnect?.reset();
|
|
232
321
|
}
|
|
322
|
+
function startHeartbeat() {
|
|
323
|
+
if (_heartbeatTimer) clearInterval(_heartbeatTimer);
|
|
324
|
+
_heartbeatTimer = setInterval(() => {
|
|
325
|
+
if (ws?.readyState === import_ws.default.OPEN) {
|
|
326
|
+
sendRaw({ version: 1, session_id: sessionId ?? "", msg_id: makeId(), type: "PING", sender_role: "agent" });
|
|
327
|
+
}
|
|
328
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
329
|
+
}
|
|
330
|
+
function stopHeartbeat() {
|
|
331
|
+
if (_heartbeatTimer) {
|
|
332
|
+
clearInterval(_heartbeatTimer);
|
|
333
|
+
_heartbeatTimer = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
233
336
|
function connect() {
|
|
234
337
|
if (_stopped) return;
|
|
235
338
|
reconnect.setState("connecting");
|
|
236
|
-
const
|
|
339
|
+
const resumeId = sessionId || config.sessionId;
|
|
340
|
+
const url = resumeId ? `${config.relayUrl}${config.relayUrl.includes("?") ? "&" : "?"}resume=${resumeId}` : config.relayUrl;
|
|
237
341
|
console.log(`[relay-agent] Connecting to ${url}...`);
|
|
238
342
|
ws = new import_ws.default(url);
|
|
239
343
|
ws.on("open", () => {
|
|
240
344
|
reconnect.setState("connected");
|
|
345
|
+
startHeartbeat();
|
|
241
346
|
console.log("[relay-agent] Connected to relay server");
|
|
242
347
|
const registerMsg = {
|
|
243
348
|
version: 1,
|
|
@@ -255,7 +360,7 @@ function connect() {
|
|
|
255
360
|
});
|
|
256
361
|
ws.on("close", (code, reason) => {
|
|
257
362
|
console.log(`[relay-agent] Disconnected (code=${code}, reason=${reason.toString()})`);
|
|
258
|
-
|
|
363
|
+
stopHeartbeat();
|
|
259
364
|
scheduleReconnect();
|
|
260
365
|
});
|
|
261
366
|
ws.on("error", (err) => {
|
|
@@ -327,6 +432,24 @@ async function handleHello(msg) {
|
|
|
327
432
|
});
|
|
328
433
|
reconnect.setState("paired");
|
|
329
434
|
console.log("[relay-agent] \u2705 E2E encryption established \u2014 session paired");
|
|
435
|
+
try {
|
|
436
|
+
await saveSession(
|
|
437
|
+
sessionId,
|
|
438
|
+
sessionKey,
|
|
439
|
+
config.relayUrl,
|
|
440
|
+
config.bridgeUrl,
|
|
441
|
+
pairingCode ?? void 0
|
|
442
|
+
);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.warn("[relay-agent] Failed to persist session:", err.message);
|
|
445
|
+
}
|
|
446
|
+
if (_onPaired) {
|
|
447
|
+
try {
|
|
448
|
+
_onPaired();
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
_onPaired = null;
|
|
452
|
+
}
|
|
330
453
|
}
|
|
331
454
|
}
|
|
332
455
|
async function handleData(msg) {
|
|
@@ -375,7 +498,357 @@ function sendRaw(msg) {
|
|
|
375
498
|
}
|
|
376
499
|
}
|
|
377
500
|
|
|
501
|
+
// src/service/installer.ts
|
|
502
|
+
var import_fs2 = __toESM(require("fs"));
|
|
503
|
+
var import_path2 = __toESM(require("path"));
|
|
504
|
+
var import_child_process = require("child_process");
|
|
505
|
+
var SERVICE_NAME = "me.echoclaw.relay-agent";
|
|
506
|
+
var LABEL = "EchoClaw Relay Agent";
|
|
507
|
+
function resolveNodeBin() {
|
|
508
|
+
const scriptPath = process.argv[1];
|
|
509
|
+
try {
|
|
510
|
+
const globalBin = (0, import_child_process.execSync)("npm root -g", { encoding: "utf-8" }).trim();
|
|
511
|
+
const globalAgent = import_path2.default.join(import_path2.default.dirname(globalBin), "bin", "echoclaw-relay-agent");
|
|
512
|
+
if (import_fs2.default.existsSync(globalAgent)) {
|
|
513
|
+
return globalAgent;
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
if (scriptPath && import_fs2.default.existsSync(scriptPath)) {
|
|
518
|
+
return scriptPath;
|
|
519
|
+
}
|
|
520
|
+
throw new Error(
|
|
521
|
+
"Cannot determine the installed path of echoclaw-relay-agent.\nPlease install globally first:\n\n npm install -g echoclaw-relay-agent\n\nThen run: echoclaw-relay-agent --install"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
function resolveNodePath() {
|
|
525
|
+
return process.execPath;
|
|
526
|
+
}
|
|
527
|
+
function macosInstall() {
|
|
528
|
+
const agentBin = resolveNodeBin();
|
|
529
|
+
const nodePath = resolveNodePath();
|
|
530
|
+
const logDir = getConfigDir();
|
|
531
|
+
const plistDir = import_path2.default.join(process.env.HOME, "Library", "LaunchAgents");
|
|
532
|
+
const plistPath = import_path2.default.join(plistDir, `${SERVICE_NAME}.plist`);
|
|
533
|
+
if (!import_fs2.default.existsSync(plistDir)) {
|
|
534
|
+
import_fs2.default.mkdirSync(plistDir, { recursive: true });
|
|
535
|
+
}
|
|
536
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
537
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
538
|
+
<plist version="1.0">
|
|
539
|
+
<dict>
|
|
540
|
+
<key>Label</key>
|
|
541
|
+
<string>${SERVICE_NAME}</string>
|
|
542
|
+
|
|
543
|
+
<key>ProgramArguments</key>
|
|
544
|
+
<array>
|
|
545
|
+
<string>${nodePath}</string>
|
|
546
|
+
<string>${agentBin}</string>
|
|
547
|
+
</array>
|
|
548
|
+
|
|
549
|
+
<key>RunAtLoad</key>
|
|
550
|
+
<true/>
|
|
551
|
+
|
|
552
|
+
<key>KeepAlive</key>
|
|
553
|
+
<true/>
|
|
554
|
+
|
|
555
|
+
<key>ThrottleInterval</key>
|
|
556
|
+
<integer>10</integer>
|
|
557
|
+
|
|
558
|
+
<key>StandardOutPath</key>
|
|
559
|
+
<string>${logDir}/stdout.log</string>
|
|
560
|
+
|
|
561
|
+
<key>StandardErrorPath</key>
|
|
562
|
+
<string>${logDir}/stderr.log</string>
|
|
563
|
+
|
|
564
|
+
<key>EnvironmentVariables</key>
|
|
565
|
+
<dict>
|
|
566
|
+
<key>PATH</key>
|
|
567
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
568
|
+
</dict>
|
|
569
|
+
</dict>
|
|
570
|
+
</plist>`;
|
|
571
|
+
import_fs2.default.writeFileSync(plistPath, plist, { mode: 420 });
|
|
572
|
+
try {
|
|
573
|
+
(0, import_child_process.execSync)(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
574
|
+
(0, import_child_process.execSync)(`launchctl load "${plistPath}"`);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.error(`[install] Failed to load LaunchAgent: ${err.message}`);
|
|
577
|
+
console.log(`[install] Plist written to ${plistPath} \u2014 you can load it manually:`);
|
|
578
|
+
console.log(` launchctl load "${plistPath}"`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log(" \u2705 Installed as macOS LaunchAgent");
|
|
583
|
+
console.log(` Plist: ${plistPath}`);
|
|
584
|
+
console.log(` Logs: ${logDir}/stdout.log`);
|
|
585
|
+
console.log("");
|
|
586
|
+
console.log(" The relay agent will now start automatically on login.");
|
|
587
|
+
console.log(" To uninstall: echoclaw-relay-agent --uninstall");
|
|
588
|
+
console.log("");
|
|
589
|
+
}
|
|
590
|
+
function macosUninstall() {
|
|
591
|
+
const plistPath = import_path2.default.join(
|
|
592
|
+
process.env.HOME,
|
|
593
|
+
"Library",
|
|
594
|
+
"LaunchAgents",
|
|
595
|
+
`${SERVICE_NAME}.plist`
|
|
596
|
+
);
|
|
597
|
+
if (!import_fs2.default.existsSync(plistPath)) {
|
|
598
|
+
console.log("[uninstall] No LaunchAgent found \u2014 nothing to remove.");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
(0, import_child_process.execSync)(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
import_fs2.default.unlinkSync(plistPath);
|
|
606
|
+
console.log("");
|
|
607
|
+
console.log(" \u2705 Uninstalled macOS LaunchAgent");
|
|
608
|
+
console.log(` Removed: ${plistPath}`);
|
|
609
|
+
console.log("");
|
|
610
|
+
}
|
|
611
|
+
function macosStatus() {
|
|
612
|
+
const plistPath = import_path2.default.join(
|
|
613
|
+
process.env.HOME,
|
|
614
|
+
"Library",
|
|
615
|
+
"LaunchAgents",
|
|
616
|
+
`${SERVICE_NAME}.plist`
|
|
617
|
+
);
|
|
618
|
+
if (!import_fs2.default.existsSync(plistPath)) {
|
|
619
|
+
console.log(" Service: not installed");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const output = (0, import_child_process.execSync)(`launchctl list "${SERVICE_NAME}" 2>&1`, { encoding: "utf-8" });
|
|
624
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
|
|
625
|
+
const pid = pidMatch ? pidMatch[1] : null;
|
|
626
|
+
console.log(` Service: installed (${pid ? `running, PID ${pid}` : "not running"})`);
|
|
627
|
+
console.log(` Plist: ${plistPath}`);
|
|
628
|
+
} catch {
|
|
629
|
+
console.log(" Service: installed (not currently loaded)");
|
|
630
|
+
console.log(` Plist: ${plistPath}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function linuxInstall() {
|
|
634
|
+
const agentBin = resolveNodeBin();
|
|
635
|
+
const nodePath = resolveNodePath();
|
|
636
|
+
const logDir = getConfigDir();
|
|
637
|
+
const serviceDir = import_path2.default.join(
|
|
638
|
+
process.env.HOME,
|
|
639
|
+
".config",
|
|
640
|
+
"systemd",
|
|
641
|
+
"user"
|
|
642
|
+
);
|
|
643
|
+
const servicePath = import_path2.default.join(serviceDir, `${SERVICE_NAME}.service`);
|
|
644
|
+
if (!import_fs2.default.existsSync(serviceDir)) {
|
|
645
|
+
import_fs2.default.mkdirSync(serviceDir, { recursive: true });
|
|
646
|
+
}
|
|
647
|
+
const unit = `[Unit]
|
|
648
|
+
Description=${LABEL}
|
|
649
|
+
After=network-online.target
|
|
650
|
+
Wants=network-online.target
|
|
651
|
+
|
|
652
|
+
[Service]
|
|
653
|
+
Type=simple
|
|
654
|
+
ExecStart=${nodePath} ${agentBin}
|
|
655
|
+
Restart=always
|
|
656
|
+
RestartSec=10
|
|
657
|
+
StandardOutput=append:${logDir}/stdout.log
|
|
658
|
+
StandardError=append:${logDir}/stderr.log
|
|
659
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
660
|
+
|
|
661
|
+
[Install]
|
|
662
|
+
WantedBy=default.target
|
|
663
|
+
`;
|
|
664
|
+
import_fs2.default.writeFileSync(servicePath, unit, { mode: 420 });
|
|
665
|
+
try {
|
|
666
|
+
(0, import_child_process.execSync)("systemctl --user daemon-reload");
|
|
667
|
+
(0, import_child_process.execSync)(`systemctl --user enable ${SERVICE_NAME}.service`);
|
|
668
|
+
(0, import_child_process.execSync)(`systemctl --user start ${SERVICE_NAME}.service`);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
console.error(`[install] Failed to start service: ${err.message}`);
|
|
671
|
+
console.log(`[install] Service file written to ${servicePath}`);
|
|
672
|
+
console.log(" Enable manually:");
|
|
673
|
+
console.log(` systemctl --user enable --now ${SERVICE_NAME}.service`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
(0, import_child_process.execSync)(`loginctl enable-linger ${process.env.USER}`);
|
|
678
|
+
console.log(" Linger enabled \u2014 service will run even without login.");
|
|
679
|
+
} catch {
|
|
680
|
+
console.log(" \u26A0\uFE0F Could not enable linger \u2014 service may stop on logout.");
|
|
681
|
+
console.log(` Run: sudo loginctl enable-linger ${process.env.USER}`);
|
|
682
|
+
}
|
|
683
|
+
console.log("");
|
|
684
|
+
console.log(" \u2705 Installed as systemd user service");
|
|
685
|
+
console.log(` Unit: ${servicePath}`);
|
|
686
|
+
console.log(` Logs: ${logDir}/stdout.log`);
|
|
687
|
+
console.log("");
|
|
688
|
+
console.log(" The relay agent will now start automatically on boot.");
|
|
689
|
+
console.log(" To uninstall: echoclaw-relay-agent --uninstall");
|
|
690
|
+
console.log("");
|
|
691
|
+
}
|
|
692
|
+
function linuxUninstall() {
|
|
693
|
+
const servicePath = import_path2.default.join(
|
|
694
|
+
process.env.HOME,
|
|
695
|
+
".config",
|
|
696
|
+
"systemd",
|
|
697
|
+
"user",
|
|
698
|
+
`${SERVICE_NAME}.service`
|
|
699
|
+
);
|
|
700
|
+
if (!import_fs2.default.existsSync(servicePath)) {
|
|
701
|
+
console.log("[uninstall] No systemd service found \u2014 nothing to remove.");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
(0, import_child_process.execSync)(`systemctl --user stop ${SERVICE_NAME}.service 2>/dev/null || true`);
|
|
706
|
+
(0, import_child_process.execSync)(`systemctl --user disable ${SERVICE_NAME}.service 2>/dev/null || true`);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
import_fs2.default.unlinkSync(servicePath);
|
|
710
|
+
try {
|
|
711
|
+
(0, import_child_process.execSync)("systemctl --user daemon-reload");
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
console.log("");
|
|
715
|
+
console.log(" \u2705 Uninstalled systemd user service");
|
|
716
|
+
console.log(` Removed: ${servicePath}`);
|
|
717
|
+
console.log("");
|
|
718
|
+
}
|
|
719
|
+
function linuxStatus() {
|
|
720
|
+
const servicePath = import_path2.default.join(
|
|
721
|
+
process.env.HOME,
|
|
722
|
+
".config",
|
|
723
|
+
"systemd",
|
|
724
|
+
"user",
|
|
725
|
+
`${SERVICE_NAME}.service`
|
|
726
|
+
);
|
|
727
|
+
if (!import_fs2.default.existsSync(servicePath)) {
|
|
728
|
+
console.log(" Service: not installed");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const output = (0, import_child_process.execSync)(
|
|
733
|
+
`systemctl --user is-active ${SERVICE_NAME}.service 2>&1`,
|
|
734
|
+
{ encoding: "utf-8" }
|
|
735
|
+
).trim();
|
|
736
|
+
console.log(` Service: installed (${output})`);
|
|
737
|
+
console.log(` Unit: ${servicePath}`);
|
|
738
|
+
} catch {
|
|
739
|
+
console.log(" Service: installed (inactive)");
|
|
740
|
+
console.log(` Unit: ${servicePath}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function windowsInstall() {
|
|
744
|
+
const agentBin = resolveNodeBin();
|
|
745
|
+
const nodePath = resolveNodePath();
|
|
746
|
+
try {
|
|
747
|
+
(0, import_child_process.execSync)(
|
|
748
|
+
`schtasks /Delete /TN "${SERVICE_NAME}" /F 2>nul`,
|
|
749
|
+
{ encoding: "utf-8", stdio: "pipe" }
|
|
750
|
+
);
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
(0, import_child_process.execSync)(
|
|
755
|
+
`schtasks /Create /TN "${SERVICE_NAME}" /TR "\\"${nodePath}\\" \\"${agentBin}\\"" /SC ONLOGON /RL HIGHEST /F`,
|
|
756
|
+
{ encoding: "utf-8" }
|
|
757
|
+
);
|
|
758
|
+
(0, import_child_process.execSync)(`schtasks /Run /TN "${SERVICE_NAME}"`, { encoding: "utf-8" });
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.error(`[install] Failed to create scheduled task: ${err.message}`);
|
|
761
|
+
console.log(" You may need to run this command as Administrator.");
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
console.log("");
|
|
765
|
+
console.log(" \u2705 Installed as Windows Scheduled Task");
|
|
766
|
+
console.log(` Task: ${SERVICE_NAME}`);
|
|
767
|
+
console.log("");
|
|
768
|
+
console.log(" The relay agent will now start automatically on login.");
|
|
769
|
+
console.log(" To uninstall: echoclaw-relay-agent --uninstall");
|
|
770
|
+
console.log("");
|
|
771
|
+
}
|
|
772
|
+
function windowsUninstall() {
|
|
773
|
+
try {
|
|
774
|
+
(0, import_child_process.execSync)(`schtasks /Delete /TN "${SERVICE_NAME}" /F`, { encoding: "utf-8" });
|
|
775
|
+
console.log("");
|
|
776
|
+
console.log(" \u2705 Uninstalled Windows Scheduled Task");
|
|
777
|
+
console.log("");
|
|
778
|
+
} catch {
|
|
779
|
+
console.log("[uninstall] No scheduled task found \u2014 nothing to remove.");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function windowsStatus() {
|
|
783
|
+
try {
|
|
784
|
+
const output = (0, import_child_process.execSync)(
|
|
785
|
+
`schtasks /Query /TN "${SERVICE_NAME}" /FO LIST 2>nul`,
|
|
786
|
+
{ encoding: "utf-8" }
|
|
787
|
+
);
|
|
788
|
+
const statusMatch = output.match(/Status:\s*(.+)/);
|
|
789
|
+
console.log(` Service: installed (${statusMatch ? statusMatch[1].trim() : "unknown"})`);
|
|
790
|
+
} catch {
|
|
791
|
+
console.log(" Service: not installed");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function getPlatform() {
|
|
795
|
+
switch (process.platform) {
|
|
796
|
+
case "darwin":
|
|
797
|
+
return "macos";
|
|
798
|
+
case "linux":
|
|
799
|
+
return "linux";
|
|
800
|
+
case "win32":
|
|
801
|
+
return "windows";
|
|
802
|
+
default:
|
|
803
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function installService() {
|
|
807
|
+
const platform = getPlatform();
|
|
808
|
+
console.log(`[install] Platform: ${platform}`);
|
|
809
|
+
switch (platform) {
|
|
810
|
+
case "macos":
|
|
811
|
+
macosInstall();
|
|
812
|
+
break;
|
|
813
|
+
case "linux":
|
|
814
|
+
linuxInstall();
|
|
815
|
+
break;
|
|
816
|
+
case "windows":
|
|
817
|
+
windowsInstall();
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function uninstallService() {
|
|
822
|
+
const platform = getPlatform();
|
|
823
|
+
switch (platform) {
|
|
824
|
+
case "macos":
|
|
825
|
+
macosUninstall();
|
|
826
|
+
break;
|
|
827
|
+
case "linux":
|
|
828
|
+
linuxUninstall();
|
|
829
|
+
break;
|
|
830
|
+
case "windows":
|
|
831
|
+
windowsUninstall();
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function serviceStatus() {
|
|
836
|
+
const platform = getPlatform();
|
|
837
|
+
switch (platform) {
|
|
838
|
+
case "macos":
|
|
839
|
+
macosStatus();
|
|
840
|
+
break;
|
|
841
|
+
case "linux":
|
|
842
|
+
linuxStatus();
|
|
843
|
+
break;
|
|
844
|
+
case "windows":
|
|
845
|
+
windowsStatus();
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
378
850
|
// src/main.ts
|
|
851
|
+
var VERSION = "0.2.0";
|
|
379
852
|
function parseArgs() {
|
|
380
853
|
const args = process.argv.slice(2);
|
|
381
854
|
const opts = {
|
|
@@ -401,13 +874,22 @@ function parseArgs() {
|
|
|
401
874
|
case "-b":
|
|
402
875
|
opts.bridgeUrl = args[++i] || opts.bridgeUrl;
|
|
403
876
|
break;
|
|
877
|
+
case "--install":
|
|
878
|
+
opts.command = "install";
|
|
879
|
+
break;
|
|
880
|
+
case "--uninstall":
|
|
881
|
+
opts.command = "uninstall";
|
|
882
|
+
break;
|
|
883
|
+
case "--status":
|
|
884
|
+
opts.command = "status";
|
|
885
|
+
break;
|
|
404
886
|
case "--help":
|
|
405
887
|
case "-h":
|
|
406
888
|
printHelp();
|
|
407
889
|
process.exit(0);
|
|
408
890
|
case "--version":
|
|
409
891
|
case "-v":
|
|
410
|
-
console.log(
|
|
892
|
+
console.log(`echoclaw-relay-agent ${VERSION}`);
|
|
411
893
|
process.exit(0);
|
|
412
894
|
default:
|
|
413
895
|
if (arg.startsWith("-")) {
|
|
@@ -428,7 +910,7 @@ function printHelp() {
|
|
|
428
910
|
|
|
429
911
|
OPTIONS
|
|
430
912
|
--code, -c <code> Join an existing session by pairing code (e.g. ABC-1234).
|
|
431
|
-
If omitted, a
|
|
913
|
+
If omitted, resumes a saved session or creates a new one.
|
|
432
914
|
|
|
433
915
|
--relay, -r <url> Relay server WebSocket URL
|
|
434
916
|
(default: wss://relay.echoclaw.me/agent/connect)
|
|
@@ -436,24 +918,74 @@ function printHelp() {
|
|
|
436
918
|
--bridge, -b <url> Local OpenClaw bridge HTTP URL
|
|
437
919
|
(default: http://localhost:8013)
|
|
438
920
|
|
|
921
|
+
--install Install as system service (auto-start on boot)
|
|
922
|
+
--uninstall Remove system service and saved session
|
|
923
|
+
--status Show service and connection status
|
|
924
|
+
|
|
439
925
|
--help, -h Show this help message
|
|
440
926
|
--version, -v Show version
|
|
441
927
|
|
|
442
928
|
EXAMPLES
|
|
443
|
-
echoclaw-relay-agent
|
|
444
|
-
echoclaw-relay-agent --code ABC-1234
|
|
445
|
-
echoclaw-relay-agent --
|
|
929
|
+
echoclaw-relay-agent --code ABC-1234 # Pair and connect
|
|
930
|
+
echoclaw-relay-agent --code ABC-1234 --install # Pair, connect, and install as service
|
|
931
|
+
echoclaw-relay-agent --status # Check status
|
|
932
|
+
echoclaw-relay-agent --uninstall # Remove service
|
|
446
933
|
|
|
447
934
|
SECURITY
|
|
448
935
|
All communication is end-to-end encrypted (X25519 + AES-256-GCM).
|
|
449
936
|
The relay server cannot read your messages \u2014 it only forwards ciphertext.
|
|
450
937
|
`);
|
|
451
938
|
}
|
|
939
|
+
function handleInstall() {
|
|
940
|
+
console.log("");
|
|
941
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
942
|
+
console.log(` \u2502 EchoClaw Relay Agent v${VERSION} \u2502`);
|
|
943
|
+
console.log(" \u2502 Installing system service... \u2502");
|
|
944
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
945
|
+
console.log("");
|
|
946
|
+
if (!hasSession()) {
|
|
947
|
+
console.error(" \u274C No saved session found.");
|
|
948
|
+
console.error(" You must pair first before installing as a service:");
|
|
949
|
+
console.error("");
|
|
950
|
+
console.error(" echoclaw-relay-agent --code ABC-1234 --install");
|
|
951
|
+
console.error("");
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
installService();
|
|
955
|
+
}
|
|
956
|
+
function handleUninstall() {
|
|
957
|
+
uninstallService();
|
|
958
|
+
clearSession();
|
|
959
|
+
console.log(" Session data cleared.");
|
|
960
|
+
}
|
|
961
|
+
function handleStatus() {
|
|
962
|
+
console.log("");
|
|
963
|
+
console.log(` EchoClaw Relay Agent v${VERSION}`);
|
|
964
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
965
|
+
serviceStatus();
|
|
966
|
+
console.log(` Session: ${hasSession() ? "saved (will resume on start)" : "none"}`);
|
|
967
|
+
console.log("");
|
|
968
|
+
}
|
|
452
969
|
async function main() {
|
|
453
970
|
const opts = parseArgs();
|
|
971
|
+
if (opts.command === "install" && !opts.code) {
|
|
972
|
+
handleInstall();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (opts.command === "uninstall") {
|
|
976
|
+
handleUninstall();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (opts.command === "status") {
|
|
980
|
+
handleStatus();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (opts.command === "install" && opts.code) {
|
|
984
|
+
opts.installAfterPairing = true;
|
|
985
|
+
}
|
|
454
986
|
console.log("");
|
|
455
987
|
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
456
|
-
console.log(
|
|
988
|
+
console.log(` \u2502 EchoClaw Relay Agent v${VERSION} \u2502`);
|
|
457
989
|
console.log(" \u2502 Open Source \xB7 Apache License 2.0 \u2502");
|
|
458
990
|
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
459
991
|
console.log("");
|
|
@@ -481,9 +1013,35 @@ async function main() {
|
|
|
481
1013
|
const separator = relayUrl.includes("?") ? "&" : "?";
|
|
482
1014
|
relayUrl = `${relayUrl}${separator}code=${opts.code}`;
|
|
483
1015
|
}
|
|
1016
|
+
let resumeSessionKey;
|
|
1017
|
+
let resumeSessionId;
|
|
1018
|
+
if (!opts.code) {
|
|
1019
|
+
const saved = await loadSession();
|
|
1020
|
+
if (saved) {
|
|
1021
|
+
console.log(` \u{1F4C2} Found saved session: ${saved.sessionId}`);
|
|
1022
|
+
console.log(" Attempting to resume...");
|
|
1023
|
+
console.log("");
|
|
1024
|
+
resumeSessionKey = saved.sessionKey;
|
|
1025
|
+
resumeSessionId = saved.sessionId;
|
|
1026
|
+
relayUrl = opts.relayUrl;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (opts.installAfterPairing) {
|
|
1030
|
+
onPaired(() => {
|
|
1031
|
+
console.log("");
|
|
1032
|
+
console.log("[relay-agent] Pairing successful \u2014 installing system service...");
|
|
1033
|
+
try {
|
|
1034
|
+
installService();
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
console.error("[relay-agent] Service install failed:", err.message);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
484
1040
|
await startRelayClient({
|
|
485
1041
|
relayUrl,
|
|
486
|
-
bridgeUrl: opts.bridgeUrl
|
|
1042
|
+
bridgeUrl: opts.bridgeUrl,
|
|
1043
|
+
sessionId: resumeSessionId,
|
|
1044
|
+
resumeSessionKey
|
|
487
1045
|
});
|
|
488
1046
|
}
|
|
489
1047
|
process.on("SIGINT", () => {
|