@matelink/cli 2026.4.16 → 2026.4.18

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 +246 -12
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -14,7 +14,7 @@ import fs from "node:fs";
14
14
  import os from "node:os";
15
15
  import path from "node:path";
16
16
  import process from "node:process";
17
- import { fileURLToPath } from "node:url";
17
+ import { fileURLToPath, pathToFileURL } from "node:url";
18
18
 
19
19
  const NUMERIC_CODE_LENGTH = 4;
20
20
  const GROUP_SIZE = 4;
@@ -51,7 +51,104 @@ const DEFAULT_GATEWAY_SCOPES = [
51
51
  "operator.approvals",
52
52
  "operator.pairing",
53
53
  ].join(",");
54
+ const SESSION_CONTEXT_MIN_TOKENS = 1024;
54
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
+ }
55
152
  const CLI_LANGUAGE = detectCliLanguage();
56
153
  const CLI_I18N = {
57
154
  zh: {
@@ -446,6 +543,18 @@ function resolveOpenClawConfigPath() {
446
543
  return path.join(resolveOpenClawHome(), "openclaw.json");
447
544
  }
448
545
 
546
+ function resolveOpenClawDistPath(fileName) {
547
+ return path.join(
548
+ resolveOpenClawHome(),
549
+ "extensions",
550
+ "memory-tdai",
551
+ "node_modules",
552
+ "openclaw",
553
+ "dist",
554
+ fileName,
555
+ );
556
+ }
557
+
449
558
  function resolveTestNextIMStatePath() {
450
559
  return path.join(resolveOpenClawHome(), "testnextim.state.json");
451
560
  }
@@ -546,6 +655,21 @@ function resolveConfiguredWorkspaceRoot() {
546
655
  return path.resolve(resolved);
547
656
  }
548
657
 
658
+ let sessionStoreHelpersPromise = null;
659
+
660
+ async function loadSessionStoreHelpers() {
661
+ if (!sessionStoreHelpersPromise) {
662
+ sessionStoreHelpersPromise = Promise.all([
663
+ import(pathToFileURL(resolveOpenClawDistPath("session-utils-PHKOUUva.js")).href),
664
+ import(pathToFileURL(resolveOpenClawDistPath("store-DACjypj4.js")).href),
665
+ ]).then(([sessionUtilsMod, storeMod]) => ({
666
+ loadSessionEntry: sessionUtilsMod.s,
667
+ updateSessionStoreEntry: storeMod.f,
668
+ }));
669
+ }
670
+ return sessionStoreHelpersPromise;
671
+ }
672
+
549
673
  function normalizeWorkspaceRelativePath(rawName) {
550
674
  const value = String(rawName ?? "").trim();
551
675
  if (!value) {
@@ -700,6 +824,7 @@ function listFixedMemoryFiles(workspaceRoot) {
700
824
  workspaceRoot,
701
825
  relativePath,
702
826
  absolutePath,
827
+ includeContent: true,
703
828
  });
704
829
  });
705
830
  }
