@matelink/cli 2026.4.12 → 2026.4.14

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 +260 -6
  2. package/package.json +1 -1
package/bin/matecli.mjs CHANGED
@@ -25,14 +25,31 @@ const DEFAULT_GATEWAY_HOST = "127.0.0.1";
25
25
  const DEFAULT_WEBHOOK_PATH = "/testnextim/webhook";
26
26
  const DEFAULT_BIND_PATH = "/testnextim/bind";
27
27
  const GATEWAY_RPC_CLI_TIMEOUT_MS = DEFAULT_NETWORK_TIMEOUT_MS;
28
+ const WORKSPACE_FILE_LIST_MAX = 2000;
29
+ const WORKSPACE_SKIP_DIRS = new Set([
30
+ ".git",
31
+ "node_modules",
32
+ ".next",
33
+ "dist",
34
+ "build",
35
+ ".turbo",
36
+ ".cache",
37
+ ]);
28
38
  const CLI_PACKAGE_NAME = "@matelink/cli";
29
39
  const CLI_COMMAND_NAME = "matecli";
30
40
  const SERVICE_LABEL = "com.matelink.matecli.bridge";
31
41
  const SERVICE_UNIT_NAME = "matelink-matecli-bridge.service";
32
42
  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";
43
+ // Mirror the WS bridge's operator scopes for local HTTP proxy calls as well.
44
+ // Some OpenClaw gateway builds require explicit read/write scopes and do not
45
+ // treat operator.admin as an umbrella permission for `/v1/responses`.
46
+ const DEFAULT_GATEWAY_SCOPES = [
47
+ "operator.admin",
48
+ "operator.read",
49
+ "operator.write",
50
+ "operator.approvals",
51
+ "operator.pairing",
52
+ ].join(",");
36
53
  const CLI_ENTRY = fileURLToPath(import.meta.url);
37
54
  const CLI_LANGUAGE = detectCliLanguage();
38
55
  const CLI_I18N = {
@@ -501,6 +518,208 @@ function readJsonFileIfExists(filePath) {
501
518
  }
502
519
  }
503
520
 
