@matelink/cli 2026.4.9 → 2026.4.11

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,22 @@ 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 resolveMacLaunchAgentPath() {
444
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
445
+ }
446
+
447
+ function resolveLinuxUserUnitPath() {
448
+ return path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT_NAME);
449
+ }
450
+
402
451
  function resolveBridgeLockPath({ relayUrl, gatewayId, gatewayBaseUrl }) {
403
452
  const digest = createHash("sha256")
404
453
  .update(
@@ -440,6 +489,39 @@ function readOptionalJsonFile(filePath) {
440
489
  }
441
490
  }
442
491
 
492
+ function readJsonFileIfExists(filePath) {
493
+ try {
494
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+
500
+ function commandToString(command, args) {
501
+ return [command, ...args]
502
+ .map((part) => {
503
+ const value = String(part);
504
+ return /[\s"]/u.test(value) ? `"${value.replaceAll("\"", "\\\"")}"` : value;
505
+ })
506
+ .join(" ");
507
+ }
508
+
509
+ function quoteWindowsCommandArg(value) {
510
+ const text = String(value ?? "");
511
+ if (!/[\s"]/u.test(text)) {
512
+ return text;
513
+ }
514
+ return `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
515
+ }
516
+
517
+ function runCommand(command, args, options = {}) {
518
+ return spawnSync(command, args, {
519
+ encoding: "utf8",
520
+ windowsHide: process.platform === "win32",
521
+ ...options,
522
+ });
523
+ }
524
+
443
525
  function isPidAlive(pid) {
444
526
  const normalized = Number(pid);
445
527
  if (!Number.isFinite(normalized) || normalized <= 0) {
@@ -774,6 +856,34 @@ function formatHostForUrl(host) {
774
856
  return value;
775
857
  }
776
858
 
859
+ function resolveGatewayHost(config) {
860
+ const fromEnv = String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim();
861
+ if (fromEnv) {
862
+ return fromEnv;
863
+ }
864
+
865
+ const gateway = ensureObject(config?.gateway);
866
+ const http = ensureObject(gateway.http);
867
+ const bindMode = String(gateway.bind ?? "").trim().toLowerCase();
868
+
869
+ const configuredHostCandidates = [
870
+ gateway.host,
871
+ gateway.hostname,
872
+ http.host,
873
+ http.hostname,
874
+ bindMode === "custom" ? gateway.customBindHost : "",
875
+ ];
876
+
877
+ for (const candidate of configuredHostCandidates) {
878
+ const value = String(candidate ?? "").trim();
879
+ if (value) {
880
+ return value;
881
+ }
882
+ }
883
+
884
+ return DEFAULT_GATEWAY_HOST;
885
+ }
886
+
777
887
  function resolveGatewayPort(config) {
778
888
  const fromEnv = parseGatewayPortEnvValue(process.env.OPENCLAW_GATEWAY_PORT);
779
889
  if (fromEnv != null) {
@@ -809,11 +919,7 @@ function resolveGatewayBaseUrl(options, config) {
809
919
  ].join("\n"),
810
920
  );
811
921
  }
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);
922
+ const host = resolveGatewayHost(config);
817
923
  return `http://${formatHostForUrl(host)}:${gatewayPort}`;
818
924
  }
819
925
 
@@ -868,7 +974,7 @@ function extractErrorMessage(decoded, fallback) {
868
974
  return fallback;
869
975
  }
870
976
 
871
- async function fetchWithTimeout(url, options = {}, timeoutMs = 30000) {
977
+ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
872
978
  const controller = new AbortController();
873
979
  const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
874
980
  try {
@@ -1137,6 +1243,242 @@ function restartOpenClaw() {
1137
1243
  };
1138
1244
  }
1139
1245
 
1246
+ function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
1247
+ return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
1248
+ }
1249
+
1250
+ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1251
+ if (!relayUrl) {
1252
+ fail("relay URL is required for bridge service");
1253
+ }
1254
+ if (!gatewayBaseUrl) {
1255
+ fail("gateway base URL is required for bridge service");
1256
+ }
1257
+ const logDir = resolveBridgeLogsDir();
1258
+ fs.mkdirSync(logDir, { recursive: true });
1259
+ return {
1260
+ relayUrl,
1261
+ gatewayBaseUrl,
1262
+ logDir,
1263
+ stdoutLog: path.join(logDir, "bridge.stdout.log"),
1264
+ stderrLog: path.join(logDir, "bridge.stderr.log"),
1265
+ nodePath: process.execPath,
1266
+ scriptPath: CLI_ENTRY,
1267
+ args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1268
+ };
1269
+ }
1270
+
1271
+ function installBridgeServiceMac(installConfig) {
1272
+ const plistPath = resolveMacLaunchAgentPath();
1273
+ 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>`)
1276
+ .join("\n");
1277
+ const plist = [
1278
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1279
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
1280
+ "<plist version=\"1.0\">",
1281
+ "<dict>",
1282
+ ` <key>Label</key><string>${SERVICE_LABEL}</string>`,
1283
+ " <key>ProgramArguments</key>",
1284
+ " <array>",
1285
+ argsXml,
1286
+ " </array>",
1287
+ " <key>RunAtLoad</key><true/>",
1288
+ " <key>KeepAlive</key><true/>",
1289
+ ` <key>WorkingDirectory</key><string>${resolveOpenClawHome()}</string>`,
1290
+ ` <key>StandardOutPath</key><string>${installConfig.stdoutLog}</string>`,
1291
+ ` <key>StandardErrorPath</key><string>${installConfig.stderrLog}</string>`,
1292
+ "</dict>",
1293
+ "</plist>",
1294
+ "",
1295
+ ].join("\n");
1296
+ fs.writeFileSync(plistPath, plist, "utf8");
1297
+
1298
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1299
+ const domains = uid == null ? [] : [`gui/${uid}`, `user/${uid}`];
1300
+ let selectedDomain = null;
1301
+ for (const domain of domains) {
1302
+ runCommand("launchctl", ["bootout", domain, plistPath]);
1303
+ const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
1304
+ if (bootstrap.status === 0) {
1305
+ selectedDomain = domain;
1306
+ runCommand("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`]);
1307
+ runCommand("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`]);
1308
+ break;
1309
+ }
1310
+ }
1311
+ if (!selectedDomain) {
1312
+ const fallback = runCommand("launchctl", ["load", "-w", plistPath]);
1313
+ if (fallback.status !== 0) {
1314
+ throw new Error((fallback.stderr || fallback.stdout || "launchctl bootstrap failed").trim());
1315
+ }
1316
+ selectedDomain = "launchd";
1317
+ }
1318
+ return {
1319
+ manager: "launchd",
1320
+ installed: true,
1321
+ running: true,
1322
+ target: plistPath,
1323
+ logDir: installConfig.logDir,
1324
+ ref: `${selectedDomain}/${SERVICE_LABEL}`,
1325
+ };
1326
+ }
1327
+
1328
+ function installBridgeServiceLinux(installConfig) {
1329
+ const unitPath = resolveLinuxUserUnitPath();
1330
+ fs.mkdirSync(path.dirname(unitPath), { recursive: true });
1331
+ const execStart = commandToString(installConfig.nodePath, installConfig.args);
1332
+ const unit = [
1333
+ "[Unit]",
1334
+ "Description=Matelink relay bridge",
1335
+ "After=network-online.target",
1336
+ "Wants=network-online.target",
1337
+ "",
1338
+ "[Service]",
1339
+ "Type=simple",
1340
+ `WorkingDirectory=${resolveOpenClawHome()}`,
1341
+ `ExecStart=${execStart}`,
1342
+ "Restart=always",
1343
+ "RestartSec=5",
1344
+ `StandardOutput=append:${installConfig.stdoutLog}`,
1345
+ `StandardError=append:${installConfig.stderrLog}`,
1346
+ "",
1347
+ "[Install]",
1348
+ "WantedBy=default.target",
1349
+ "",
1350
+ ].join("\n");
1351
+ fs.writeFileSync(unitPath, unit, "utf8");
1352
+ const commands = [
1353
+ ["systemctl", ["--user", "daemon-reload"]],
1354
+ ["systemctl", ["--user", "enable", "--now", SERVICE_UNIT_NAME]],
1355
+ ];
1356
+ for (const [command, args] of commands) {
1357
+ const result = runCommand(command, args);
1358
+ if (result.status !== 0) {
1359
+ throw new Error((result.stderr || result.stdout || `${command} failed`).trim());
1360
+ }
1361
+ }
1362
+ return {
1363
+ manager: "systemd-user",
1364
+ installed: true,
1365
+ running: true,
1366
+ target: unitPath,
1367
+ logDir: installConfig.logDir,
1368
+ ref: SERVICE_UNIT_NAME,
1369
+ };
1370
+ }
1371
+
1372
+ function installBridgeServiceWindows(installConfig) {
1373
+ const commandLine = installConfig.args.map(quoteWindowsCommandArg).join(" ");
1374
+ const script = [
1375
+ `$Action = New-ScheduledTaskAction -Execute '${installConfig.nodePath.replaceAll("'", "''")}' -Argument '${commandLine.replaceAll("'", "''")}'`,
1376
+ "$Trigger = New-ScheduledTaskTrigger -AtLogOn",
1377
+ "$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)",
1378
+ `Register-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Action $Action -Trigger $Trigger -Settings $Settings -Description 'Matelink relay bridge' -Force | Out-Null`,
1379
+ `Start-ScheduledTask -TaskName '${SERVICE_TASK_NAME}'`,
1380
+ ].join("; ");
1381
+ const result = runCommand("powershell.exe", ["-NoProfile", "-Command", script]);
1382
+ if (result.status !== 0) {
1383
+ throw new Error((result.stderr || result.stdout || "scheduled task install failed").trim());
1384
+ }
1385
+ return {
1386
+ manager: "task-scheduler",
1387
+ installed: true,
1388
+ running: true,
1389
+ target: SERVICE_TASK_NAME,
1390
+ logDir: installConfig.logDir,
1391
+ ref: SERVICE_TASK_NAME,
1392
+ };
1393
+ }
1394
+
1395
+ function installBridgeService({ relayUrl, gatewayBaseUrl }) {
1396
+ const installConfig = ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl });
1397
+ if (process.platform === "darwin") {
1398
+ return installBridgeServiceMac(installConfig);
1399
+ }
1400
+ if (process.platform === "linux") {
1401
+ return installBridgeServiceLinux(installConfig);
1402
+ }
1403
+ if (process.platform === "win32") {
1404
+ return installBridgeServiceWindows(installConfig);
1405
+ }
1406
+ throw new Error(`unsupported platform for service install: ${process.platform}`);
1407
+ }
1408
+
1409
+ function uninstallBridgeService() {
1410
+ if (process.platform === "darwin") {
1411
+ const plistPath = resolveMacLaunchAgentPath();
1412
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
1413
+ if (uid != null) {
1414
+ runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
1415
+ runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
1416
+ }
1417
+ try {
1418
+ fs.unlinkSync(plistPath);
1419
+ } catch {}
1420
+ return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
1421
+ }
1422
+ if (process.platform === "linux") {
1423
+ const unitPath = resolveLinuxUserUnitPath();
1424
+ runCommand("systemctl", ["--user", "disable", "--now", SERVICE_UNIT_NAME]);
1425
+ try {
1426
+ fs.unlinkSync(unitPath);
1427
+ } catch {}
1428
+ runCommand("systemctl", ["--user", "daemon-reload"]);
1429
+ return { manager: "systemd-user", installed: false, running: false, target: unitPath, logDir: resolveBridgeLogsDir() };
1430
+ }
1431
+ if (process.platform === "win32") {
1432
+ runCommand("powershell.exe", ["-NoProfile", "-Command", `Unregister-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Confirm:$false -ErrorAction SilentlyContinue`]);
1433
+ return { manager: "task-scheduler", installed: false, running: false, target: SERVICE_TASK_NAME, logDir: resolveBridgeLogsDir() };
1434
+ }
1435
+ throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
1436
+ }
1437
+
1438
+ function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
1439
+ const installed = installBridgeService({ relayUrl, gatewayBaseUrl });
1440
+ return installed;
1441
+ }
1442
+
1443
+ function getBridgeServiceStatus() {
1444
+ if (process.platform === "darwin") {
1445
+ const plistPath = resolveMacLaunchAgentPath();
1446
+ 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 };
1455
+ }
1456
+ if (process.platform === "linux") {
1457
+ const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
1458
+ const active = runCommand("systemctl", ["--user", "is-active", SERVICE_UNIT_NAME]);
1459
+ return {
1460
+ manager: "systemd-user",
1461
+ installed: enabled.status === 0 || fs.existsSync(resolveLinuxUserUnitPath()),
1462
+ running: active.status === 0,
1463
+ target: resolveLinuxUserUnitPath(),
1464
+ logDir: resolveBridgeLogsDir(),
1465
+ ref: SERVICE_UNIT_NAME,
1466
+ };
1467
+ }
1468
+ if (process.platform === "win32") {
1469
+ const result = runCommand("powershell.exe", ["-NoProfile", "-Command", `Get-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' | Select-Object -ExpandProperty State`]);
1470
+ return {
1471
+ manager: "task-scheduler",
1472
+ installed: result.status === 0,
1473
+ running: result.status === 0 && String(result.stdout ?? "").trim().toLowerCase() === "ready",
1474
+ target: SERVICE_TASK_NAME,
1475
+ logDir: resolveBridgeLogsDir(),
1476
+ ref: SERVICE_TASK_NAME,
1477
+ };
1478
+ }
1479
+ return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir(), ref: null };
1480
+ }
1481
+
1140
1482
  function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1141
