@matelink/cli 2026.4.10 → 2026.4.12

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/README.md CHANGED
@@ -78,7 +78,7 @@ of `openclaw.json`.
78
78
  Start OpenClaw first, then pair from the app:
79
79
 
80
80
  ```bash
81
- openclaw gateway run --bind loopback --port 18789 --force
81
+ openclaw gateway run --bind loopback --port <gateway.port in openclaw.json> --force
82
82
  ```
83
83
 
84
84
  Use the mobile app to generate a pairing code, then run:
package/bin/matecli.mjs CHANGED
@@ -19,13 +19,17 @@ import { fileURLToPath } from "node:url";
19
19
  const NUMERIC_CODE_LENGTH = 4;
20
20
  const GROUP_SIZE = 4;
21
21
  const DEFAULT_RELAY_WORKER_WAIT_SECONDS = 600;
22
+ const DEFAULT_NETWORK_TIMEOUT_MS = 600000;
22
23
  const DEFAULT_RELAY_URL = "http://43.134.64.199:8090";
23
24
  const DEFAULT_GATEWAY_HOST = "127.0.0.1";
24
25
  const DEFAULT_WEBHOOK_PATH = "/testnextim/webhook";
25
26
  const DEFAULT_BIND_PATH = "/testnextim/bind";
26
- const GATEWAY_RPC_CLI_TIMEOUT_MS = 20000;
27
+ const GATEWAY_RPC_CLI_TIMEOUT_MS = DEFAULT_NETWORK_TIMEOUT_MS;
27
28
  const CLI_PACKAGE_NAME = "@matelink/cli";
28
29
  const CLI_COMMAND_NAME = "matecli";
30
+ const SERVICE_LABEL = "com.matelink.matecli.bridge";
31
+ const SERVICE_UNIT_NAME = "matelink-matecli-bridge.service";
32
+ const SERVICE_TASK_NAME = "Matelink MateCLI Bridge";
29
33
  // Relay worker defaults to full operator scope so gateway HTTP routes and RPC-adjacent
30
34
  // compatibility endpoints remain available without per-method scope juggling.
31
35
  const DEFAULT_GATEWAY_SCOPES = "operator.admin";
