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.
Files changed (2) hide show
  1. package/dist/main.js +188 -80
  2. 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
- false,
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, sessionKey2, relayUrl, bridgeUrl, pairingCode2) {
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: keyBase64,
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.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
- );
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}ms (attempt ${reconnect.attempt})...`);
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(` \u2551 Pairing Code: ${pairingCode.padEnd(24)}\u2551`);
410
- 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\u255D");
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 client to connect.");
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
- sessionKey = await completeHandshake(
420
- keyPair.privateKey,
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
- sessionKey,
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 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 {
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
- if (scriptPath && import_fs2.default.existsSync(scriptPath)) {
518
- return scriptPath;
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
- 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;
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 agentBin = resolveNodeBin();
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>${agentBin}</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 you can load it manually:`);
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 agentBin = resolveNodeBin();
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} ${agentBin}
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 agentBin = resolveNodeBin();
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}\\" \\"${agentBin}\\"" /SC ONLOGON /RL HIGHEST /F`,
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.0";
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: "http://localhost:8013"
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: http://localhost:8013)
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("?") ? "&" : "?";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoclaw-relay-agent",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "EchoClaw Relay Agent — connects OpenClaw bridge to the EchoClaw Relay Server with E2E encryption",
5
5
  "main": "./dist/main.js",
6
6
  "bin": {