@matelink/cli 2026.4.17 → 2026.4.19

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 +191 -21
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -53,6 +53,102 @@ const DEFAULT_GATEWAY_SCOPES = [
53
53
  ].join(",");
54
54
  const SESSION_CONTEXT_MIN_TOKENS = 1024;
55
55
  const CLI_ENTRY = fileURLToPath(import.meta.url);
56
+
57
+
58
+ function buildPathString(values) {
59
+ const entries = [];
60
+ const seen = new Set();
61
+ for (const value of values) {
62
+ const raw = String(value ?? "").trim();
63
+ if (!raw) continue;
64
+ for (const part of raw.split(":")) {
65
+ const normalized = part.trim();
66
+ if (!normalized || seen.has(normalized)) continue;
67
+ seen.add(normalized);
68
+ entries.push(normalized);
69
+ }
70
+ }
71
+ return entries.join(":");
72
+ }
73
+
74
+ function resolveSiblingBinDirs(executablePath) {
75
+ const normalized = String(executablePath ?? "").trim();
76
+ if (!normalized) return [];
77
+ const binDir = path.dirname(normalized);
78
+ return [binDir];
79
+ }
80
+
81
+ function resolveOpenClawBinaryCandidates() {
82
+ const homeDir = os.homedir();
83
+ const candidates = new Set();
84
+ const addCandidate = (candidate) => {
85
+ const normalized = String(candidate ?? "").trim();
86
+ if (!normalized) {
87
+ return;
88
+ }
89
+ try {
90
+ if (fs.existsSync(normalized)) {
91
+ candidates.add(fs.realpathSync(normalized));
92
+ }
93
+ } catch {
94
+ if (fs.existsSync(normalized)) {
95
+ candidates.add(normalized);
96
+ }
97
+ }
98
+ };
99
+
100
+ for (const nodeCandidate of resolveNodeCandidates()) {
101
+ addCandidate(path.join(path.dirname(nodeCandidate), "openclaw"));
102
+ }
103
+ addCandidate(path.join(homeDir, ".nvm", "current", "bin", "openclaw"));
104
+ addCandidate(path.join(homeDir, ".volta", "bin", "openclaw"));
105
+ addCandidate("/opt/homebrew/bin/openclaw");
106
+ addCandidate("/usr/local/bin/openclaw");
107
+ addCandidate("/usr/bin/openclaw");
108
+
109
+ const whichOpenClaw = runCommand("which", ["openclaw"]);
110
+ if (whichOpenClaw.status === 0) {
111
+ const resolved = String(whichOpenClaw.stdout ?? "").trim();
112
+ if (resolved && fs.existsSync(resolved)) {
113
+ addCandidate(resolved);
114
+ }
115
+ }
116
+
117
+ return [...candidates];
118
+ }
119
+
120
+ function resolveOpenClawBinaryPath() {
121
+ for (const candidate of resolveOpenClawBinaryCandidates()) {
122
+ if (candidate && fs.existsSync(candidate)) {
123
+ return candidate;
124
+ }
125
+ }
126
+ return "openclaw";
127
+ }
128
+
129
+ function buildBridgeRuntimeEnv(extraEnv = {}) {
130
+ const homeDir = os.homedir();
131
+ const nodePath = resolveBridgeServiceNodePath();
132
+ const openclawPath = resolveOpenClawBinaryPath();
133
+ const pathValue = buildPathString([
134
+ ...resolveSiblingBinDirs(nodePath),
135
+ ...resolveSiblingBinDirs(openclawPath),
136
+ "/opt/homebrew/bin",
137
+ "/usr/local/bin",
138
+ "/usr/bin",
139
+ "/bin",
140
+ "/usr/sbin",
141
+ "/sbin",
142
+ path.join(homeDir, ".volta", "bin"),
143
+ path.join(homeDir, ".nvm", "current", "bin"),
144
+ process.env.PATH ?? "",
145
+ ]);
146
+ return {
147
+ ...process.env,
148
+ ...extraEnv,
149
+ PATH: pathValue,
150
+ };
151
+ }
56
152
  const CLI_LANGUAGE = detectCliLanguage();