1483
  let result;
1142
1484
  if (process.platform === "win32") {
@@ -1173,7 +1515,11 @@ function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
1173
1515
  (entry) =>
1174
1516
  Number.isFinite(entry.pid) &&
1175
1517
  entry.pid !== process.pid &&
1176
- entry.command.includes("testnextim") &&
1518
+ (
1519
+ entry.command.includes(CLI_COMMAND_NAME) ||
1520
+ entry.command.includes(CLI_PACKAGE_NAME) ||
1521
+ entry.command.includes(path.basename(CLI_ENTRY))
1522
+ ) &&
1177
1523
  entry.command.includes("bridge") &&
1178
1524
  entry.command.includes(relayUrl) &&
1179
1525
  entry.command.includes(gatewayBaseUrl),
@@ -1644,7 +1990,7 @@ async function publishRelayGatewayEvent({
1644
1990
  ...relayGatewayHeaders(gatewayToken),
1645
1991
  },
1646
1992
  body: JSON.stringify(payload),
1647
- }, 20000);
1993
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
1648
1994
 
1649
1995
  const text = await response.text();
1650
1996
  if (!response.ok) {
@@ -1671,7 +2017,7 @@ async function publishRelayGatewayChatEvent({
1671
2017
  ...relayGatewayHeaders(gatewayToken),
1672
2018
  },
1673
2019
  body: JSON.stringify(payload ?? {}),
1674
- }, 20000);
2020
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
1675
2021
 