@@ -38,8 +42,9 @@ const CLI_I18N = {
38
42
  help_environment: "环境变量:",
39
43
  help_package: `包名: ${CLI_PACKAGE_NAME}`,
40
44
  help_tip: "提示:",
41
- help_tip_pair: " pair 命令会自动以后台模式启动 relay bridge worker。",
45
+ help_tip_pair: " pair 命令会自动安装并启动系统后台服务,保持 relay bridge 在线。",
42
46
  help_tip_no_serve: " 如果不希望自动启动 bridge worker,可使用 --no-serve-relay。",
47
+ help_tip_service: ` 可使用 \`${CLI_COMMAND_NAME} service status|install|uninstall|restart\` 管理后台服务。`,
43
48
  help_env_home: " OPENCLAW_HOME 覆盖 OpenClaw 主目录(默认: ~/.openclaw)",
44
49
  help_env_openim: " OPENIM_RELAY_URL / OPENIM_RELAY_TOKEN",
45
50
  help_env_relay_url: ` TESTNEXTIM_RELAY_URL 覆盖 relay 地址(默认: ${DEFAULT_RELAY_URL})`,
@@ -74,6 +79,14 @@ const CLI_I18N = {
74
79
  gateway_ready_hint: "请确认 OpenClaw daemon 正在运行,并且 responses endpoint 已启用。",
75
80
  pair_success: "配对已就绪,请回到 App 完成确认。",
76
81
  pair_worker_failed: "配对初始化失败,relay bridge worker 启动失败。",
82
+ service_install_failed: "后台服务安装失败,将回退到临时后台进程。",
83
+ service_installed: "后台服务已安装并启动。",
84
+ service_uninstalled: "后台服务已卸载。",
85
+ service_status_manager: "服务管理器: {value}",
86
+ service_status_installed: "服务已安装: {value}",
87
+ service_status_running: "服务运行中: {value}",
88
+ service_status_target: "服务目标: {value}",
89
+ service_status_logs: "日志目录: {value}",
77
90
  setup_updated: "配置已更新: {path}",
78
91
  setup_ready: "配置已准备好: {path}",
79
92
  setup_state_file: "状态文件: {path}",
@@ -115,8 +128,9 @@ const CLI_I18N = {
115
128
  help_environment: "Environment:",
116
129
  help_package: `Package: ${CLI_PACKAGE_NAME}`,
117
130
  help_tip: "Tip:",
118
- help_tip_pair: " The pair command auto-starts the relay bridge worker in detached mode.",
131
+ help_tip_pair: " The pair command auto-installs and starts a background service to keep the relay bridge online.",
119
132
  help_tip_no_serve: " Use --no-serve-relay if you do not want the bridge worker to start automatically.",
133
+ help_tip_service: ` Use \`${CLI_COMMAND_NAME} service status|install|uninstall|restart\` to manage the background service.`,
120
134
  help_env_home: " OPENCLAW_HOME Override OpenClaw home directory (default: ~/.openclaw)",
121
135
  help_env_openim: " OPENIM_RELAY_URL / OPENIM_RELAY_TOKEN",
122
136
  help_env_relay_url: ` TESTNEXTIM_RELAY_URL Override relay base URL (default: ${DEFAULT_RELAY_URL})`,
@@ -152,6 +166,14 @@ const CLI_I18N = {
152
166
  "Make sure OpenClaw daemon is running and the responses endpoint is enabled.",
153
167
  pair_success: "Pairing is ready. Return to the app to finish confirmation.",
154
168
  pair_worker_failed: "Pairing initialization failed because the relay bridge worker could not start.",
169
+ service_install_failed: "Background service install failed. Falling back to a temporary detached process.",
170
+ service_installed: "Background service installed and started.",
171
+ service_uninstalled: "Background service uninstalled.",
172
+ service_status_manager: "Service Manager: {value}",
173
+ service_status_installed: "Service Installed: {value}",
174
+ service_status_running: "Service Running: {value}",
175
+ service_status_target: "Service Target: {value}",
176
+ service_status_logs: "Log Directory: {value}",
155
177
  setup_updated: "Updated config: {path}",
156
178
  setup_ready: "Config already prepared: {path}",
157
179
  setup_state_file: "State file: {path}",
@@ -233,6 +255,7 @@ function printHelp() {
233
255
  ` ${CLI_COMMAND_NAME} pair <1234|123456|XXXX-XXXX> [--mode <relay>]`,
234
256
  ` ${CLI_COMMAND_NAME} reset [--relay <url>] [--gateway <url>] [--json]`,
235
257
  ` ${CLI_COMMAND_NAME} bridge [--relay <url>] [--gateway <url>] [--json] (Deprecated)`,
258
+ ` ${CLI_COMMAND_NAME} service <status|install|uninstall|restart> [--relay <url>] [--gateway <url>] [--json]`,
236
259
  ` ${CLI_COMMAND_NAME} pair-url [<1234|123456|XXXX-XXXX>] [--account <id>] [--code <1234|123456|XXXX-XXXX>] [--json]`,
237
260
  ` ${CLI_COMMAND_NAME} status [--json]`,
238
261
  "",
@@ -249,6 +272,7 @@ function printHelp() {
249
272
  t("help_tip"),
250
273
  t("help_tip_pair"),
251
274
  t("help_tip_no_serve"),
275
+ t("help_tip_service"),
252
276
  ].join("\n"),
253
277
  );
254
278
  }
@@ -273,6 +297,7 @@ function parseArgs(argv) {
273
297
  waitBind: false,
274
298
  waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
275
299
  serveRelay: true,
300
+ serviceAction: "status",
276
301
  mode: undefined,
277
302
  };
278
303
 
@@ -369,6 +394,14 @@ function parseArgs(argv) {
369
394
  fail(`unknown option: ${token}`);
370
395
  }
371
396
 
397
+ if (command === "service") {
398
+ if (!options.serviceAction || options.serviceAction === "status") {
399
+ options.serviceAction = token.trim().toLowerCase();
400
+ continue;
401
+ }
402
+ fail(`unexpected argument: ${token}`);
403
+ }
404
+
372
405
  if (command === "pair" || command === "pair-url") {
373
406
  if (!options.code) {
374
407
  options.code = token.trim();
@@ -399,6 +432,26 @@ function resolveTestNextIMStatePath() {
399
432
  return path.join(resolveOpenClawHome(), "testnextim.state.json");
400
433
  }
401
434
 
435
+ function resolveMatecliRuntimeDir() {
436
+ return path.join(resolveOpenClawHome(), "matecli");
437
+ }
438
+
439
+ function resolveBridgeLogsDir() {
440
+ return path.join(resolveMatecliRuntimeDir(), "logs");
441
+ }
442
+
443
+ function resolveBridgeLaunchScriptPath() {
444
+ return path.join(resolveMatecliRuntimeDir(), "bridge-launch.sh");
445
+ }
446
+
447
+ function resolveMacLaunchAgentPath() {
448
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
449
+ }
450
+
451
+ function resolveLinuxUserUnitPath() {
452
+ return path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT_NAME);
453
+ }
454
+
402
455
  function resolveBridgeLockPath({ relayUrl, gatewayId, gatewayBaseUrl }) {
403
456
  const digest = createHash("sha256")
404
457
  .update(
@@ -440,6 +493,39 @@ function readOptionalJsonFile(filePath) {
440
493
  }
441
494
  }
442
495
 
496
+ function readJsonFileIfExists(filePath) {
497
+ try {
498
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+
504
+ function commandToString(command, args) {
505
+ return [command, ...args]
506
+ .map((part) => {
507
+ const value = String(part);
508
+ return /[\s"]/u.test(value) ? `"${value.replaceAll("\"", "\\\"")}"` : value;
509
+ })
510
+ .join(" ");
511
+ }
512
+
513
+ function quoteWindowsCommandArg(value) {
514
+ const text = String(value ?? "");
515
+ if (!/[\s"]/u.test(text)) {
516
+ return text;
517
+ }
518
+ return `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
519
+ }
520
+
521
+ function runCommand(command, args, options = {}) {
522
+ return spawnSync(command, args, {
523
+ encoding: "utf8",
524
+ windowsHide: process.platform === "win32",
525
+ ...options,
526
+ });
527
+ }
528
+
443
529
  function isPidAlive(pid) {
444
530
  const normalized = Number(pid);
445
531
  if (!Number.isFinite(normalized) || normalized <= 0) {
@@ -774,6 +860,34 @@ function formatHostForUrl(host) {
774
860
  return value;
775
861
  }
776
862
 
863
+ function resolveGatewayHost(config) {
864
+ const fromEnv = String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim();
865
+ if (fromEnv) {
866
+ return fromEnv;
867
+ }
868
+
869
+ const gateway = ensureObject(config?.gateway);
870
+ const http = ensureObject(gateway.http);
871
+ const bindMode = String(gateway.bind ?? "").trim().toLowerCase();
872
+
873
+ const configuredHostCandidates = [
874
+ gateway.host,
875
+ gateway.hostname,
876
+ http.host,
877
+ http.hostname,
878
+ bindMode === "custom" ? gateway.customBindHost : "",
879
+ ];
880
+
881
+ for (const candidate of configuredHostCandidates) {
882
+ const value = String(candidate ?? "").trim();
883
+ if (value) {
884
+ return value;
885
+ }
886
+ }
887
+
888
+ return DEFAULT_GATEWAY_HOST;
889
+ }
890
+
777
891
  function resolveGatewayPort(config) {
778
892
  const fromEnv = parseGatewayPortEnvValue(process.env.OPENCLAW_GATEWAY_PORT);
779
893
  if (fromEnv != null) {
@@ -809,11 +923,7 @@ function resolveGatewayBaseUrl(options, config) {
809
923
  ].join("\n"),
810
924
  );
811
925
  }
812
- const bindMode = String(config?.gateway?.bind ?? "").trim().toLowerCase();
813
- const customBindHost = String(config?.gateway?.customBindHost ?? "").trim();
814
- const host =
815
- String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim() ||
816
- (bindMode === "custom" && customBindHost ? customBindHost : DEFAULT_GATEWAY_HOST);
926
+ const host = resolveGatewayHost(config);
817
927
  return `http://${formatHostForUrl(host)}:${gatewayPort}`;
818
928
  }
819
929
 
@@ -868,7 +978,7 @@ function extractErrorMessage(decoded, fallback) {
868
978
  return fallback;
869
979
  }
870
980
 
871
- async function fetchWithTimeout(url, options = {}, timeoutMs = 30000) {
981
+ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
872
982
  const controller = new AbortController();
873
983
  const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
874
984
  try {
@@ -1137,6 +1247,462 @@ function restartOpenClaw() {
1137
1247
  };
1138
1248
  }
1139
1249
 
1250
+ function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
1251
+ return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
1252
+ }
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
+
1375
+ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1376
+ if (!relayUrl) {
1377
+ fail("relay URL is required for bridge service");
1378
+ }
1379
+ if (!gatewayBaseUrl) {
1380
+ fail("gateway base URL is required for bridge service");
1381
+ }
1382
+ const logDir = resolveBridgeLogsDir();
1383
+ fs.mkdirSync(logDir, { recursive: true });
1384
+ return {
1385
+ relayUrl,
1386
+ gatewayBaseUrl,
1387
+ logDir,
1388
+ stdoutLog: path.join(logDir, "bridge.stdout.log"),
1389
+ stderrLog: path.join(logDir, "bridge.stderr.log"),
1390
+ nodePath: resolveBridgeServiceNodePath(),
1391
+ scriptPath: CLI_ENTRY,
1392
+ args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1393
+ };
1394
+ }
1395
+
1396
+ function escapeXmlText(value) {
1397
+ return String(value)
1398
+ .replaceAll("&", "&amp;")
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
+
1450
+ function installBridgeServiceMac(installConfig) {
1451
+ const plistPath = resolveMacLaunchAgentPath();
1452
+ const launchScriptPath = writeBridgeLaunchScript(installConfig);
1453
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
1454
+ const argsXml = [launchScriptPath]
1455
+ .map((arg) => ` <string>${escapeXmlText(arg)}</string>`)
1456
+ .join("\n");
1457
+ const plist = [
1458
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1459
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
1460
+ "<plist version=\"1.0\">",
1461
+ "<dict>",
1462
+ ` <key>Label</key><string>${SERVICE_LABEL}</string>`,
1463
+ " <key>ProgramArguments</key>",
1464
+ " <array>",
1465
+ argsXml,
1466
+ " </array>",
1467
+ " <key>RunAtLoad</key><true/>",
1468
+ " <key>KeepAlive</key><true/>",
1469
+ ` <key>WorkingDirectory</key><string>${resolveOpenClawHome()}</string>`,
1470
+ ` <key>StandardOutPath</key><string>${installConfig.stdoutLog}</string>`,
1471
+ ` <key>StandardErrorPath</key><string>${installConfig.stderrLog}</string>`,
1472
+ "</dict>",
1473
+ "</plist>",
1474
+ "",
1475
+ ].join("\n");
1476
+ fs.writeFileSync(plistPath, plist, "utf8");
1477
+
1478
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1479
+ const domains = uid == null ? [] : [`gui/${uid}`, `user/${uid}`];
1480
+ let selectedDomain = null;
1481
+ for (const domain of domains) {
1482
+ runCommand("launchctl", ["bootout", domain, plistPath]);
1483
+ const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
1484
+ if (bootstrap.status === 0) {
1485
+ selectedDomain = domain;
1486
+ runCommand("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`]);
1487
+ runCommand("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`]);
1488
+ break;
1489
+ }
1490
+ }
1491
+ if (!selectedDomain) {
1492
+ const fallback = runCommand("launchctl", ["load", "-w", plistPath]);
1493
+ if (fallback.status !== 0) {
1494
+ throw new Error((fallback.stderr || fallback.stdout || "launchctl bootstrap failed").trim());
1495
+ }
1496
+ selectedDomain = "launchd";
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
+ }
1505
+ return {
1506
+ manager: "launchd",
1507
+ installed: true,
1508
+ running: true,
1509
+ target: plistPath,
1510
+ logDir: installConfig.logDir,
1511
+ ref: `${selectedDomain}/${SERVICE_LABEL}`,
1512
+ };
1513
+ }
1514
+
1515
+ function installBridgeServiceLinux(installConfig) {
1516
+ const unitPath = resolveLinuxUserUnitPath();
1517
+ fs.mkdirSync(path.dirname(unitPath), { recursive: true });
1518
+ const execStart = commandToString(installConfig.nodePath, installConfig.args);
1519
+ const unit = [
1520
+ "[Unit]",
1521
+ "Description=Matelink relay bridge",
1522
+ "After=network-online.target",
1523
+ "Wants=network-online.target",
1524
+ "",
1525
+ "[Service]",
1526
+ "Type=simple",
1527
+ `WorkingDirectory=${resolveOpenClawHome()}`,
1528
+ `ExecStart=${execStart}`,
1529
+ "Restart=always",
1530
+ "RestartSec=5",
1531
+ `StandardOutput=append:${installConfig.stdoutLog}`,
1532
+ `StandardError=append:${installConfig.stderrLog}`,
1533
+ "",
1534
+ "[Install]",
1535
+ "WantedBy=default.target",
1536
+ "",
1537
+ ].join("\n");
1538
+ fs.writeFileSync(unitPath, unit, "utf8");
1539
+ const commands = [
1540
+ ["systemctl", ["--user", "daemon-reload"]],
1541
+ ["systemctl", ["--user", "enable", "--now", SERVICE_UNIT_NAME]],
1542
+ ];
1543
+ for (const [command, args] of commands) {
1544
+ const result = runCommand(command, args);
1545
+ if (result.status !== 0) {
1546
+ throw new Error((result.stderr || result.stdout || `${command} failed`).trim());
1547
+ }
1548
+ }
1549
+ return {
1550
+ manager: "systemd-user",
1551
+ installed: true,
1552
+ running: true,
1553
+ target: unitPath,
1554
+ logDir: installConfig.logDir,
1555
+ ref: SERVICE_UNIT_NAME,
1556
+ };
1557
+ }
1558
+
1559
+ function installBridgeServiceWindows(installConfig) {
1560
+ const commandLine = installConfig.args.map(quoteWindowsCommandArg).join(" ");
1561
+ const script = [
1562
+ `$Action = New-ScheduledTaskAction -Execute '${installConfig.nodePath.replaceAll("'", "''")}' -Argument '${commandLine.replaceAll("'", "''")}'`,
1563
+ "$Trigger = New-ScheduledTaskTrigger -AtLogOn",
1564
+ "$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)",
1565
+ `Register-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Action $Action -Trigger $Trigger -Settings $Settings -Description 'Matelink relay bridge' -Force | Out-Null`,
1566
+ `Start-ScheduledTask -TaskName '${SERVICE_TASK_NAME}'`,
1567
+ ].join("; ");
1568
+ const result = runCommand("powershell.exe", ["-NoProfile", "-Command", script]);
1569
+ if (result.status !== 0) {
1570
+ throw new Error((result.stderr || result.stdout || "scheduled task install failed").trim());
1571
+ }
1572
+ return {
1573
+ manager: "task-scheduler",
1574
+ installed: true,
1575
+ running: true,
1576
+ target: SERVICE_TASK_NAME,
1577
+ logDir: installConfig.logDir,
1578
+ ref: SERVICE_TASK_NAME,
1579
+ };
1580
+ }
1581
+
1582
+ function installBridgeService({ relayUrl, gatewayBaseUrl }) {
1583
+ const installConfig = ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl });
1584
+ if (process.platform === "darwin") {
1585
+ return installBridgeServiceMac(installConfig);
1586
+ }
1587
+ if (process.platform === "linux") {
1588
+ return installBridgeServiceLinux(installConfig);
1589
+ }
1590
+ if (process.platform === "win32") {
1591
+ return installBridgeServiceWindows(installConfig);
1592
+ }
1593
+ throw new Error(`unsupported platform for service install: ${process.platform}`);
1594
+ }
1595
+
1596
+ function uninstallBridgeService() {
1597
+ if (process.platform === "darwin") {
1598
+ const plistPath = resolveMacLaunchAgentPath();
1599
+ const launchScriptPath = resolveBridgeLaunchScriptPath();
1600
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1601
+ if (uid != null) {
1602
+ runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
1603
+ runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
1604
+ }
1605
+ try {
1606
+ fs.unlinkSync(plistPath);
1607
+ } catch {}
1608
+ try {
1609
+ fs.unlinkSync(launchScriptPath);
1610
+ } catch {}
1611
+ return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
1612
+ }
1613
+ if (process.platform === "linux") {
1614
+ const unitPath = resolveLinuxUserUnitPath();
1615
+ runCommand("systemctl", ["--user", "disable", "--now", SERVICE_UNIT_NAME]);
1616
+ try {
1617
+ fs.unlinkSync(unitPath);
1618
+ } catch {}
1619
+ runCommand("systemctl", ["--user", "daemon-reload"]);
1620
+ return { manager: "systemd-user", installed: false, running: false, target: unitPath, logDir: resolveBridgeLogsDir() };
1621
+ }
1622
+ if (process.platform === "win32") {
1623
+ runCommand("powershell.exe", ["-NoProfile", "-Command", `Unregister-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Confirm:$false -ErrorAction SilentlyContinue`]);
1624
+ return { manager: "task-scheduler", installed: false, running: false, target: SERVICE_TASK_NAME, logDir: resolveBridgeLogsDir() };
1625
+ }
1626
+ throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
1627
+ }
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
+
1669
+ function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
1670
+ stopBridgeService();
1671
+ return installBridgeService({ relayUrl, gatewayBaseUrl });
1672
+ }
1673
+
1674
+ function getBridgeServiceStatus() {
1675
+ if (process.platform === "darwin") {
1676
+ const plistPath = resolveMacLaunchAgentPath();
1677
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1678
+ return readMacLaunchdServiceStatus(uid, plistPath);
1679
+ }
1680
+ if (process.platform === "linux") {
1681
+ const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
1682
+ const active = runCommand("systemctl", ["--user", "is-active", SERVICE_UNIT_NAME]);
1683
+ return {
1684
+ manager: "systemd-user",
1685
+ installed: enabled.status === 0 || fs.existsSync(resolveLinuxUserUnitPath()),
1686
+ running: active.status === 0,
1687
+ target: resolveLinuxUserUnitPath(),
1688
+ logDir: resolveBridgeLogsDir(),
1689
+ ref: SERVICE_UNIT_NAME,
1690
+ };
1691
+ }
1692
+ if (process.platform === "win32") {
1693
+ const result = runCommand("powershell.exe", ["-NoProfile", "-Command", `Get-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' | Select-Object -ExpandProperty State`]);
1694
+ return {
1695
+ manager: "task-scheduler",
1696
+ installed: result.status === 0,
1697
+ running: result.status === 0 && String(result.stdout ?? "").trim().toLowerCase() === "ready",
1698
+ target: SERVICE_TASK_NAME,
1699
+ logDir: resolveBridgeLogsDir(),
1700
+ ref: SERVICE_TASK_NAME,
1701
+ };
1702
+ }
1703
+ return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir(), ref: null };
1704
+ }
1705
+
1140
1706
  function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1141
1707
  let result;
1142
1708
  if (process.platform === "win32") {
@@ -1173,7 +1739,11 @@ function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1173
1739
  (entry) =>
1174
1740
  Number.isFinite(entry.pid) &&
1175
1741
  entry.pid !== process.pid &&
1176
- entry.command.includes("testnextim") &&
1742
+ (
1743
+ entry.command.includes(CLI_COMMAND_NAME) ||
1744
+ entry.command.includes(CLI_PACKAGE_NAME) ||
1745
+ entry.command.includes(path.basename(CLI_ENTRY))
1746
+ ) &&
1177
1747
  entry.command.includes("bridge") &&
1178
1748
  entry.command.includes(relayUrl) &&
1179
1749
  entry.command.includes(gatewayBaseUrl),
@@ -1195,6 +1765,29 @@ function stopBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1195
1765
  };
1196
1766
  }
1197
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
+
1198
1791
  function resolvePublishedGatewayBaseUrl({
1199
1792
  relayUrl,
1200
1793
  bindPublicBaseUrl,
@@ -1644,7 +2237,7 @@ async function publishRelayGatewayEvent({
1644
2237
  ...relayGatewayHeaders(gatewayToken),
1645
2238
  },
1646
2239
  body: JSON.stringify(payload),
1647
- }, 20000);
2240
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
1648
2241
 
1649
2242
  const text = await response.text();
1650
2243
  if (!response.ok) {
@@ -1671,7 +2264,7 @@ async function publishRelayGatewayChatEvent({
1671
2264
  ...relayGatewayHeaders(gatewayToken),
1672
2265
  },
1673
2266
  body: JSON.stringify(payload ?? {}),
1674
- }, 20000);
2267
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
1675
2268
 
1676
2269
  const text = await response.text();
1677
2270
  if (!response.ok) {
@@ -2071,7 +2664,7 @@ function createGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken }) {
2071
2664
  const timer = setTimeout(() => {
2072
2665
  pending.delete(id);
2073
2666
  reject(new Error(`gateway rpc timeout: ${method}`));
2074
- }, 30000);
2667
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
2075
2668
  pending.set(id, {
2076
2669
  resolve: (v) => { clearTimeout(timer); resolve(v); },
2077
2670
  reject: (e) => { clearTimeout(timer); reject(e); },
@@ -2488,7 +3081,7 @@ async function runRelayBridge({
2488
3081
  relayUrl,
2489
3082
  gatewayId,
2490
3083
  gatewayToken,
2491
- waitSeconds: 30,
3084
+ waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
2492
3085
  });
2493
3086
  if (!request) {
2494
3087
  continue;
@@ -2571,6 +3164,18 @@ function startRelayBridgeDetached({
2571
3164
  return { started: true, alreadyRunning: false, mode: "detached" };
2572
3165
  }
2573
3166
 
3167
+ function formatServiceStatusOutput(result) {
3168
+ return [
3169
+ t("service_status_manager", { value: result.manager }),
3170
+ t("service_status_installed", { value: result.installed ? t("yes") : t("no") }),
3171
+ t("service_status_running", { value: result.running ? t("yes") : t("no") }),
3172
+ result.target ? t("service_status_target", { value: result.target }) : null,
3173
+ result.logDir ? t("service_status_logs", { value: result.logDir }) : null,
3174
+ ]
3175
+ .filter(Boolean)
3176
+ .join("\n");
3177
+ }
3178
+
2574
3179
  async function runPair({
2575
3180
  json,
2576
3181
  account,
@@ -2804,22 +3409,55 @@ async function runPair({
2804
3409
  publishedGatewayBaseUrl,
2805
3410
  gatewayRestarted: Boolean(restartResult?.ok),
2806
3411
  relayGatewayId: relayCredentials?.gatewayId ?? null,
3412
+ bridgeCleanup: null,
2807
3413
  };
2808
3414
 
2809
3415
  if (serveRelay && !json) {
3416
+ const bridgeCleanup = clearExistingBridgeRuntime({
3417
+ relayUrl,
3418
+ gatewayBaseUrl: localGatewayBaseUrl,
3419
+ });
3420
+ output.bridgeCleanup = {
3421
+ serviceInstalled: bridgeCleanup.serviceStatus?.installed === true,
3422
+ serviceRunning: bridgeCleanup.serviceStatus?.running === true,
3423
+ serviceStopError: bridgeCleanup.serviceStopped?.error ?? null,
3424
+ stoppedProcessCount: bridgeCleanup.stoppedProcesses?.count ?? 0,
3425
+ stoppedPids: bridgeCleanup.stoppedProcesses?.pids ?? [],
3426
+ };
2810
3427
  try {
2811
- const bridgeResult = startRelayBridgeDetached({
3428
+ const serviceStatus = getBridgeServiceStatus();
3429
+ const serviceResult = (serviceStatus.installed ? restartBridgeService : installBridgeService)({
2812
3430
  relayUrl,
2813
3431
  gatewayBaseUrl: localGatewayBaseUrl,
2814
3432
  });
2815
- output.relayWorkerResult = bridgeResult;
2816
- } catch (error) {
2817
3433
  output.relayWorkerResult = {
2818
- mode: "detached",
2819
- started: false,
2820
- error: String(error instanceof Error ? error.message : error),
3434
+ ...serviceResult,
3435
+ started: true,
3436
+ alreadyRunning: serviceResult.running,
3437
+ mode: "service",
2821
3438
  };
2822
- console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
3439
+ console.log(t("service_installed"));
3440
+ } catch (error) {
3441
+ const serviceError = String(error instanceof Error ? error.message : error);
3442
+ console.warn(t("service_install_failed"));
3443
+ console.warn(serviceError);
3444
+ try {
3445
+ const bridgeResult = startRelayBridgeDetached({
3446
+ relayUrl,
3447
+ gatewayBaseUrl: localGatewayBaseUrl,
3448
+ });
3449
+ output.relayWorkerResult = {
3450
+ ...bridgeResult,
3451
+ error: serviceError,
3452
+ };
3453
+ } catch (fallbackError) {
3454
+ output.relayWorkerResult = {
3455
+ mode: "detached",
3456
+ started: false,
3457
+ error: String(fallbackError instanceof Error ? fallbackError.message : fallbackError),
3458
+ };
3459
+ console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
3460
+ }
2823
3461
  }
2824
3462
  }
2825
3463
 
@@ -2862,6 +3500,14 @@ async function runReset({ json, relay, noRelay, gateway }) {
2862
3500
  });
2863
3501
  }
2864
3502
 
3503
+ let serviceReset = null;
3504
+ if (relayUrl && gatewayBaseUrl) {
3505
+ try {
3506
+ serviceReset = uninstallBridgeService();
3507
+ } catch (error) {
3508
+ serviceReset = { error: String(error instanceof Error ? error.message : error) };
3509
+ }
3510
+ }
2865
3511
  const stoppedBridges =
2866
3512
  relayUrl && gatewayBaseUrl
2867
3513
  ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
@@ -2883,6 +3529,7 @@ async function runReset({ json, relay, noRelay, gateway }) {
2883
3529
  clearedCodes: relayResetResult.clearedCodes,
2884
3530
  },
2885
3531
  stoppedBridgeCount: stoppedBridges.count,
3532
+ serviceReset,
2886
3533
  nextGatewayId: nextState.state.relayGatewayId ?? null,
2887
3534
  statePath: nextState.statePath,
2888
3535
  };
@@ -2917,6 +3564,61 @@ async function runReset({ json, relay, noRelay, gateway }) {
2917
3564
  console.log(t("reset_ready"));
2918
3565
  }
2919
3566
 
3567
+ async function runService({ json, relay, noRelay, gateway, serviceAction }) {
3568
+ const configPath = resolveOpenClawConfigPath();
3569
+ const action = String(serviceAction ?? "status").trim().toLowerCase() || "status";
3570
+ const config = readJsonFileIfExists(configPath);
3571
+ const section = config ? readChannelSection(config) : {};
3572
+ const relayUrl =
3573
+ action === "install" || action === "restart"
3574
+ ? resolveRelayUrl({ relay, noRelay }, section)
3575
+ : normalizeBaseUrl(relay) || (config ? resolveRelayUrl({ relay: null, noRelay }, section) : null);
3576
+ const gatewayBaseUrl =
3577
+ action === "install" || action === "restart"
3578
+ ? resolveGatewayBaseUrl({ gateway }, config)
3579
+ : normalizeBaseUrl(gateway) || (config ? resolveGatewayBaseUrl({ gateway: null }, config) : null);
3580
+
3581
+ let result;
3582
+ switch (action) {
3583
+ case "status":
3584
+ result = getBridgeServiceStatus();
3585
+ break;
3586
+ case "install":
3587
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3588
+ result = installBridgeService({ relayUrl, gatewayBaseUrl });
3589
+ break;
3590
+ case "restart":
3591
+ clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
3592
+ result = restartBridgeService({ relayUrl, gatewayBaseUrl });
3593
+ break;
3594
+ case "uninstall":
3595
+ result = uninstallBridgeService();
3596
+ break;
3597
+ default:
3598
+ fail(`unknown service action: ${action}`);
3599
+ }
3600
+
3601
+ const output = {
3602
+ command: "service",
3603
+ action,
3604
+ relayUrl: relayUrl ?? null,
3605
+ gatewayBaseUrl: gatewayBaseUrl ?? null,
3606
+ ...result,
3607
+ };
3608
+
3609
+ if (json) {
3610
+ console.log(JSON.stringify(output, null, 2));
3611
+ return;
3612
+ }
3613
+
3614
+ if (action === "uninstall") {
3615
+ console.log(t("service_uninstalled"));
3616
+ } else if (action === "install" || action === "restart") {
3617
+ console.log(t("service_installed"));
3618
+ }
3619
+ console.log(formatServiceStatusOutput(output));
3620
+ }
3621
+
2920
3622
  function runSetup({
2921
3623
  json,
2922
3624
  account,
@@ -3192,6 +3894,7 @@ function runStatus({ json }) {
3192
3894
  const config = readJsonFile(configPath);
3193
3895
  const section = readChannelSection(config);
3194
3896
  const creds = resolveRelayCredentialsFromSection(section);
3897
+ const service = getBridgeServiceStatus();
3195
3898
 
3196
3899
  const status = {
3197
3900
  configPath,
@@ -3213,6 +3916,10 @@ function runStatus({ json }) {
3213
3916
  relayGatewayId: creds.gatewayId || null,
3214
3917
  hasRelayClientToken: Boolean(creds.clientToken),
3215
3918
  hasRelayGatewayToken: Boolean(creds.gatewayToken),
3919
+ serviceManager: service.manager,
3920
+ serviceInstalled: service.installed,
3921
+ serviceRunning: service.running,
3922
+ serviceTarget: service.target,
3216
3923
  linkedUserId:
3217
3924
  typeof section?.linkedUserId === "string" && section.linkedUserId.trim()
3218
3925
  ? section.linkedUserId.trim()
@@ -3254,6 +3961,20 @@ function runStatus({ json }) {
3254
3961
  value: status.hasAccessToken ? t("set") : t("missing"),
3255
3962
  }),
3256
3963
  );
3964
+ console.log(t("service_status_manager", { value: status.serviceManager }));
3965
+ console.log(
3966
+ t("service_status_installed", {
3967
+ value: status.serviceInstalled ? t("yes") : t("no"),
3968
+ }),
3969
+ );
3970
+ console.log(
3971
+ t("service_status_running", {
3972
+ value: status.serviceRunning ? t("yes") : t("no"),
3973
+ }),
3974
+ );
3975
+ if (status.serviceTarget) {
3976
+ console.log(t("service_status_target", { value: status.serviceTarget }));
3977
+ }
3257
3978
  console.log(t("status_linked_user", { value: status.linkedUserId ?? t("not_linked") }));
3258
3979
  }
3259
3980
 
@@ -3273,6 +3994,9 @@ async function main() {
3273
3994
  case "bridge":
3274
3995
  await runBridge(options);
3275
3996
  return;
3997
+ case "service":
3998
+ await runService(options);
3999
+ return;
3276
4000
  case "pair-url":
3277
4001
  runPairUrl(options);
3278
4002
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.10",
3
+ "version": "2026.4.12",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",