57
153
  const CLI_I18N = {
58
154
  zh: {
@@ -728,6 +824,7 @@ function listFixedMemoryFiles(workspaceRoot) {
728
824
  workspaceRoot,
729
825
  relativePath,
730
826
  absolutePath,
827
+ includeContent: true,
731
828
  });
732
829
  });
733
830
  }
@@ -1375,6 +1472,40 @@ function extractErrorMessage(decoded, fallback) {
1375
1472
  return fallback;
1376
1473
  }
1377
1474
 
1475
+ function formatErrorDetails(error) {
1476
+ if (!(error instanceof Error)) {
1477
+ return String(error ?? "");
1478
+ }
1479
+ const details = [];
1480
+ if (error.name && error.name !== "Error") {
1481
+ details.push(`name=${error.name}`);
1482
+ }
1483
+ if (error.code) {
1484
+ details.push(`code=${String(error.code)}`);
1485
+ }
1486
+ if (error.errno) {
1487
+ details.push(`errno=${String(error.errno)}`);
1488
+ }
1489
+ if (error.type) {
1490
+ details.push(`type=${String(error.type)}`);
1491
+ }
1492
+ if (error.cause && typeof error.cause === "object") {
1493
+ const cause = error.cause;
1494
+ if (cause?.code) {
1495
+ details.push(`cause.code=${String(cause.code)}`);
1496
+ }
1497
+ if (cause?.errno) {
1498
+ details.push(`cause.errno=${String(cause.errno)}`);
1499
+ }
1500
+ if (cause?.message) {
1501
+ details.push(`cause=${String(cause.message)}`);
1502
+ }
1503
+ }
1504
+ return details.length > 0
1505
+ ? `${error.message} [${details.join(", ")}]`
1506
+ : error.message;
1507
+ }
1508
+
1378
1509
  async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
1379
1510
  const controller = new AbortController();
1380
1511
  const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
@@ -1383,6 +1514,8 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_T
1383
1514
  ...options,
1384
1515
  signal: controller.signal,
1385
1516
  });
1517
+ } catch (error) {
1518
+ throw new Error(formatErrorDetails(error));
1386
1519
  } finally {
1387
1520
  clearTimeout(timer);
1388
1521
  }
