echoclaw-relay-agent 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +188 -80
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -62,7 +62,7 @@ async function computeSharedSecret(myPrivateKey, theirPublicKeyRaw) {
|
|
|
62
62
|
var subtle2 = globalThis.crypto.subtle;
|
|
63
63
|
var PROTOCOL_SALT = new TextEncoder().encode("echoclaw-relay-v1");
|
|
64
64
|
var INFO_PREFIX = "echoclaw-e2e-";
|
|
65
|
-
async function deriveSessionKey(sharedSecret, pairingCode2) {
|
|
65
|
+
async function deriveSessionKey(sharedSecret, pairingCode2, extractable = false) {
|
|
66
66
|
const hkdfKey = await subtle2.importKey(
|
|
67
67
|
"raw",
|
|
68
68
|
sharedSecret,
|
|
@@ -82,8 +82,7 @@ async function deriveSessionKey(sharedSecret, pairingCode2) {
|
|
|
82
82
|
},
|
|
83
83
|
hkdfKey,
|
|
84
84
|
{ name: "AES-GCM", length: 256 },
|
|
85
|
-
|
|
86
|
-
// non-extractable
|
|
85
|
+
extractable,
|
|
87
86
|
["encrypt", "decrypt"]
|
|
88
87
|
);
|
|
89
88
|
}
|
|
@@ -113,10 +112,6 @@ async function decrypt(key, iv, ciphertext) {
|
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
// ../../packages/crypto/src/index.ts
|
|
116
|
-
async function completeHandshake(myPrivateKey, theirPublicKey, pairingCode2) {
|
|
117
|
-
const shared = await computeSharedSecret(myPrivateKey, theirPublicKey);
|
|
118
|
-
return deriveSessionKey(shared, pairingCode2);
|
|
119
|
-
}
|
|
120
115
|
function toBase64(bytes) {
|
|
121
116
|
let binary = "";
|
|
122
117
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -206,7 +201,6 @@ async function forwardToBridge(bridgeUrl, request) {
|
|
|
206
201
|
// src/service/session.ts
|
|
207
202
|
var import_fs = __toESM(require("fs"));
|
|
208
203
|
var import_path = __toESM(require("path"));
|
|
209
|
-
var import_crypto = require("crypto");
|
|
210
204
|
var CONFIG_DIR = import_path.default.join(
|
|
211
205
|
process.env.HOME || process.env.USERPROFILE || ".",
|
|
212
206
|
".echoclaw-relay"
|
|
@@ -217,13 +211,13 @@ function ensureDir() {
|
|
|
217
211
|
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
218
212
|
}
|
|
219
213
|
}
|
|
220
|
-
async function saveSession(sessionId2,
|
|
214
|
+
async function saveSession(sessionId2, sharedSecret, relayUrl, bridgeUrl, pairingCode2) {
|
|
221
215
|
ensureDir();
|
|
222
|
-
const rawKey = await import_crypto.webcrypto.subtle.exportKey("raw", sessionKey2);
|
|
223
|
-
const keyBase64 = Buffer.from(rawKey).toString("base64");
|
|
224
216
|
const data = {
|
|
225
217
|
sessionId: sessionId2,
|
|
226
|
-
sessionKeyBase64:
|
|
218
|
+
sessionKeyBase64: "",
|
|
219
|
+
// unused, kept for backwards compat
|
|
220
|
+
sharedSecretBase64: Buffer.from(sharedSecret).toString("base64"),
|
|
227
221
|
relayUrl,
|
|
228
222
|
bridgeUrl,
|
|
229
223
|
pairingCode: pairingCode2,
|
|
@@ -240,15 +234,9 @@ async function loadSession() {
|
|
|
240
234
|
try {
|
|
241
235
|
const raw = import_fs.default.readFileSync(SESSION_FILE, "utf-8");
|
|
242
236
|
const data = JSON.parse(raw);
|
|
243
|
-
if (!data.sessionId || !data.
|
|
244
|
-
const
|
|
245
|
-
const sessionKey2 = await
|
|
246
|
-
"raw",
|
|
247
|
-
keyBytes,
|
|
248
|
-
"AES-GCM",
|
|
249
|
-
true,
|
|
250
|
-
["encrypt", "decrypt"]
|
|
251
|
-
);
|
|
237
|
+
if (!data.sessionId || !data.sharedSecretBase64) return null;
|
|
238
|
+
const sharedSecret = new Uint8Array(Buffer.from(data.sharedSecretBase64, "base64"));
|
|
239
|
+
const sessionKey2 = await deriveSessionKey(sharedSecret, data.pairingCode);
|
|
252
240
|
console.log(`[session] Loaded saved session (ID: ${data.sessionId})`);
|
|
253
241
|
return {
|
|
254
242
|
sessionId: data.sessionId,
|
|
@@ -338,12 +326,10 @@ function connect() {
|
|
|
338
326
|
reconnect.setState("connecting");
|
|
339
327
|
const resumeId = sessionId || config.sessionId;
|
|
340
328
|
const url = resumeId ? `${config.relayUrl}${config.relayUrl.includes("?") ? "&" : "?"}resume=${resumeId}` : config.relayUrl;
|
|
341
|
-
console.log(`[relay-agent] Connecting to ${url}...`);
|
|
342
329
|
ws = new import_ws.default(url);
|
|
343
330
|
ws.on("open", () => {
|
|
344
331
|
reconnect.setState("connected");
|
|
345
332
|
startHeartbeat();
|
|
346
|
-
console.log("[relay-agent] Connected to relay server");
|
|
347
333
|
const registerMsg = {
|
|
348
334
|
version: 1,
|
|
349
335
|
session_id: sessionId ?? "",
|
|
@@ -371,7 +357,7 @@ function scheduleReconnect() {
|
|
|
371
357
|
if (_stopped) return;
|
|
372
358
|
reconnect.setState("disconnected");
|
|
373
359
|
const delay = reconnect.nextDelay();
|
|
374
|
-
console.log(`[relay-agent] Reconnecting in ${delay}
|
|
360
|
+
console.log(`[relay-agent] Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${reconnect.attempt})`);
|
|
375
361
|
setTimeout(connect, delay);
|
|
376
362
|
}
|
|
377
363
|
async function handleMessage(raw) {
|
|
@@ -405,22 +391,22 @@ async function handleHello(msg) {
|
|
|
405
391
|
if (msg.payload) {
|
|
406
392
|
pairingCode = msg.payload;
|
|
407
393
|
console.log("");
|
|
408
|
-
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
409
|
-
console.log(
|
|
410
|
-
console.log(
|
|
394
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
395
|
+
console.log(" \u2551 \u2551");
|
|
396
|
+
console.log(` \u2551 Pairing Code: ${pairingCode.padEnd(28)}\u2551`);
|
|
397
|
+
console.log(" \u2551 \u2551");
|
|
398
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
411
399
|
console.log("");
|
|
412
|
-
console.log(" Enter this code in your EchoClaw desktop
|
|
400
|
+
console.log(" Enter this code in your EchoClaw desktop app.");
|
|
401
|
+
console.log(" Waiting for desktop to connect...");
|
|
413
402
|
console.log("");
|
|
414
403
|
}
|
|
415
404
|
}
|
|
416
405
|
if (msg.pubkey && msg.sender_role === "desktop") {
|
|
417
406
|
console.log("[relay-agent] Received desktop public key, completing handshake...");
|
|
418
407
|
const theirPubKey = fromBase64(msg.pubkey);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
theirPubKey,
|
|
422
|
-
pairingCode ?? void 0
|
|
423
|
-
);
|
|
408
|
+
const sharedSecret = await computeSharedSecret(keyPair.privateKey, theirPubKey);
|
|
409
|
+
sessionKey = await deriveSessionKey(sharedSecret, pairingCode ?? void 0);
|
|
424
410
|
sendRaw({
|
|
425
411
|
version: 1,
|
|
426
412
|
session_id: sessionId,
|
|
@@ -435,7 +421,7 @@ async function handleHello(msg) {
|
|
|
435
421
|
try {
|
|
436
422
|
await saveSession(
|
|
437
423
|
sessionId,
|
|
438
|
-
|
|
424
|
+
sharedSecret,
|
|
439
425
|
config.relayUrl,
|
|
440
426
|
config.bridgeUrl,
|
|
441
427
|
pairingCode ?? void 0
|
|
@@ -504,29 +490,31 @@ var import_path2 = __toESM(require("path"));
|
|
|
504
490
|
var import_child_process = require("child_process");
|
|
505
491
|
var SERVICE_NAME = "me.echoclaw.relay-agent";
|
|
506
492
|
var LABEL = "EchoClaw Relay Agent";
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
} catch {
|
|
493
|
+
function getInstallDir() {
|
|
494
|
+
return import_path2.default.join(getConfigDir(), "bin");
|
|
495
|
+
}
|
|
496
|
+
function freezeRuntime() {
|
|
497
|
+
const installDir = getInstallDir();
|
|
498
|
+
if (!import_fs2.default.existsSync(installDir)) {
|
|
499
|
+
import_fs2.default.mkdirSync(installDir, { recursive: true, mode: 493 });
|
|
516
500
|
}
|
|
517
|
-
|
|
518
|
-
|
|
501
|
+
const sourceScript = process.argv[1];
|
|
502
|
+
const destScript = import_path2.default.join(installDir, "agent.js");
|
|
503
|
+
if (sourceScript && import_fs2.default.existsSync(sourceScript)) {
|
|
504
|
+
import_fs2.default.copyFileSync(sourceScript, destScript);
|
|
505
|
+
import_fs2.default.chmodSync(destScript, 493);
|
|
506
|
+
} else {
|
|
507
|
+
throw new Error("Cannot locate the running agent script to install.");
|
|
519
508
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return
|
|
509
|
+
const nodePath = process.execPath;
|
|
510
|
+
const nodeInfoPath = import_path2.default.join(installDir, "node-path.txt");
|
|
511
|
+
import_fs2.default.writeFileSync(nodeInfoPath, nodePath, { mode: 420 });
|
|
512
|
+
console.log(` Copied agent to: ${destScript}`);
|
|
513
|
+
console.log(` Node runtime: ${nodePath}`);
|
|
514
|
+
return { nodePath, agentScript: destScript };
|
|
526
515
|
}
|
|
527
516
|
function macosInstall() {
|
|
528
|
-
const
|
|
529
|
-
const nodePath = resolveNodePath();
|
|
517
|
+
const { nodePath, agentScript } = freezeRuntime();
|
|
530
518
|
const logDir = getConfigDir();
|
|
531
519
|
const plistDir = import_path2.default.join(process.env.HOME, "Library", "LaunchAgents");
|
|
532
520
|
const plistPath = import_path2.default.join(plistDir, `${SERVICE_NAME}.plist`);
|
|
@@ -543,7 +531,7 @@ function macosInstall() {
|
|
|
543
531
|
<key>ProgramArguments</key>
|
|
544
532
|
<array>
|
|
545
533
|
<string>${nodePath}</string>
|
|
546
|
-
<string>${
|
|
534
|
+
<string>${agentScript}</string>
|
|
547
535
|
</array>
|
|
548
536
|
|
|
549
537
|
<key>RunAtLoad</key>
|
|
@@ -574,7 +562,7 @@ function macosInstall() {
|
|
|
574
562
|
(0, import_child_process.execSync)(`launchctl load "${plistPath}"`);
|
|
575
563
|
} catch (err) {
|
|
576
564
|
console.error(`[install] Failed to load LaunchAgent: ${err.message}`);
|
|
577
|
-
console.log(`[install] Plist written to ${plistPath} \u2014
|
|
565
|
+
console.log(`[install] Plist written to ${plistPath} \u2014 load manually:`);
|
|
578
566
|
console.log(` launchctl load "${plistPath}"`);
|
|
579
567
|
return;
|
|
580
568
|
}
|
|
@@ -603,6 +591,10 @@ function macosUninstall() {
|
|
|
603
591
|
} catch {
|
|
604
592
|
}
|
|
605
593
|
import_fs2.default.unlinkSync(plistPath);
|
|
594
|
+
const installDir = getInstallDir();
|
|
595
|
+
if (import_fs2.default.existsSync(installDir)) {
|
|
596
|
+
import_fs2.default.rmSync(installDir, { recursive: true, force: true });
|
|
597
|
+
}
|
|
606
598
|
console.log("");
|
|
607
599
|
console.log(" \u2705 Uninstalled macOS LaunchAgent");
|
|
608
600
|
console.log(` Removed: ${plistPath}`);
|
|
@@ -631,8 +623,7 @@ function macosStatus() {
|
|
|
631
623
|
}
|
|
632
624
|
}
|
|
633
625
|
function linuxInstall() {
|
|
634
|
-
const
|
|
635
|
-
const nodePath = resolveNodePath();
|
|
626
|
+
const { nodePath, agentScript } = freezeRuntime();
|
|
636
627
|
const logDir = getConfigDir();
|
|
637
628
|
const serviceDir = import_path2.default.join(
|
|
638
629
|
process.env.HOME,
|
|
@@ -651,12 +642,13 @@ Wants=network-online.target
|
|
|
651
642
|
|
|
652
643
|
[Service]
|
|
653
644
|
Type=simple
|
|
654
|
-
ExecStart=${nodePath} ${
|
|
645
|
+
ExecStart=${nodePath} ${agentScript}
|
|
655
646
|
Restart=always
|
|
656
647
|
RestartSec=10
|
|
657
648
|
StandardOutput=append:${logDir}/stdout.log
|
|
658
649
|
StandardError=append:${logDir}/stderr.log
|
|
659
650
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
651
|
+
Environment=HOME=${process.env.HOME}
|
|
660
652
|
|
|
661
653
|
[Install]
|
|
662
654
|
WantedBy=default.target
|
|
@@ -711,6 +703,10 @@ function linuxUninstall() {
|
|
|
711
703
|
(0, import_child_process.execSync)("systemctl --user daemon-reload");
|
|
712
704
|
} catch {
|
|
713
705
|
}
|
|
706
|
+
const installDir = getInstallDir();
|
|
707
|
+
if (import_fs2.default.existsSync(installDir)) {
|
|
708
|
+
import_fs2.default.rmSync(installDir, { recursive: true, force: true });
|
|
709
|
+
}
|
|
714
710
|
console.log("");
|
|
715
711
|
console.log(" \u2705 Uninstalled systemd user service");
|
|
716
712
|
console.log(` Removed: ${servicePath}`);
|
|
@@ -741,8 +737,7 @@ function linuxStatus() {
|
|
|
741
737
|
}
|
|
742
738
|
}
|
|
743
739
|
function windowsInstall() {
|
|
744
|
-
const
|
|
745
|
-
const nodePath = resolveNodePath();
|
|
740
|
+
const { nodePath, agentScript } = freezeRuntime();
|
|
746
741
|
try {
|
|
747
742
|
(0, import_child_process.execSync)(
|
|
748
743
|
`schtasks /Delete /TN "${SERVICE_NAME}" /F 2>nul`,
|
|
@@ -752,7 +747,7 @@ function windowsInstall() {
|
|
|
752
747
|
}
|
|
753
748
|
try {
|
|
754
749
|
(0, import_child_process.execSync)(
|
|
755
|
-
`schtasks /Create /TN "${SERVICE_NAME}" /TR "\\"${nodePath}\\" \\"${
|
|
750
|
+
`schtasks /Create /TN "${SERVICE_NAME}" /TR "\\"${nodePath}\\" \\"${agentScript}\\"" /SC ONLOGON /RL HIGHEST /F`,
|
|
756
751
|
{ encoding: "utf-8" }
|
|
757
752
|
);
|
|
758
753
|
(0, import_child_process.execSync)(`schtasks /Run /TN "${SERVICE_NAME}"`, { encoding: "utf-8" });
|
|
@@ -772,12 +767,17 @@ function windowsInstall() {
|
|
|
772
767
|
function windowsUninstall() {
|
|
773
768
|
try {
|
|
774
769
|
(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
770
|
} catch {
|
|
779
771
|
console.log("[uninstall] No scheduled task found \u2014 nothing to remove.");
|
|
772
|
+
return;
|
|
780
773
|
}
|
|
774
|
+
const installDir = getInstallDir();
|
|
775
|
+
if (import_fs2.default.existsSync(installDir)) {
|
|
776
|
+
import_fs2.default.rmSync(installDir, { recursive: true, force: true });
|
|
777
|
+
}
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log(" \u2705 Uninstalled Windows Scheduled Task");
|
|
780
|
+
console.log("");
|
|
781
781
|
}
|
|
782
782
|
function windowsStatus() {
|
|
783
783
|
try {
|
|
@@ -846,14 +846,70 @@ function serviceStatus() {
|
|
|
846
846
|
break;
|
|
847
847
|
}
|
|
848
848
|
}
|
|
849
|
+
function restartService() {
|
|
850
|
+
const platform = getPlatform();
|
|
851
|
+
switch (platform) {
|
|
852
|
+
case "macos": {
|
|
853
|
+
const plistPath = import_path2.default.join(
|
|
854
|
+
process.env.HOME,
|
|
855
|
+
"Library",
|
|
856
|
+
"LaunchAgents",
|
|
857
|
+
`${SERVICE_NAME}.plist`
|
|
858
|
+
);
|
|
859
|
+
try {
|
|
860
|
+
(0, import_child_process.execSync)(`launchctl unload "${plistPath}" 2>/dev/null || true`);
|
|
861
|
+
(0, import_child_process.execSync)(`launchctl load "${plistPath}"`);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
console.error(`[restart] Failed: ${err.message}`);
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
case "linux":
|
|
868
|
+
try {
|
|
869
|
+
(0, import_child_process.execSync)(`systemctl --user restart ${SERVICE_NAME}.service`);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.error(`[restart] Failed: ${err.message}`);
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
case "windows":
|
|
875
|
+
try {
|
|
876
|
+
(0, import_child_process.execSync)(`schtasks /End /TN "${SERVICE_NAME}" 2>nul`, { stdio: "pipe" });
|
|
877
|
+
(0, import_child_process.execSync)(`schtasks /Run /TN "${SERVICE_NAME}"`, { encoding: "utf-8" });
|
|
878
|
+
} catch (err) {
|
|
879
|
+
console.error(`[restart] Failed: ${err.message}`);
|
|
880
|
+
}
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function isServiceInstalled() {
|
|
885
|
+
const platform = getPlatform();
|
|
886
|
+
switch (platform) {
|
|
887
|
+
case "macos":
|
|
888
|
+
return import_fs2.default.existsSync(
|
|
889
|
+
import_path2.default.join(process.env.HOME, "Library", "LaunchAgents", `${SERVICE_NAME}.plist`)
|
|
890
|
+
);
|
|
891
|
+
case "linux":
|
|
892
|
+
return import_fs2.default.existsSync(
|
|
893
|
+
import_path2.default.join(process.env.HOME, ".config", "systemd", "user", `${SERVICE_NAME}.service`)
|
|
894
|
+
);
|
|
895
|
+
case "windows":
|
|
896
|
+
try {
|
|
897
|
+
(0, import_child_process.execSync)(`schtasks /Query /TN "${SERVICE_NAME}" 2>nul`, { stdio: "pipe" });
|
|
898
|
+
return true;
|
|
899
|
+
} catch {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
849
904
|
|
|
850
905
|
// src/main.ts
|
|
851
|
-
var VERSION = "0.2.
|
|
906
|
+
var VERSION = "0.2.1";
|
|
852
907
|
function parseArgs() {
|
|
853
908
|
const args = process.argv.slice(2);
|
|
854
909
|
const opts = {
|
|
855
910
|
relayUrl: "wss://relay.echoclaw.me/agent/connect",
|
|
856
|
-
bridgeUrl: "
|
|
911
|
+
bridgeUrl: ""
|
|
912
|
+
// auto-discover if not specified
|
|
857
913
|
};
|
|
858
914
|
for (let i = 0; i < args.length; i++) {
|
|
859
915
|
const arg = args[i];
|
|
@@ -883,6 +939,9 @@ function parseArgs() {
|
|
|
883
939
|
case "--status":
|
|
884
940
|
opts.command = "status";
|
|
885
941
|
break;
|
|
942
|
+
case "--restart":
|
|
943
|
+
opts.command = "restart";
|
|
944
|
+
break;
|
|
886
945
|
case "--help":
|
|
887
946
|
case "-h":
|
|
888
947
|
printHelp();
|
|
@@ -916,9 +975,10 @@ function printHelp() {
|
|
|
916
975
|
(default: wss://relay.echoclaw.me/agent/connect)
|
|
917
976
|
|
|
918
977
|
--bridge, -b <url> Local OpenClaw bridge HTTP URL
|
|
919
|
-
(default:
|
|
978
|
+
(default: auto-discover on port 18789, 8013)
|
|
920
979
|
|
|
921
980
|
--install Install as system service (auto-start on boot)
|
|
981
|
+
--restart Restart the system service (repair connection)
|
|
922
982
|
--uninstall Remove system service and saved session
|
|
923
983
|
--status Show service and connection status
|
|
924
984
|
|
|
@@ -928,6 +988,7 @@ function printHelp() {
|
|
|
928
988
|
EXAMPLES
|
|
929
989
|
echoclaw-relay-agent --code ABC-1234 # Pair and connect
|
|
930
990
|
echoclaw-relay-agent --code ABC-1234 --install # Pair, connect, and install as service
|
|
991
|
+
echoclaw-relay-agent --restart # Restart service (repair)
|
|
931
992
|
echoclaw-relay-agent --status # Check status
|
|
932
993
|
echoclaw-relay-agent --uninstall # Remove service
|
|
933
994
|
|
|
@@ -936,6 +997,22 @@ function printHelp() {
|
|
|
936
997
|
The relay server cannot read your messages \u2014 it only forwards ciphertext.
|
|
937
998
|
`);
|
|
938
999
|
}
|
|
1000
|
+
var BRIDGE_PORTS = [18789, 8013];
|
|
1001
|
+
async function discoverBridge() {
|
|
1002
|
+
for (const port of BRIDGE_PORTS) {
|
|
1003
|
+
const url = `http://localhost:${port}`;
|
|
1004
|
+
try {
|
|
1005
|
+
const res = await fetch(`${url}/health`, {
|
|
1006
|
+
signal: AbortSignal.timeout(1500)
|
|
1007
|
+
});
|
|
1008
|
+
if (res.ok) {
|
|
1009
|
+
return url;
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
939
1016
|
function handleInstall() {
|
|
940
1017
|
console.log("");
|
|
941
1018
|
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");
|
|
@@ -958,6 +1035,22 @@ function handleUninstall() {
|
|
|
958
1035
|
clearSession();
|
|
959
1036
|
console.log(" Session data cleared.");
|
|
960
1037
|
}
|
|
1038
|
+
function handleRestart() {
|
|
1039
|
+
if (!hasSession()) {
|
|
1040
|
+
console.error(" \u274C No saved session. Re-pair first:");
|
|
1041
|
+
console.error(" echoclaw-relay-agent --code ABC-1234 --install");
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
if (!isServiceInstalled()) {
|
|
1045
|
+
console.log(" Service not installed \u2014 installing now...");
|
|
1046
|
+
installService();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
restartService();
|
|
1050
|
+
console.log("");
|
|
1051
|
+
console.log(" \u2705 Relay agent service restarted.");
|
|
1052
|
+
console.log("");
|
|
1053
|
+
}
|
|
961
1054
|
function handleStatus() {
|
|
962
1055
|
console.log("");
|
|
963
1056
|
console.log(` EchoClaw Relay Agent v${VERSION}`);
|
|
@@ -976,6 +1069,10 @@ async function main() {
|
|
|
976
1069
|
handleUninstall();
|
|
977
1070
|
return;
|
|
978
1071
|
}
|
|
1072
|
+
if (opts.command === "restart") {
|
|
1073
|
+
handleRestart();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
979
1076
|
if (opts.command === "status") {
|
|
980
1077
|
handleStatus();
|
|
981
1078
|
return;
|
|
@@ -989,25 +1086,36 @@ async function main() {
|
|
|
989
1086
|
console.log(" \u2502 Open Source \xB7 Apache License 2.0 \u2502");
|
|
990
1087
|
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");
|
|
991
1088
|
console.log("");
|
|
1089
|
+
if (!opts.bridgeUrl) {
|
|
1090
|
+
const discovered = await discoverBridge();
|
|
1091
|
+
if (discovered) {
|
|
1092
|
+
opts.bridgeUrl = discovered;
|
|
1093
|
+
console.log(` \u2705 Found OpenClaw bridge at ${discovered}`);
|
|
1094
|
+
} else {
|
|
1095
|
+
opts.bridgeUrl = `http://localhost:${BRIDGE_PORTS[0]}`;
|
|
1096
|
+
console.warn(` \u26A0\uFE0F Cannot find OpenClaw bridge (tried ports ${BRIDGE_PORTS.join(", ")})`);
|
|
1097
|
+
console.warn(" Will retry when messages arrive. Or specify: --bridge http://localhost:<port>");
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
try {
|
|
1101
|
+
const healthRes = await fetch(`${opts.bridgeUrl}/health`, {
|
|
1102
|
+
signal: AbortSignal.timeout(3e3)
|
|
1103
|
+
});
|
|
1104
|
+
if (healthRes.ok) {
|
|
1105
|
+
console.log(` \u2705 Bridge reachable at ${opts.bridgeUrl}`);
|
|
1106
|
+
} else {
|
|
1107
|
+
console.warn(` \u26A0\uFE0F Bridge returned HTTP ${healthRes.status} \u2014 starting anyway`);
|
|
1108
|
+
}
|
|
1109
|
+
} catch {
|
|
1110
|
+
console.warn(` \u26A0\uFE0F Cannot reach ${opts.bridgeUrl} \u2014 will retry when messages arrive`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
992
1113
|
console.log(` Relay: ${opts.relayUrl}`);
|
|
993
1114
|
console.log(` Bridge: ${opts.bridgeUrl}`);
|
|
994
1115
|
if (opts.code) {
|
|
995
1116
|
console.log(` Code: ${opts.code} (joining existing session)`);
|
|
996
1117
|
}
|
|
997
1118
|
console.log("");
|
|
998
|
-
try {
|
|
999
|
-
const healthRes = await fetch(`${opts.bridgeUrl}/health`, {
|
|
1000
|
-
signal: AbortSignal.timeout(3e3)
|
|
1001
|
-
});
|
|
1002
|
-
if (healthRes.ok) {
|
|
1003
|
-
console.log(" \u2705 Local bridge is reachable");
|
|
1004
|
-
} else {
|
|
1005
|
-
console.warn(` \u26A0\uFE0F Bridge returned HTTP ${healthRes.status} \u2014 starting anyway`);
|
|
1006
|
-
}
|
|
1007
|
-
} catch {
|
|
1008
|
-
console.warn(" \u26A0\uFE0F Cannot reach local bridge \u2014 will retry when messages arrive");
|
|
1009
|
-
}
|
|
1010
|
-
console.log("");
|
|
1011
1119
|
let relayUrl = opts.relayUrl;
|
|
1012
1120
|
if (opts.code) {
|
|
1013
1121
|
const separator = relayUrl.includes("?") ? "&" : "?";
|