@matelink/cli 2026.4.11 → 2026.4.13

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/bin/matecli.mjs +310 -21
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -440,6 +440,10 @@ function resolveBridgeLogsDir() {
440
440
  return path.join(resolveMatecliRuntimeDir(), "logs");
441
441
  }
442
442
 
443
+ function resolveBridgeLaunchScriptPath() {
444
+ return path.join(resolveMatecliRuntimeDir(), "bridge-launch.sh");
445
+ }
446
+
443
447
  function resolveMacLaunchAgentPath() {
444
448
  return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
445
449
  }
@@ -1247,6 +1251,127 @@ function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
1247
1251
  return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
1248
1252
  }
1249
1253
 
1254
+ function escapePosixShellSingleQuoted(value) {
1255
+ return String(value ?? "").replaceAll("'", "'\"'\"'");
1256
+ }
1257
+
1258
+ function buildPosixShellExec(command, args) {
1259
+ return [command, ...args]
1260
+ .map((part) => `'${escapePosixShellSingleQuoted(part)}'`)
1261
+ .join(" ");
1262
+ }
1263
+
1264
+ function resolveNodeCandidates() {
1265
+ const homeDir = os.homedir();
1266
+ const candidates = new Set();
1267
+ const addCandidate = (candidate) => {
1268
+ const normalized = String(candidate ?? "").trim();
1269
+ if (!normalized) {
1270
+ return;
1271
+ }
1272
+ try {
1273
+ if (fs.existsSync(normalized)) {
1274
+ candidates.add(fs.realpathSync(normalized));
1275
+ }
1276
+ } catch {
1277
+ if (fs.existsSync(normalized)) {
1278
+ candidates.add(normalized);
1279
+ }
1280
+ }
1281
+ };
1282
+
1283
+ addCandidate(process.execPath);
1284
+ addCandidate(path.join(homeDir, ".nvm", "current", "bin", "node"));
1285
+ addCandidate(path.join(homeDir, ".volta", "bin", "node"));
1286
+ addCandidate("/opt/homebrew/bin/node");
1287
+ addCandidate("/usr/local/bin/node");
1288
+ addCandidate("/usr/bin/node");
1289
+
1290
+ const nvmVersionsDir = path.join(homeDir, ".nvm", "versions", "node");
1291
+ try {
1292
+ const nvmEntries = fs
1293
+ .readdirSync(nvmVersionsDir, { withFileTypes: true })
1294
+ .filter((entry) => entry.isDirectory())
1295
+ .map((entry) => entry.name)
1296
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
1297
+ for (const version of nvmEntries) {
1298
+ addCandidate(path.join(nvmVersionsDir, version, "bin", "node"));
1299
+ }
1300
+ } catch {}
1301
+
1302
+ const localNodeRoots = [
1303
+ path.join(homeDir, ".local"),
1304
+ path.join(homeDir, ".local", "node"),
1305
+ ];
1306
+ for (const root of localNodeRoots) {
1307
+ try {
1308
+ const entries = fs
1309
+ .readdirSync(root, { withFileTypes: true })
1310
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("node-"))
1311
+ .map((entry) => entry.name)
1312
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
1313
+ for (const name of entries) {
1314
+ addCandidate(path.join(root, name, "bin", "node"));
1315
+ }
1316
+ } catch {}
1317
+ }
1318
+
1319
+ return [...candidates];
1320
+ }
1321
+
1322
+ function resolveBridgeServiceNodePath() {
1323
+ for (const candidate of resolveNodeCandidates()) {
1324
+ if (candidate && fs.existsSync(candidate)) {
1325
+ return candidate;
1326
+ }
1327
+ }
1328
+ const whichNode = runCommand("which", ["node"]);
1329
+ if (whichNode.status === 0) {
1330
+ const resolved = String(whichNode.stdout ?? "").trim();
1331
+ if (resolved && fs.existsSync(resolved)) {
1332
+ return resolved;
1333
+ }
1334
+ }
1335
+ fail("node runtime is required for bridge service install");
1336
+ }
1337
+
1338
+ function writeBridgeLaunchScript(installConfig) {
1339
+ const scriptPath = resolveBridgeLaunchScriptPath();
1340
+ fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
1341
+ const defaultPath = [
1342
+ "/opt/homebrew/bin",
1343
+ "/usr/local/bin",
1344
+ "/usr/bin",
1345
+ "/bin",
1346
+ "/usr/sbin",
1347
+ "/sbin",
1348
+ path.join(os.homedir(), ".volta", "bin"),
1349
+ path.join(os.homedir(), ".nvm", "current", "bin"),
1350
+ ].join(":");
1351
+ const knownNodeCandidates = resolveNodeCandidates();
1352
+ const candidateChecks = knownNodeCandidates
1353
+ .map((candidate) => `if [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}'; fi`)
1354
+ .join("\n");
1355
+ const fallbackLine = buildPosixShellExec("node", installConfig.args);
1356
+ const script = [
1357
+ "#!/bin/sh",
1358
+ "set -eu",
1359
+ "export PATH='" + escapePosixShellSingleQuoted(defaultPath) + ":'\"${PATH:-}\"",
1360
+ "NODE_BIN=''",
1361
+ candidateChecks,
1362
+ "if [ -z \"$NODE_BIN\" ] && command -v node >/dev/null 2>&1; then",
1363
+ " NODE_BIN=\"$(command -v node)\"",
1364
+ "fi",
1365
+ "if [ -n \"$NODE_BIN\" ]; then",
1366
+ ` exec "$NODE_BIN" ${installConfig.args.map((arg) => `'${escapePosixShellSingleQuoted(arg)}'`).join(" ")}`,
1367
+ "fi",
1368
+ `exec ${fallbackLine}`,
1369
+ "",
1370
+ ].join("\n");
1371
+ fs.writeFileSync(scriptPath, script, { encoding: "utf8", mode: 0o755 });
1372
+ return scriptPath;
1373
+ }
1374
+
1250
1375
  function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1251