521
+ function readOpenClawConfigForRuntime() {
522
+ const configPath = resolveOpenClawConfigPath();
523
+ let content;
524
+ try {
525
+ content = fs.readFileSync(configPath, "utf8");
526
+ } catch {
527
+ throw new Error(t("config_read_failed", { path: configPath }));
528
+ }
529
+ try {
530
+ return JSON.parse(content);
531
+ } catch {
532
+ throw new Error(t("config_invalid_json", { path: configPath }));
533
+ }
534
+ }
535
+
536
+ function resolveConfiguredWorkspaceRoot() {
537
+ const configPath = resolveOpenClawConfigPath();
538
+ const config = ensureObject(readOpenClawConfigForRuntime());
539
+ const workspaceRaw = String(config?.agents?.defaults?.workspace ?? "").trim();
540
+ const resolved = workspaceRaw
541
+ ? (path.isAbsolute(workspaceRaw)
542
+ ? workspaceRaw
543
+ : path.resolve(path.dirname(configPath), workspaceRaw))
544
+ : path.join(resolveOpenClawHome(), "workspace");
545
+ return path.resolve(resolved);
546
+ }
547
+
548
+ function normalizeWorkspaceRelativePath(rawName) {
549
+ const value = String(rawName ?? "").trim();
550
+ if (!value) {
551
+ throw new Error("workspace file name is required");
552
+ }
553
+ const normalized = value.replaceAll("\\", "/");
554
+ if (normalized.includes("\0") || /^[a-z]:\//i.test(normalized)) {
555
+ throw new Error("invalid workspace file path");
556
+ }
557
+ const relative = path.posix.normalize(normalized).replace(/^\/+/, "");
558
+ if (!relative || relative === "." || relative === ".." || relative.startsWith("../")) {
559
+ throw new Error("invalid workspace file path");
560
+ }
561
+ return relative;
562
+ }
563
+
564
+ function resolveWorkspaceFilePath(workspaceRoot, rawName) {
565
+ const relativePath = normalizeWorkspaceRelativePath(rawName);
566
+ const absolutePath = path.resolve(workspaceRoot, relativePath);
567
+ const relativeFromRoot = path.relative(workspaceRoot, absolutePath);
568
+ if (
569
+ !relativeFromRoot ||
570
+ relativeFromRoot === ".." ||
571
+ relativeFromRoot.startsWith(`..${path.sep}`) ||
572
+ path.isAbsolute(relativeFromRoot)
573
+ ) {
574
+ throw new Error("workspace file path escapes workspace root");
575
+ }
576
+ return {
577
+ relativePath,
578
+ absolutePath,
579
+ };
580
+ }
581
+
582
+ function buildWorkspaceFileMeta({
583
+ workspaceRoot,
584
+ relativePath,
585
+ absolutePath,
586
+ includeContent = false,
587
+ missing = false,
588
+ }) {
589
+ if (missing) {
590
+ return {
591
+ name: relativePath,
592
+ path: absolutePath,
593
+ missing: true,
594
+ size: null,
595
+ updatedAtMs: null,
596
+ content: null,
597
+ };
598
+ }
599
+ const stats = fs.statSync(absolutePath);
600
+ if (!stats.isFile()) {
601
+ throw new Error(`workspace path is not a file: ${relativePath}`);
602
+ }
603
+ return {
604
+ name: relativePath,
605
+ path: absolutePath,
606
+ missing: false,
607
+ size: stats.size,
608
+ updatedAtMs: Math.round(stats.mtimeMs),
609
+ content: includeContent ? fs.readFileSync(absolutePath, "utf8") : null,
610
+ };
611
+ }
612
+
613
+ function listWorkspaceFiles(workspaceRoot) {
614
+ const files = [];
615
+ const walk = (relativeDir = "") => {
616
+ if (files.length >= WORKSPACE_FILE_LIST_MAX) {
617
+ return;
618
+ }
619
+ const absoluteDir = relativeDir
620
+ ? path.join(workspaceRoot, relativeDir)
621
+ : workspaceRoot;
622
+ let entries = [];
623
+ try {
624
+ entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
625
+ } catch {
626
+ return;
627
+ }
628
+ entries.sort((a, b) => a.name.localeCompare(b.name));
629
+ for (const entry of entries) {
630
+ if (files.length >= WORKSPACE_FILE_LIST_MAX) {
631
+ break;
632
+ }
633
+ if (entry.name === "." || entry.name === ".." || entry.isSymbolicLink()) {
634
+ continue;
635
+ }
636
+ const relativePath = relativeDir
637
+ ? path.posix.join(relativeDir.replaceAll("\\", "/"), entry.name)
638
+ : entry.name;
639
+ const absolutePath = path.join(absoluteDir, entry.name);
640
+ if (entry.isDirectory()) {
641
+ if (WORKSPACE_SKIP_DIRS.has(entry.name)) {
642
+ continue;
643
+ }
644
+ walk(relativePath);
645
+ continue;
646
+ }
647
+ if (!entry.isFile()) {
648
+ continue;
649
+ }
650
+ try {
651
+ files.push(buildWorkspaceFileMeta({
652
+ workspaceRoot,
653
+ relativePath,
654
+ absolutePath,
655
+ }));
656
+ } catch {
657
+ // Ignore transient file stat/read issues.
658
+ }
659
+ }
660
+ };
661
+ walk("");
662
+ return files;
663
+ }
664
+
665
+ async function callWorkspaceRpcLocal({
666
+ method,
667
+ params,
668
+ }) {
669
+ const workspaceRoot = resolveConfiguredWorkspaceRoot();
670
+ if (method === "agents.files.list") {
671
+ return {
672
+ ok: true,
673
+ workspace: workspaceRoot,
674
+ files: listWorkspaceFiles(workspaceRoot),
675
+ };
676
+ }
677
+
678
+ if (method === "agents.files.get") {
679
+ const { relativePath, absolutePath } = resolveWorkspaceFilePath(workspaceRoot, params?.name);
680
+ if (!fs.existsSync(absolutePath)) {
681
+ return {
682
+ ok: true,
683
+ workspace: workspaceRoot,
684
+ file: buildWorkspaceFileMeta({
685
+ workspaceRoot,
686
+ relativePath,
687
+ absolutePath,
688
+ missing: true,
689
+ }),
690
+ };
691
+ }
692
+ return {
693
+ ok: true,
694
+ workspace: workspaceRoot,
695
+ file: buildWorkspaceFileMeta({
696
+ workspaceRoot,
697
+ relativePath,
698
+ absolutePath,
699
+ includeContent: true,
700
+ }),
701
+ };
702
+ }
703
+
704
+ if (method === "agents.files.set") {
705
+ const { relativePath, absolutePath } = resolveWorkspaceFilePath(workspaceRoot, params?.name);
706
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
707
+ fs.writeFileSync(absolutePath, String(params?.content ?? ""), "utf8");
708
+ return {
709
+ ok: true,
710
+ workspace: workspaceRoot,
711
+ file: buildWorkspaceFileMeta({
712
+ workspaceRoot,
713
+ relativePath,
714
+ absolutePath,
715
+ includeContent: true,
716
+ }),
717
+ };
718
+ }
719
+
720
+ return null;
721
+ }
722
+
504
723
  function commandToString(command, args) {
505
724
  return [command, ...args]
506
725
  .map((part) => {
@@ -2709,9 +2928,42 @@ async function callGatewayRpcLocal({
2709
2928
  };
2710
2929
  }
2711
2930
 
2712
- // In bridge mode the CLI path is more reliable than the shared WebSocket client,
2713
- // especially while long-running streamed chats are also active.
2714
- return callGatewayRpcLocalViaCli({ gatewayBaseUrl, gatewayAuthToken, method, params });
2931
+ if (
2932
+ method === "agents.files.list" ||
2933
+ method === "agents.files.get" ||
2934
+ method === "agents.files.set"
2935
+ ) {
2936
+ return callWorkspaceRpcLocal({ method, params });
2937
+ }
2938
+
2939
+ // Prefer CLI first for broad compatibility, but fall back to the persistent
2940
+ // WS client when the CLI gateway call hits transient websocket closures.
2941
+ try {
2942
+ return await callGatewayRpcLocalViaCli({
2943
+ gatewayBaseUrl,
2944
+ gatewayAuthToken,
2945
+ method,
2946
+ params,
2947
+ });
2948
+ } catch (error) {
2949
+ if (!shouldFallbackGatewayRpcViaWs(error)) {
2950
+ throw error;
2951
+ }
2952
+ const client = getOrCreateGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken });
2953
+ return client.call(method, params ?? {});
2954
+ }
2955
+ }
2956
+
2957
+ function shouldFallbackGatewayRpcViaWs(error) {
2958
+ const message = String(error instanceof Error ? error.message : error).toLowerCase();
2959
+ return (
2960
+ message.includes("gateway closed") ||
2961
+ message.includes("abnormal closure") ||
2962
+ message.includes("(1006") ||
2963
+ message.includes("no close reason") ||
2964
+ message.includes("gateway ws error") ||
2965
+ message.includes("gateway disconnected")
2966
+ );
2715
2967
  }
2716
2968
 
2717
2969
  async function callGatewayRpcLocalViaCli({
@@ -2727,6 +2979,8 @@ async function callGatewayRpcLocalViaCli({
2727
2979
  method,
2728
2980
  "--url",
2729
2981
  wsUrl,
2982
+ "--timeout",
2983
+ String(GATEWAY_RPC_CLI_TIMEOUT_MS),
2730
2984
  "--params",
2731
2985
  JSON.stringify(params ?? {}),
2732
2986
  "--json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.12",
3
+ "version": "2026.4.14",
4
4
  "private": false,
5
5
  "description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
6
6
  "type": "module",