@@ -814,6 +939,62 @@ async function callWorkspaceRpcLocal({
814
939
  return null;
815
940
  }
816
941
 
942
+ async function callSessionContextRpcLocal({
943
+ method,
944
+ params,
945
+ }) {
946
+ const sessionKey = String(params?.sessionKey ?? params?.key ?? "").trim();
947
+ if (!sessionKey) {
948
+ throw new Error("session key is required");
949
+ }
950
+
951
+ const { loadSessionEntry, updateSessionStoreEntry } = await loadSessionStoreHelpers();
952
+ const loaded = loadSessionEntry(sessionKey);
953
+ const resolvedSessionKey = String(loaded?.canonicalKey ?? sessionKey).trim() || sessionKey;
954
+
955
+ if (method === "session.context.get") {
956
+ return {
957
+ ok: true,
958
+ sessionKey: resolvedSessionKey,
959
+ contextTokens: Number.isFinite(loaded?.entry?.contextTokens)
960
+ ? Math.max(0, Math.floor(loaded.entry.contextTokens))
961
+ : null,
962
+ storePath: loaded?.storePath ?? "",
963
+ };
964
+ }
965
+
966
+ if (method === "session.context.set") {
967
+ if (!loaded?.entry || !String(loaded?.storePath ?? "").trim()) {
968
+ throw new Error(`session not found: ${sessionKey}`);
969
+ }
970
+ const contextTokens = Math.max(
971
+ SESSION_CONTEXT_MIN_TOKENS,
972
+ Math.floor(Number(params?.contextTokens ?? 0) || 0),
973
+ );
974
+ const updated = await updateSessionStoreEntry({
975
+ storePath: loaded.storePath,
976
+ sessionKey: resolvedSessionKey,
977
+ update: async (existing) => ({
978
+ contextTokens,
979
+ updatedAt: Math.max(existing?.updatedAt ?? 0, Date.now()),
980
+ }),
981
+ });
982
+ if (!updated) {
983
+ throw new Error(`session not found: ${sessionKey}`);
984
+ }
985
+ return {
986
+ ok: true,
987
+ sessionKey: resolvedSessionKey,
988
+ contextTokens: Number.isFinite(updated?.contextTokens)
989
+ ? Math.max(0, Math.floor(updated.contextTokens))
990
+ : contextTokens,
991
+ storePath: loaded.storePath,
992
+ };
993
+ }
994
+
995
+ return null;
996
+ }
997
+
817
998
  function commandToString(command, args) {
818
999
  return [command, ...args]
819
1000
  .map((part) => {
@@ -1291,6 +1472,40 @@ function extractErrorMessage(decoded, fallback) {
1291
1472
  return fallback;
1292
1473
  }
1293
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
+
1294
1509
  async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
1295
1510
  const controller = new AbortController();
1296
1511
  const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
@@ -1299,6 +1514,8 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_T
1299
1514
  ...options,
1300
1515
  signal: controller.signal,
1301
1516
  });
1517
+ } catch (error) {
1518
+ throw new Error(formatErrorDetails(error));
1302
1519
  } finally {
1303
1520
  clearTimeout(timer);
1304
1521
  }
@@ -1672,7 +1889,9 @@ function resolveBridgeServiceNodePath() {
1672
1889
  function writeBridgeLaunchScript(installConfig) {
1673
1890
  const scriptPath = resolveBridgeLaunchScriptPath();
1674
1891
  fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
1675
- const defaultPath = [
1892
+ const defaultPath = buildPathString([
1893
+ ...resolveSiblingBinDirs(installConfig.nodePath),
1894
+ ...resolveSiblingBinDirs(installConfig.openclawPath),
1676
1895
  "/opt/homebrew/bin",
1677
1896
  "/usr/local/bin",
1678
1897
  "/usr/bin",
@@ -1681,11 +1900,16 @@ function writeBridgeLaunchScript(installConfig) {
1681
1900
  "/sbin",
1682
1901
  path.join(os.homedir(), ".volta", "bin"),
1683
1902
  path.join(os.homedir(), ".nvm", "current", "bin"),
1684
- ].join(":");
1685
- const knownNodeCandidates = resolveNodeCandidates();
1686
- const candidateChecks = knownNodeCandidates
1687
- .map((candidate) => `if [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}'; fi`)
1688
- .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
+ : "";
1689
1913
  const fallbackLine = buildPosixShellExec("node", installConfig.args);
1690
1914
  const script = [
1691
1915
  "#!/bin/sh",
@@ -1722,6 +1946,7 @@ function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
1722
1946
  stdoutLog: path.join(logDir, "bridge.stdout.log"),
1723
1947
  stderrLog: path.join(logDir, "bridge.stderr.log"),
1724
1948
  nodePath: resolveBridgeServiceNodePath(),
1949
+ openclawPath: resolveOpenClawBinaryPath(),
1725
1950
  scriptPath: CLI_ENTRY,
1726
1951
  args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
1727
1952
  };
@@ -2526,7 +2751,7 @@ async function readRelayNextGatewayRequest({
2526
2751
  if (message.includes("timeout")) {
2527
2752
  return null;
2528
2753
  }
2529
- throw error;
2754
+ throw new Error(`relay gateway poll transport failed: GET ${url} -> ${message}`);
2530
2755
  }
2531
2756
 
2532
2757
  if (response.status === 204) {
@@ -3037,12 +3262,20 @@ async function callGatewayRpcLocal({
3037
3262
  return {
3038
3263
  ok: true,
3039
3264
  methods: ["sessions.list", "sessions.abort", "sessions.delete", "sessions.usage", "sessions.patch",
3040
- "usage.cost", "memory.files.list", "memory.files.get", "memory.files.set", "agents.files.list", "agents.files.get", "agents.files.set",
3265
+ "usage.cost", "session.context.get", "session.context.set",
3266
+ "memory.files.list", "memory.files.get", "memory.files.set", "agents.files.list", "agents.files.get", "agents.files.set",
3041
3267
  "skills.status", "models.list", "chat.history", "chat.abort",
3042
3268
  "config.get", "config.patch"],
3043
3269
  };
3044
3270
  }
3045
3271
 
3272
+ if (
3273
+ method === "session.context.get" ||
3274
+ method === "session.context.set"
3275
+ ) {
3276
+ return callSessionContextRpcLocal({ method, params });
3277
+ }
3278
+
3046
3279
  if (
3047
3280
  method === "memory.files.list" ||
3048
3281
  method === "memory.files.get" ||
@@ -3107,8 +3340,9 @@ async function callGatewayRpcLocalViaCli({
3107
3340
  args.push("--token", gatewayAuthToken);
3108
3341
  }
3109
3342
 
3110
- const child = spawn("openclaw", args, {
3343
+ const child = spawn(resolveOpenClawBinaryPath(), args, {
3111
3344
  stdio: ["ignore", "pipe", "pipe"],
3345
+ env: buildBridgeRuntimeEnv(),
3112
3346
  });
3113
3347
  let timedOut = false;
3114
3348
  const timeout = setTimeout(() => {
@@ -3537,12 +3771,12 @@ function startRelayBridgeDetached({
3537
3771
  }
3538
3772
 
3539
3773
  const child = spawn(
3540
- process.execPath,
3774
+ resolveBridgeServiceNodePath(),
3541
3775
  [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl],
3542
3776
  {
3543
3777
  detached: true,
3544
3778
  stdio: "ignore",
3545
- env: { ...process.env },
3779
+ env: buildBridgeRuntimeEnv(),
3546
3780
  windowsHide: process.platform === "win32",
3547
3781
  },
3548
3782
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.16",
3
+ "version": "2026.4.18",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",