@matelink/cli 2026.4.13 → 2026.4.15

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 +272 -9
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -20,19 +20,37 @@ const NUMERIC_CODE_LENGTH = 4;
20
20
  const GROUP_SIZE = 4;
21
21
  const DEFAULT_RELAY_WORKER_WAIT_SECONDS = 600;
22
22
  const DEFAULT_NETWORK_TIMEOUT_MS = 600000;
23
+ const BRIDGE_RETRY_DELAY_MS = 2000;
23
24
  const DEFAULT_RELAY_URL = "http://43.134.64.199:8090";
24
25
  const DEFAULT_GATEWAY_HOST = "127.0.0.1";
25
26
  const DEFAULT_WEBHOOK_PATH = "/testnextim/webhook";
26
27
  const DEFAULT_BIND_PATH = "/testnextim/bind";
27
28
  const GATEWAY_RPC_CLI_TIMEOUT_MS = DEFAULT_NETWORK_TIMEOUT_MS;
29
+ const WORKSPACE_FILE_LIST_MAX = 2000;
30
+ const WORKSPACE_SKIP_DIRS = new Set([
31
+ ".git",
32
+ "node_modules",
33
+ ".next",
34
+ "dist",
35
+ "build",
36
+ ".turbo",
37
+ ".cache",
38
+ ]);
28
39
  const CLI_PACKAGE_NAME = "@matelink/cli";
29
40
  const CLI_COMMAND_NAME = "matecli";
30
41
  const SERVICE_LABEL = "com.matelink.matecli.bridge";
31
42
  const SERVICE_UNIT_NAME = "matelink-matecli-bridge.service";
32
43
  const SERVICE_TASK_NAME = "Matelink MateCLI Bridge";
33
- // Relay worker defaults to full operator scope so gateway HTTP routes and RPC-adjacent
34
- // compatibility endpoints remain available without per-method scope juggling.
35
- const DEFAULT_GATEWAY_SCOPES = "operator.admin";
44
+ // Mirror the WS bridge's operator scopes for local HTTP proxy calls as well.
45
+ // Some OpenClaw gateway builds require explicit read/write scopes and do not
46
+ // treat operator.admin as an umbrella permission for `/v1/responses`.
47
+ const DEFAULT_GATEWAY_SCOPES = [
48
+ "operator.admin",
49
+ "operator.read",
50
+ "operator.write",
51
+ "operator.approvals",
52
+ "operator.pairing",
53
+ ].join(",");
36
54
  const CLI_ENTRY = fileURLToPath(import.meta.url);
37
55
  const CLI_LANGUAGE = detectCliLanguage();
38
56
  const CLI_I18N = {
@@ -501,6 +519,208 @@ function readJsonFileIfExists(filePath) {
501
519
  }
502
520
  }
503
521
 