1376
  if (!relayUrl) {
1252
1377
  fail("relay URL is required for bridge service");
@@ -1262,17 +1387,72 @@ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1262
1387
  logDir,
1263
1388
  stdoutLog: path.join(logDir, "bridge.stdout.log"),
1264
1389
  stderrLog: path.join(logDir, "bridge.stderr.log"),
1265
- nodePath: process.execPath,
1390
+ nodePath: resolveBridgeServiceNodePath(),
1266
1391
  scriptPath: CLI_ENTRY,
1267
1392
  args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1268
1393
  };
1269
1394
  }
1270
1395
 
1396
+ function escapeXmlText(value) {
1397
+ return String(value)
1398
+ .replaceAll("&", "&")
1399
+ .replaceAll("<", "&lt;")
1400
+ .replaceAll(">", "&gt;");
1401
+ }
1402
+
1403
+ function readMacLaunchdServiceStatus(uid, plistPath) {
1404
+ const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
1405
+ for (const ref of refs) {
1406
+ const result = runCommand("launchctl", ["print", ref]);
1407
+ if (result.status !== 0) {
1408
+ continue;
1409
+ }
1410
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
1411
+ const stateMatch = output.match(/state = ([^\n]+)/u);
1412
+ const activeCountMatch = output.match(/active count = (\d+)/u);
1413
+ const pidMatch = output.match(/\bpid = (\d+)/u);
1414
+ const exitCodeMatch = output.match(/last exit code = (\d+)/u);
1415
+ const state = stateMatch?.[1]?.trim() ?? "";
1416
+ const activeCount = Number.parseInt(activeCountMatch?.[1] ?? "0", 10) || 0;
1417
+ const pid = Number.parseInt(pidMatch?.[1] ?? "0", 10) || 0;
1418
+ const lastExitCode = Number.parseInt(exitCodeMatch?.[1] ?? "0", 10) || 0;
1419
+ const running =
1420
+ state.toLowerCase() === "running" ||
1421
+ activeCount > 0 ||
1422
+ (pid > 0 && isPidAlive(pid));
1423
+ return {
1424
+ manager: "launchd",
1425
+ installed: true,
1426
+ running,
1427
+ target: plistPath,
1428
+ logDir: resolveBridgeLogsDir(),
1429
+ ref,
1430
+ state,
1431
+ activeCount,
1432
+ pid: pid || null,
1433
+ lastExitCode,
1434
+ };
1435
+ }
1436
+ return {
1437
+ manager: "launchd",
1438
+ installed: fs.existsSync(plistPath),
1439
+ running: false,
1440
+ target: plistPath,
1441
+ logDir: resolveBridgeLogsDir(),
1442
+ ref: SERVICE_LABEL,
1443
+ state: "not-loaded",
1444
+ activeCount: 0,
1445
+ pid: null,
1446
+ lastExitCode: 0,
1447
+ };
1448
+ }
1449
+
1271
1450
  function installBridgeServiceMac(installConfig) {
1272
1451
  const plistPath = resolveMacLaunchAgentPath();
1452
+ const launchScriptPath = writeBridgeLaunchScript(installConfig);
1273
1453
  fs.mkdirSync(path.dirname(plistPath), { recursive: true });
1274
- const argsXml = installConfig.args
1275
- .map((arg) => ` <string>${String(arg).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</string>`)
1454
+ const argsXml = [launchScriptPath]
1455
+ .map((arg) => ` <string>${escapeXmlText(arg)}</string>`)
1276
1456
  .join("\n");
1277
1457
  const plist = [
1278
1458
  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
@@ -1315,6 +1495,13 @@ function installBridgeServiceMac(installConfig) {
1315
1495
  }
1316
1496
  selectedDomain = "launchd";
1317
1497
  }
1498
+ const status = readMacLaunchdServiceStatus(uid, plistPath);
1499
+ if (!status.running) {
1500
+ const details = status.lastExitCode
1501
+ ? `launchd last exit code: ${status.lastExitCode}`
1502
+ : "launchd service failed to stay running";
1503
+ throw new Error(details);
1504
+ }
1318
1505
  return {
1319
1506
  manager: "launchd",
1320
1507
  installed: true,
@@ -1409,6 +1596,7 @@ function installBridgeService({ relayUrl, gatewayBaseUrl }) {
1409
1596
  function uninstallBridgeService() {
1410
1597
  if (process.platform === "darwin") {
1411
1598
  const plistPath = resolveMacLaunchAgentPath();
1599
+ const launchScriptPath = resolveBridgeLaunchScriptPath();
1412
1600
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
1413
1601
  if (uid != null) {
1414
1602
  runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
@@ -1417,6 +1605,9 @@ function uninstallBridgeService() {
1417
1605
  try {
1418
1606
  fs.unlinkSync(plistPath);
1419
1607
  } catch {}
1608
+ try {
1609
+ fs.unlinkSync(launchScriptPath);
1610
+ } catch {}
1420
1611
  return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
1421
1612
  }
1422
1613
  if (process.platform === "linux") {
@@ -1435,23 +1626,56 @@ function uninstallBridgeService() {
1435
1626
  throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
1436
1627
  }
1437
1628
 
1629
+ function stopBridgeService() {
1630
+ if (process.platform === "darwin") {
1631
+ const plistPath = resolveMacLaunchAgentPath();
1632
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1633
+ if (uid != null) {
1634
+ runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
1635
+ runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
1636
+ }
1637
+ return {
1638
+ manager: "launchd",
1639
+ installed: fs.existsSync(plistPath),
1640
+ running: false,
1641
+ target: plistPath,
1642
+ logDir: resolveBridgeLogsDir(),
1643
+ };
1644
+ }
1645
+ if (process.platform === "linux") {
1646
+ const unitPath = resolveLinuxUserUnitPath();
1647
+ runCommand("systemctl", ["--user", "stop", SERVICE_UNIT_NAME]);
1648
+ return {
1649
+ manager: "systemd-user",
1650
+ installed: fs.existsSync(unitPath),
1651
+ running: false,
1652
+ target: unitPath,
1653
+ logDir: resolveBridgeLogsDir(),
1654
+ };
1655
+ }
1656
+ if (process.platform === "win32") {
1657
+ runCommand("powershell.exe", ["-NoProfile", "-Command", `Stop-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -ErrorAction SilentlyContinue`]);
1658
+ return {
1659
+ manager: "task-scheduler",
1660
+ installed: true,
1661
+ running: false,
1662
+ target: SERVICE_TASK_NAME,
1663
+ logDir: resolveBridgeLogsDir(),
1664
+ };
1665
+ }
1666
+ return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir() };
1667
+ }
1668
+
1438
1669
  function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
1439
- const installed = installBridgeService({ relayUrl, gatewayBaseUrl });
1440
- return installed;
1670
+ stopBridgeService();
1671
+ return installBridgeService({ relayUrl, gatewayBaseUrl });
1441
1672
  }
1442
1673
 
1443
1674
  function getBridgeServiceStatus() {
1444
1675
  if (process.platform === "darwin") {
1445
1676
  const plistPath = resolveMacLaunchAgentPath();
1446
1677
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
1447
- const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
1448
- for (const ref of refs) {
1449
- const result = runCommand("launchctl", ["print", ref]);
1450
- if (result.status === 0) {
1451
- return { manager: "launchd", installed: true, running: true, target: plistPath, logDir: resolveBridgeLogsDir(), ref };
1452
- }
1453
- }
1454
- return { manager: "launchd", installed: fs.existsSync(plistPath), running: false, target: plistPath, logDir: resolveBridgeLogsDir(), ref: SERVICE_LABEL };
1678
+ return readMacLaunchdServiceStatus(uid, plistPath);
1455
1679
  }
1456
1680
  if (process.platform === "linux") {
1457
1681
  const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
@@ -1541,6 +1765,29 @@ function stopBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1541
1765
  };
1542
1766
  }
1543
1767
 
1768
+ function clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl }) {
1769
+ const serviceStatus = getBridgeServiceStatus();
1770
+ let serviceStopped = null;
1771
+ if (serviceStatus.installed) {
1772
+ try {
1773
+ serviceStopped = stopBridgeService();
1774
+ } catch (error) {
1775
+ serviceStopped = {
1776
+ error: String(error instanceof Error ? error.message : error),
1777
+ };
1778
+ }
1779
+ }
1780
+ const stoppedProcesses =
1781
+ relayUrl && gatewayBaseUrl
1782
+ ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
1783
+ : { count: 0, pids: [] };
1784
+ return {
1785
+ serviceStatus,
1786
+ serviceStopped,
1787
+ stoppedProcesses,
1788
+ };
1789
+ }
1790
+
1544
1791
  function resolvePublishedGatewayBaseUrl({
1545
1792
  relayUrl,
1546
1793
  bindPublicBaseUrl,
@@ -2462,9 +2709,34 @@ async function callGatewayRpcLocal({
2462
2709
  };
2463
2710
  }
2464
2711
 
2465
- // In bridge mode the CLI path is more reliable than the shared WebSocket client,
2466
- // especially while long-running streamed chats are also active.
2467
- return callGatewayRpcLocalViaCli({ gatewayBaseUrl, gatewayAuthToken, method, params });
2712
+ // Prefer CLI first for broad compatibility, but fall back to the persistent
2713
+ // WS client when the CLI gateway call hits transient websocket closures.
2714
+ try {
2715
+ return await callGatewayRpcLocalViaCli({
2716
+ gatewayBaseUrl,
2717
+ gatewayAuthToken,
2718
+ method,
2719
+ params,
2720
+ });
2721
+ } catch (error) {
2722
+ if (!shouldFallbackGatewayRpcViaWs(error)) {
2723
+ throw error;
2724
+ }
2725
+ const client = getOrCreateGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken });
2726
+ return client.call(method, params ?? {});
2727
+ }
2728
+ }
2729
+
2730
+ function shouldFallbackGatewayRpcViaWs(error) {
2731
+ const message = String(error instanceof Error ? error.message : error).toLowerCase();
2732
+ return (
2733
+ message.includes("gateway closed") ||
2734
+ message.includes("abnormal closure") ||
2735
+ message.includes("(1006") ||
2736
+ message.includes("no close reason") ||
2737
+ message.includes("gateway ws error") ||
2738
+ message.includes("gateway disconnected")
2739
+ );
2468
2740
  }
2469
2741
 
2470
2742
  async function callGatewayRpcLocalViaCli({
@@ -2480,6 +2752,8 @@ async function callGatewayRpcLocalViaCli({
2480
2752
  method,
2481
2753
  "--url",
2482
2754
  wsUrl,
2755
+ "--timeout",
2756
+ String(GATEWAY_RPC_CLI_TIMEOUT_MS),
2483
2757
  "--params",
2484
2758
  JSON.stringify(params ?? {}),
2485
2759
  "--json",
@@ -3162,11 +3436,24 @@ async function runPair({
3162
3436
  publishedGatewayBaseUrl,
3163
3437
  gatewayRestarted: Boolean(restartResult?.ok),
3164
3438
  relayGatewayId: relayCredentials?.gatewayId ?? null,
3439
+ bridgeCleanup: null,
3165
3440
  };
3166
3441
 
3167
3442
  if (serveRelay && !json) {
3443
+ const bridgeCleanup = clearExistingBridgeRuntime({
3444
+ relayUrl,
3445
+ gatewayBaseUrl: localGatewayBaseUrl,
3446
+ });
3447
+ output.bridgeCleanup = {
3448
+ serviceInstalled: bridgeCleanup.serviceStatus?.installed === true,
3449
+ serviceRunning: bridgeCleanup.serviceStatus?.running === true,
3450
+ serviceStopError: bridgeCleanup.serviceStopped?.error ?? null,
3451
+ stoppedProcessCount: bridgeCleanup.stoppedProcesses?.count ?? 0,
3452
+ stoppedPids: bridgeCleanup.stoppedProcesses?.pids ?? [],
3453
+ };
3168
3454
  try {
3169
- const serviceResult = installBridgeService({
3455
+ const serviceStatus = getBridgeServiceStatus();
3456
+ const serviceResult = (serviceStatus.installed ? restartBridgeService : installBridgeService)({
3170
3457
  relayUrl,
3171
3458
  gatewayBaseUrl: localGatewayBaseUrl,
3172
3459
  });
@@ -3240,10 +3527,6 @@ async function runReset({ json, relay, noRelay, gateway }) {
3240
3527
  });
3241
3528
  }
3242
3529
 
3243
- const stoppedBridges =
3244
- relayUrl && gatewayBaseUrl
3245
- ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
3246
- : { count: 0, pids: [] };
3247
3530
  let serviceReset = null;
3248
3531
  if (relayUrl && gatewayBaseUrl) {
3249
3532
  try {
@@ -3252,6 +3535,10 @@ async function runReset({ json, relay, noRelay, gateway }) {
3252
3535
  serviceReset = { error: String(error instanceof Error ? error.message : error) };
3253
3536
  }
3254
3537
  }
3538
+ const stoppedBridges =
3539
+ relayUrl && gatewayBaseUrl
3540
+ ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
3541
+ : { count: 0, pids: [] };
3255
3542
  const nextState = resetLocalBindingState({ accountId });
3256
3543
 
3257
3544
  const output = {
@@ -3324,9 +3611,11 @@ async function runService({ json, relay, noRelay, gateway, serviceAction }) {
3324
3611
  result = getBridgeServiceStatus();
3325
3612
  break;
3326
3613
  case "install":
3614
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3327
3615
  result = installBridgeService({ relayUrl, gatewayBaseUrl });
3328
3616
  break;
3329
3617
  case "restart":
3618
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3330
3619
  result = restartBridgeService({ relayUrl, gatewayBaseUrl });
3331
3620
  break;
3332
3621
  case "uninstall":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.11",
3
+ "version": "2026.4.13",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",