1676
2022
  const text = await response.text();
1677
2023
  if (!response.ok) {
@@ -2071,7 +2417,7 @@ function createGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken }) {
2071
2417
  const timer = setTimeout(() => {
2072
2418
  pending.delete(id);
2073
2419
  reject(new Error(`gateway rpc timeout: ${method}`));
2074
- }, 30000);
2420
+ }, DEFAULT_NETWORK_TIMEOUT_MS);
2075
2421
  pending.set(id, {
2076
2422
  resolve: (v) => { clearTimeout(timer); resolve(v); },
2077
2423
  reject: (e) => { clearTimeout(timer); reject(e); },
@@ -2109,7 +2455,7 @@ async function callGatewayRpcLocal({
2109
2455
  if (method === "__gateway.features") {
2110
2456
  return {
2111
2457
  ok: true,
2112
- methods: ["sessions.list", "sessions.abort", "sessions.usage", "sessions.patch",
2458
+ methods: ["sessions.list", "sessions.abort", "sessions.delete", "sessions.usage", "sessions.patch",
2113
2459
  "usage.cost", "agents.files.list", "agents.files.get", "agents.files.set",
2114
2460
  "skills.status", "models.list", "chat.history", "chat.abort",
2115
2461
  "config.get", "config.patch"],
@@ -2488,7 +2834,7 @@ async function runRelayBridge({
2488
2834
  relayUrl,
2489
2835
  gatewayId,
2490
2836
  gatewayToken,
2491
- waitSeconds: 30,
2837
+ waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
2492
2838
  });
2493
2839
  if (!request) {
2494
2840
  continue;
@@ -2571,6 +2917,18 @@ function startRelayBridgeDetached({
2571
2917
  return { started: true, alreadyRunning: false, mode: "detached" };
2572
2918
  }
2573
2919
 
2920
+ function formatServiceStatusOutput(result) {
2921
+ return [
2922
+ t("service_status_manager", { value: result.manager }),
2923
+ t("service_status_installed", { value: result.installed ? t("yes") : t("no") }),
2924
+ t("service_status_running", { value: result.running ? t("yes") : t("no") }),
2925
+ result.target ? t("service_status_target", { value: result.target }) : null,
2926
+ result.logDir ? t("service_status_logs", { value: result.logDir }) : null,
2927
+ ]
2928
+ .filter(Boolean)
2929
+ .join("\n");
2930
+ }
2931
+
2574
2932
  async function runPair({
2575
2933
  json,
2576
2934
  account,
@@ -2808,18 +3166,38 @@ async function runPair({
2808
3166
 
2809
3167
  if (serveRelay && !json) {
2810
3168
  try {
2811
- const bridgeResult = startRelayBridgeDetached({
3169
+ const serviceResult = installBridgeService({
2812
3170
  relayUrl,
2813
3171
  gatewayBaseUrl: localGatewayBaseUrl,
2814
3172
  });
2815
- output.relayWorkerResult = bridgeResult;
2816
- } catch (error) {
2817
3173
  output.relayWorkerResult = {
2818
- mode: "detached",
2819
- started: false,
2820
- error: String(error instanceof Error ? error.message : error),
3174
+ ...serviceResult,
3175
+ started: true,
3176
+ alreadyRunning: serviceResult.running,
3177
+ mode: "service",
2821
3178
  };
2822
- console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
3179
+ console.log(t("service_installed"));
3180
+ } catch (error) {
3181
+ const serviceError = String(error instanceof Error ? error.message : error);
3182
+ console.warn(t("service_install_failed"));
3183
+ console.warn(serviceError);
3184
+ try {
3185
+ const bridgeResult = startRelayBridgeDetached({
3186
+ relayUrl,
3187
+ gatewayBaseUrl: localGatewayBaseUrl,
3188
+ });
3189
+ output.relayWorkerResult = {
3190
+ ...bridgeResult,
3191
+ error: serviceError,
3192
+ };
3193
+ } catch (fallbackError) {
3194
+ output.relayWorkerResult = {
3195
+ mode: "detached",
3196
+ started: false,
3197
+ error: String(fallbackError instanceof Error ? fallbackError.message : fallbackError),
3198
+ };
3199
+ console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
3200
+ }
2823
3201
  }
2824
3202
  }
2825
3203
 
@@ -2866,6 +3244,14 @@ async function runReset({ json, relay, noRelay, gateway }) {
2866
3244
  relayUrl && gatewayBaseUrl
2867
3245
  ? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
2868
3246
  : { count: 0, pids: [] };
3247
+ let serviceReset = null;
3248
+ if (relayUrl && gatewayBaseUrl) {
3249
+ try {
3250
+ serviceReset = uninstallBridgeService();
3251
+ } catch (error) {
3252
+ serviceReset = { error: String(error instanceof Error ? error.message : error) };
3253
+ }
3254
+ }
2869
3255
  const nextState = resetLocalBindingState({ accountId });
2870
3256
 
2871
3257
  const output = {
@@ -2883,6 +3269,7 @@ async function runReset({ json, relay, noRelay, gateway }) {
2883
3269
  clearedCodes: relayResetResult.clearedCodes,
2884
3270
  },
2885
3271
  stoppedBridgeCount: stoppedBridges.count,
3272
+ serviceReset,
2886
3273
  nextGatewayId: nextState.state.relayGatewayId ?? null,
2887
3274
  statePath: nextState.statePath,
2888
3275
  };
@@ -2917,6 +3304,59 @@ async function runReset({ json, relay, noRelay, gateway }) {
2917
3304
  console.log(t("reset_ready"));
2918
3305
  }
2919
3306
 
3307
+ async function runService({ json, relay, noRelay, gateway, serviceAction }) {
3308
+ const configPath = resolveOpenClawConfigPath();
3309
+ const action = String(serviceAction ?? "status").trim().toLowerCase() || "status";
3310
+ const config = readJsonFileIfExists(configPath);
3311
+ const section = config ? readChannelSection(config) : {};
3312
+ const relayUrl =
3313
+ action === "install" || action === "restart"
3314
+ ? resolveRelayUrl({ relay, noRelay }, section)
3315
+ : normalizeBaseUrl(relay) || (config ? resolveRelayUrl({ relay: null, noRelay }, section) : null);
3316
+ const gatewayBaseUrl =
3317
+ action === "install" || action === "restart"
3318
+ ? resolveGatewayBaseUrl({ gateway }, config)
3319
+ : normalizeBaseUrl(gateway) || (config ? resolveGatewayBaseUrl({ gateway: null }, config) : null);
3320
+
3321
+ let result;
3322
+ switch (action) {
3323
+ case "status":
3324
+ result = getBridgeServiceStatus();
3325
+ break;
3326
+ case "install":
3327
+ result = installBridgeService({ relayUrl, gatewayBaseUrl });
3328
+ break;
3329
+ case "restart":
3330
+ result = restartBridgeService({ relayUrl, gatewayBaseUrl });
3331
+ break;
3332
+ case "uninstall":
3333
+ result = uninstallBridgeService();
3334
+ break;
3335
+ default:
3336
+ fail(`unknown service action: ${action}`);
3337
+ }
3338
+
3339
+ const output = {
3340
+ command: "service",
3341
+ action,
3342
+ relayUrl: relayUrl ?? null,
3343
+ gatewayBaseUrl: gatewayBaseUrl ?? null,
3344
+ ...result,
3345
+ };
3346
+
3347
+ if (json) {
3348
+ console.log(JSON.stringify(output, null, 2));
3349
+ return;
3350
+ }
3351
+
3352
+ if (action === "uninstall") {
3353
+ console.log(t("service_uninstalled"));
3354
+ } else if (action === "install" || action === "restart") {
3355
+ console.log(t("service_installed"));
3356
+ }
3357
+ console.log(formatServiceStatusOutput(output));
3358
+ }
3359
+
2920
3360
  function runSetup({
2921
3361
  json,
2922
3362
  account,
@@ -3192,6 +3632,7 @@ function runStatus({ json }) {
3192
3632
  const config = readJsonFile(configPath);
3193
3633
  const section = readChannelSection(config);
3194
3634
  const creds = resolveRelayCredentialsFromSection(section);
3635
+ const service = getBridgeServiceStatus();
3195
3636
 
3196
3637
  const status = {
3197
3638
  configPath,
@@ -3213,6 +3654,10 @@ function runStatus({ json }) {
3213
3654
  relayGatewayId: creds.gatewayId || null,
3214
3655
  hasRelayClientToken: Boolean(creds.clientToken),
3215
3656
  hasRelayGatewayToken: Boolean(creds.gatewayToken),
3657
+ serviceManager: service.manager,
3658
+ serviceInstalled: service.installed,
3659
+ serviceRunning: service.running,
3660
+ serviceTarget: service.target,
3216
3661
  linkedUserId:
3217
3662
  typeof section?.linkedUserId === "string" && section.linkedUserId.trim()
3218
3663
  ? section.linkedUserId.trim()
@@ -3254,6 +3699,20 @@ function runStatus({ json }) {
3254
3699
  value: status.hasAccessToken ? t("set") : t("missing"),
3255
3700
  }),
3256
3701
  );
3702
+ console.log(t("service_status_manager", { value: status.serviceManager }));
3703
+ console.log(
3704
+ t("service_status_installed", {
3705
+ value: status.serviceInstalled ? t("yes") : t("no"),
3706
+ }),
3707
+ );
3708
+ console.log(
3709
+ t("service_status_running", {
3710
+ value: status.serviceRunning ? t("yes") : t("no"),
3711
+ }),
3712
+ );
3713
+ if (status.serviceTarget) {
3714
+ console.log(t("service_status_target", { value: status.serviceTarget }));
3715
+ }
3257
3716
  console.log(t("status_linked_user", { value: status.linkedUserId ?? t("not_linked") }));
3258
3717
  }
3259
3718
 
@@ -3273,6 +3732,9 @@ async function main() {
3273
3732
  case "bridge":
3274
3733
  await runBridge(options);
3275
3734
  return;
3735
+ case "service":
3736
+ await runService(options);
3737
+ return;
3276
3738
  case "pair-url":
3277
3739
  runPairUrl(options);
3278
3740
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.9",
3
+ "version": "2026.4.11",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",