522
+ function readOpenClawConfigForRuntime() {
523
+ const configPath = resolveOpenClawConfigPath();
524
+ let content;
525
+ try {
526
+ content = fs.readFileSync(configPath, "utf8");
527
+ } catch {
528
+ throw new Error(t("config_read_failed", { path: configPath }));
529
+ }
530
+ try {
531
+ return JSON.parse(content);
532
+ } catch {
533
+ throw new Error(t("config_invalid_json", { path: configPath }));
534
+ }
535
+ }
536
+
537
+ function resolveConfiguredWorkspaceRoot() {
538
+ const configPath = resolveOpenClawConfigPath();
539
+ const config = ensureObject(readOpenClawConfigForRuntime());
540
+ const workspaceRaw = String(config?.agents?.defaults?.workspace ?? "").trim();
541
+ const resolved = workspaceRaw
542
+ ? (path.isAbsolute(workspaceRaw)
543
+ ? workspaceRaw
544
+ : path.resolve(path.dirname(configPath), workspaceRaw))
545
+ : path.join(resolveOpenClawHome(), "workspace");
546
+ return path.resolve(resolved);
547
+ }
548
+
549
+ function normalizeWorkspaceRelativePath(rawName) {
550
+ const value = String(rawName ?? "").trim();
551
+ if (!value) {
552
+ throw new Error("workspace file name is required");
553
+ }
554
+ const normalized = value.replaceAll("\\", "/");
555
+ if (normalized.includes("\0") || /^[a-z]:\//i.test(normalized)) {
556
+ throw new Error("invalid workspace file path");
557
+ }
558
+ const relative = path.posix.normalize(normalized).replace(/^\/+/, "");
559
+ if (!relative || relative === "." || relative === ".." || relative.startsWith("../")) {
560
+ throw new Error("invalid workspace file path");
561
+ }
562
+ return relative;
563
+ }
564
+
565
+ function resolveWorkspaceFilePath(workspaceRoot, rawName) {
566
+ const relativePath = normalizeWorkspaceRelativePath(rawName);
567
+ const absolutePath = path.resolve(workspaceRoot, relativePath);
568
+ const relativeFromRoot = path.relative(workspaceRoot, absolutePath);
569
+ if (
570
+ !relativeFromRoot ||
571
+ relativeFromRoot === ".." ||
572
+ relativeFromRoot.startsWith(`..${path.sep}`) ||
573
+ path.isAbsolute(relativeFromRoot)
574
+ ) {
575
+ throw new Error("workspace file path escapes workspace root");
576
+ }
577
+ return {
578
+ relativePath,
579
+ absolutePath,
580
+ };
581
+ }
582
+
583
+ function buildWorkspaceFileMeta({
584
+ workspaceRoot,
585
+ relativePath,
586
+ absolutePath,
587
+ includeContent = false,
588
+ missing = false,
589
+ }) {
590
+ if (missing) {
591
+ return {
592
+ name: relativePath,
593
+ path: absolutePath,
594
+ missing: true,
595
+ size: null,
596
+ updatedAtMs: null,
597
+ content: null,
598
+ };
599
+ }
600
+ const stats = fs.statSync(absolutePath);
601
+ if (!stats.isFile()) {
602
+ throw new Error(`workspace path is not a file: ${relativePath}`);
603
+ }
604
+ return {
605
+ name: relativePath,
606
+ path: absolutePath,
607
+ missing: false,
608
+ size: stats.size,
609
+ updatedAtMs: Math.round(stats.mtimeMs),
610
+ content: includeContent ? fs.readFileSync(absolutePath, "utf8") : null,
611
+ };
612
+ }
613
+
614
+ function listWorkspaceFiles(workspaceRoot) {
615
+ const files = [];
616
+ const walk = (relativeDir = "") => {
617
+ if (files.length >= WORKSPACE_FILE_LIST_MAX) {
618
+ return;
619
+ }
620
+ const absoluteDir = relativeDir
621
+ ? path.join(workspaceRoot, relativeDir)
622
+ : workspaceRoot;
623
+ let entries = [];
624
+ try {
625
+ entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
626
+ } catch {
627
+ return;
628
+ }
629
+ entries.sort((a, b) => a.name.localeCompare(b.name));
630
+ for (const entry of entries) {
631
+ if (files.length >= WORKSPACE_FILE_LIST_MAX) {
632
+ break;
633
+ }
634
+ if (entry.name === "." || entry.name === ".." || entry.isSymbolicLink()) {
635
+ continue;
636
+ }
637
+ const relativePath = relativeDir
638
+ ? path.posix.join(relativeDir.replaceAll("\\", "/"), entry.name)
639
+ : entry.name;
640
+ const absolutePath = path.join(absoluteDir, entry.name);
641
+ if (entry.isDirectory()) {
642
+ if (WORKSPACE_SKIP_DIRS.has(entry.name)) {
643
+ continue;
644
+ }
645
+ walk(relativePath);
646
+ continue;
647
+ }
648
+ if (!entry.isFile()) {
649
+ continue;
650
+ }
651
+ try {
652
+ files.push(buildWorkspaceFileMeta({
653
+ workspaceRoot,
654
+ relativePath,
655
+ absolutePath,
656
+ }));
657
+ } catch {
658
+ // Ignore transient file stat/read issues.
659
+ }
660
+ }
661
+ };
662
+ walk("");
663
+ return files;
664
+ }
665
+
666
+ async function callWorkspaceRpcLocal({
667
+ method,
668
+ params,
669
+ }) {
670
+ const workspaceRoot = resolveConfiguredWorkspaceRoot();
671
+ if (method === "agents.files.list") {
672
+ return {
673
+ ok: true,
674
+ workspace: workspaceRoot,
675
+ files: listWorkspaceFiles(workspaceRoot),
676
+ };
677
+ }
678
+
679
+ if (method === "agents.files.get") {
680
+ const { relativePath, absolutePath } = resolveWorkspaceFilePath(workspaceRoot, params?.name);
681
+ if (!fs.existsSync(absolutePath)) {
682
+ return {
683
+ ok: true,
684
+ workspace: workspaceRoot,
685
+ file: buildWorkspaceFileMeta({
686
+ workspaceRoot,
687
+ relativePath,
688
+ absolutePath,
689
+ missing: true,
690
+ }),
691
+ };
692
+ }
693
+ return {
694
+ ok: true,
695
+ workspace: workspaceRoot,
696
+ file: buildWorkspaceFileMeta({
697
+ workspaceRoot,
698
+ relativePath,
699
+ absolutePath,
700
+ includeContent: true,
701
+ }),
702
+ };
703
+ }
704
+
705
+ if (method === "agents.files.set") {
706
+ const { relativePath, absolutePath } = resolveWorkspaceFilePath(workspaceRoot, params?.name);
707
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
708
+ fs.writeFileSync(absolutePath, String(params?.content ?? ""), "utf8");
709
+ return {
710
+ ok: true,
711
+ workspace: workspaceRoot,
712
+ file: buildWorkspaceFileMeta({
713
+ workspaceRoot,
714
+ relativePath,
715
+ absolutePath,
716
+ includeContent: true,
717
+ }),
718
+ };
719
+ }
720
+
721
+ return null;
722
+ }
723
+
504
724
  function commandToString(command, args) {
505
725
  return [command, ...args]
506
726
  .map((part) => {
@@ -991,6 +1211,27 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_T
991
1211
  }
992
1212
  }
993
1213
 
1214
+ function isTransientNetworkError(error) {
1215
+ const message = String(error instanceof Error ? error.message : error).toLowerCase();
1216
+ return (
1217
+ message.includes("timeout") ||
1218
+ message.includes("fetch failed") ||
1219
+ message.includes("terminated") ||
1220
+ message.includes("socket") ||
1221
+ message.includes("econnreset") ||
1222
+ message.includes("econnrefused") ||
1223
+ message.includes("ehostunreach") ||
1224
+ message.includes("enotfound") ||
1225
+ message.includes("etimedout") ||
1226
+ message.includes("epipe") ||
1227
+ message.includes("network")
1228
+ );
1229
+ }
1230
+
1231
+ function sleep(ms) {
1232
+ return new Promise((resolve) => setTimeout(resolve, ms));
1233
+ }
1234
+
994
1235
  function normalizeGatewayIdRaw(value) {
995
1236
  return String(value ?? "")
996
1237
  .trim()
@@ -2709,6 +2950,14 @@ async function callGatewayRpcLocal({
2709
2950
  };
2710
2951
  }
2711
2952
 
2953
+ if (
2954
+ method === "agents.files.list" ||
2955
+ method === "agents.files.get" ||
2956
+ method === "agents.files.set"
2957
+ ) {
2958
+ return callWorkspaceRpcLocal({ method, params });
2959
+ }
2960
+
2712
2961
  // Prefer CLI first for broad compatibility, but fall back to the persistent
2713
2962
  // WS client when the CLI gateway call hits transient websocket closures.
2714
2963
  try {
@@ -3104,12 +3353,26 @@ async function runRelayBridge({
3104
3353
  const activeRequests = new Set();
3105
3354
 
3106
3355
  while (true) {
3107
- const request = await readRelayNextGatewayRequest({
3108
- relayUrl,
3109
- gatewayId,
3110
- gatewayToken,
3111
- waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
3112
- });
3356
+ let request;
3357
+ try {
3358
+ request = await readRelayNextGatewayRequest({
3359
+ relayUrl,
3360
+ gatewayId,
3361
+ gatewayToken,
3362
+ waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
3363
+ });
3364
+ } catch (error) {
3365
+ if (!isTransientNetworkError(error)) {
3366
+ throw error;
3367
+ }
3368
+ if (!json) {
3369
+ console.warn(
3370
+ `[matecli] relay poll failed: ${String(error instanceof Error ? error.message : error)}; retrying in ${Math.round(BRIDGE_RETRY_DELAY_MS / 1000)}s`,
3371
+ );
3372
+ }
3373
+ await sleep(BRIDGE_RETRY_DELAY_MS);
3374
+ continue;
3375
+ }
3113
3376
  if (!request) {
3114
3377
  continue;
3115
3378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.13",
3
+ "version": "2026.4.15",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",