@@ -1756,7 +1889,9 @@ function resolveBridgeServiceNodePath() {
1756
1889
  function writeBridgeLaunchScript(installConfig) {
1757
1890
  const scriptPath = resolveBridgeLaunchScriptPath();
1758
1891
  fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
1759
- const defaultPath = [
1892
+ const defaultPath = buildPathString([
1893
+ ...resolveSiblingBinDirs(installConfig.nodePath),
1894
+ ...resolveSiblingBinDirs(installConfig.openclawPath),
1760
1895
  "/opt/homebrew/bin",
1761
1896
  "/usr/local/bin",
1762
1897
  "/usr/bin",
@@ -1765,11 +1900,16 @@ function writeBridgeLaunchScript(installConfig) {
1765
1900
  "/sbin",
1766
1901
  path.join(os.homedir(), ".volta", "bin"),
1767
1902
  path.join(os.homedir(), ".nvm", "current", "bin"),
1768
- ].join(":");
1769
- const knownNodeCandidates = resolveNodeCandidates();
1770
- const candidateChecks = knownNodeCandidates
1771
- .map((candidate) => `if [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}'; fi`)
1772
- .join("\n");
1903
+ ]);
1904
+ const knownNodeCandidates = [installConfig.nodePath, ...resolveNodeCandidates()]
1905
+ .filter(Boolean)
1906
+ .filter((candidate, index, values) => values.indexOf(candidate) === index);
1907
+ const candidateChecks = knownNodeCandidates.length > 0
1908
+ ? `${knownNodeCandidates
1909
+ .map((candidate, index) => `${index === 0 ? 'if' : 'elif'} [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}';`)
1910
+ .join("\n")}
1911
+ fi`
1912
+ : "";
1773
1913
  const fallbackLine = buildPosixShellExec("node", installConfig.args);
1774
1914
  const script = [
1775
1915
  "#!/bin/sh",
@@ -1806,6 +1946,7 @@ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1806
1946
  stdoutLog: path.join(logDir, "bridge.stdout.log"),
1807
1947
  stderrLog: path.join(logDir, "bridge.stderr.log"),
1808
1948
  nodePath: resolveBridgeServiceNodePath(),
1949
+ openclawPath: resolveOpenClawBinaryPath(),
1809
1950
  scriptPath: CLI_ENTRY,
1810
1951
  args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1811
1952
  };
@@ -2610,7 +2751,7 @@ async function readRelayNextGatewayRequest({
2610
2751
  if (message.includes("timeout")) {
2611
2752
  return null;
2612
2753
  }
2613
- throw error;
2754
+ throw new Error(`relay gateway poll transport failed: GET ${url} -> ${message}`);
2614
2755
  }
2615
2756
 
2616
2757
  if (response.status === 204) {
@@ -3011,13 +3152,11 @@ function createGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken }) {
3011
3152
  connectTimer = null;
3012
3153
  }
3013
3154
  const role = "operator";
3014
- const scopes = [
3015
- "operator.admin",
3016
- "operator.read",
3017
- "operator.write",
3018
- "operator.approvals",
3019
- "operator.pairing",
3020
- ];
3155
+ // The bridge WS client only performs read-side gateway RPCs such as
3156
+ // config.get, sessions.list, skills.status, and chat.history. Asking
3157
+ // for broader scopes causes local token-mode gateways to reject the
3158
+ // connection with a scope-upgrade/pairing-required error.
3159
+ const scopes = ["operator.read"];
3021
3160
  const clientId = "openclaw-macos";
3022
3161
  const clientMode = "ui";
3023
3162
  const signedAtMs = Date.now();
@@ -3146,6 +3285,11 @@ async function callGatewayRpcLocal({
3146
3285
  return callWorkspaceRpcLocal({ method, params });
3147
3286
  }
3148
3287
 
3288
+ if (method === "config.get" || method === "sessions.list") {
3289
+ const client = getOrCreateGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken });
3290
+ return client.call(method, params ?? {});
3291
+ }
3292
+
3149
3293
  // Prefer CLI first for broad compatibility, but fall back to the persistent
3150
3294
  // WS client when the CLI gateway call hits transient websocket closures.
3151
3295
  try {
@@ -3172,7 +3316,10 @@ function shouldFallbackGatewayRpcViaWs(error) {
3172
3316
  message.includes("(1006") ||
3173
3317
  message.includes("no close reason") ||
3174
3318
  message.includes("gateway ws error") ||
3175
- message.includes("gateway disconnected")
3319
+ message.includes("gateway disconnected") ||
3320
+ message.includes("spawn openclaw enoent") ||
3321
+ message.includes("enoent") ||
3322
+ message.includes("gateway call timed out")
3176
3323
  );
3177
3324
  }
3178
3325
 
@@ -3199,8 +3346,9 @@ async function callGatewayRpcLocalViaCli({
3199
3346
  args.push("--token", gatewayAuthToken);
3200
3347
  }
3201
3348
 
3202
- const child = spawn("openclaw", args, {
3349
+ const child = spawn(resolveOpenClawBinaryPath(), args, {
3203
3350
  stdio: ["ignore", "pipe", "pipe"],
3351
+ env: buildBridgeRuntimeEnv(),
3204
3352
  });
3205
3353
  let timedOut = false;
3206
3354
  const timeout = setTimeout(() => {
@@ -3212,7 +3360,7 @@ async function callGatewayRpcLocalViaCli({
3212
3360
  }
3213
3361
  }, GATEWAY_RPC_CLI_TIMEOUT_MS);
3214
3362
 
3215
- const [stdoutChunks, stderrChunks] = await Promise.all([
3363
+ const [stdoutChunks, stderrChunks, spawnError] = await Promise.all([
3216
3364
  new Promise((resolve) => {
3217
3365
  const chunks = [];
3218
3366
  child.stdout?.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
@@ -3225,10 +3373,18 @@ async function callGatewayRpcLocalViaCli({
3225
3373
  child.stderr?.on("end", () => resolve(chunks));
3226
3374
  child.stderr?.on("error", () => resolve(chunks));
3227
3375
  }),
3376
+ new Promise((resolve) => {
3377
+ child.once("error", (error) => resolve(error));
3378
+ child.once("spawn", () => resolve(null));
3379
+ }),
3228
3380
  new Promise((resolve) => child.on("close", resolve)),
3229
3381
  ]);
3230
3382
  clearTimeout(timeout);
3231
3383
 
3384
+ if (spawnError instanceof Error) {
3385
+ throw spawnError;
3386
+ }
3387
+
3232
3388
  const stdoutText = Buffer.concat(stdoutChunks).toString("utf8");
3233
3389
  const stderrText = Buffer.concat(stderrChunks).toString("utf8");
3234
3390
  if (timedOut) {
@@ -3453,6 +3609,11 @@ async function proxyGatewayRequestLocal({
3453
3609
  const reader = response.body.getReader();
3454
3610
  const decoder = new TextDecoder();
3455
3611
 
3612
+ // Fire-and-forget chunk publishing: don't await each POST so the reader
3613
+ // is never blocked by network round-trips. We keep an ordered queue of
3614
+ // in-flight promises and drain it before sending the final "end" event.
3615
+ const inflightChunks = [];
3616
+
3456
3617
  while (true) {
3457
3618
  const { done, value } = await reader.read();
3458
3619
  if (done) {
@@ -3462,7 +3623,7 @@ async function proxyGatewayRequestLocal({
3462
3623
  if (!text) {
3463
3624
  continue;
3464
3625
  }
3465
- await publishRelayGatewayEvent({
3626
+ const p = publishRelayGatewayEvent({
3466
3627
  relayUrl,
3467
3628
  gatewayId,
3468
3629
  gatewayToken,
@@ -3471,12 +3632,15 @@ async function proxyGatewayRequestLocal({
3471
3632
  event: "chunk",
3472
3633
  data: text,
3473
3634
  },
3635
+ }).catch((err) => {
3636
+ console.error(`[relay chunk publish error] ${err?.message || err}`);
3474
3637
  });
3638
+ inflightChunks.push(p);
3475
3639
  }
3476
3640
 
3477
3641
  const tail = decoder.decode();
3478
3642
  if (tail) {
3479
- await publishRelayGatewayEvent({
3643
+ const p = publishRelayGatewayEvent({
3480
3644
  relayUrl,
3481
3645
  gatewayId,
3482
3646
  gatewayToken,
@@ -3485,9 +3649,15 @@ async function proxyGatewayRequestLocal({
3485
3649
  event: "chunk",
3486
3650
  data: tail,
3487
3651
  },
3652
+ }).catch((err) => {
3653
+ console.error(`[relay tail publish error] ${err?.message || err}`);
3488
3654
  });
3655
+ inflightChunks.push(p);
3489
3656
  }
3490
3657
 
3658
+ // Wait for all chunk publishes to complete before sending "end".
3659
+ await Promise.all(inflightChunks);
3660
+
3491
3661
  await publishRelayGatewayEvent({
3492
3662
  relayUrl,
3493
3663
  gatewayId,
@@ -3629,12 +3799,12 @@ function startRelayBridgeDetached({
3629
3799
  }
3630
3800
 
3631
3801
  const child = spawn(
3632
- process.execPath,
3802
+ resolveBridgeServiceNodePath(),
3633
3803
  [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl],
3634
3804
  {
3635
3805
  detached: true,
3636
3806
  stdio: "ignore",
3637
- env: { ...process.env },
3807
+ env: buildBridgeRuntimeEnv(),
3638
3808
  windowsHide: process.platform === "win32",
3639
3809
  },
3640
3810
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.17",
3
+ "version": "2026.4.19",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",