@matelink/cli 2026.4.13 → 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 +230 -3
  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,6 +2928,14 @@ async function callGatewayRpcLocal({
2709
2928
  };
2710
2929
  }
2711
2930
 
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
+
2712
2939
  // Prefer CLI first for broad compatibility, but fall back to the persistent
2713
2940
  // WS client when the CLI gateway call hits transient websocket closures.
2714
2941
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matelink/cli",
3
- "version": "2026.4.13",
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",