squad-openclaw 2026.2.2017 → 2026.2.2019

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 (3) hide show
  1. package/README.md +33 -3
  2. package/dist/index.js +160 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,18 +15,48 @@ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity regis
15
15
 
16
16
  ## State Directory Resolution
17
17
 
18
- All paths in this plugin (and throughout this README) that reference `~/.openclaw` resolve via the `OPENCLAW_STATE_DIR` environment variable when set. This supports Docker and other containerized deployments where the OpenClaw data directory may not be at the default location.
18
+ All paths in this plugin (and throughout this README) that reference `~/.openclaw` resolve via environment override when set. This supports Docker and other containerized deployments where the OpenClaw data directory may not be at the default location.
19
+
20
+ Resolution priority:
21
+
22
+ 1. `OPENCLAW_STATE_DIR` (canonical)
23
+ 2. `OPENCLAW_DIR` (compat alias)
24
+ 3. `os.homedir() + "/.openclaw"` (default)
19
25
 
20
26
  | Environment | Typical path | How it's resolved |
21
27
  |---|---|---|
22
28
  | Standard install | `~/.openclaw` | Default — `os.homedir() + "/.openclaw"` |
23
- | Docker | `/root/data/.openclaw` | `OPENCLAW_STATE_DIR=/root/data/.openclaw` |
29
+ | Docker | `/data/.openclaw` | `OPENCLAW_STATE_DIR=/data/.openclaw` (or `OPENCLAW_DIR=/data/.openclaw`) |
24
30
  | Custom / NAS | `/mnt/data/.openclaw` | `OPENCLAW_STATE_DIR=/mnt/data/.openclaw` |
25
31
 
26
32
  **This variable only controls where the plugin looks for OpenClaw's own data directory.** It does not grant the plugin access to the parent directory or any other part of the filesystem. All security restrictions (blocked directories, allowed roots, write protection) are enforced relative to the resolved state directory — not the filesystem root.
27
33
 
28
34
  The resolution logic lives in a single shared module ([`src/paths.ts`](src/paths.ts)) imported by every file that needs the state directory path.
29
35
 
36
+ ### Docker Workspace Mapping
37
+
38
+ If your container mounts the real workspace outside the OpenClaw state directory (for example, `/data/workspace`), create a symlink so OpenClaw-compatible tools can still resolve `.../.openclaw/workspace`:
39
+
40
+ ```bash
41
+ mkdir -p /data/.openclaw /data/workspace
42
+ ln -sfn /data/workspace /data/.openclaw/workspace
43
+ ```
44
+
45
+ Then start your gateway/server with explicit env vars (useful when `.env` is not loaded):
46
+
47
+ ```bash
48
+ PORT=3334 \
49
+ OPENCLAW_STATE_DIR=/data/.openclaw \
50
+ OPENCLAW_DIR=/data/.openclaw \
51
+ node server.js
52
+ ```
53
+
54
+ Quick verification:
55
+
56
+ ```bash
57
+ ls -ld /data/.openclaw /data/.openclaw/workspace /data/workspace /data/.openclaw/openclaw.json
58
+ ```
59
+
30
60
  ## Security Model
31
61
 
32
62
  This plugin enforces a **defense-in-depth** security model with four independent layers. All security rules are hard-coded and non-configurable (except `allowedRoots`) so they can be verified by reading the source code. The bundle is intentionally **not minified** to allow security auditing of the distributed code.
@@ -231,7 +261,7 @@ Configure in your gateway's `openclaw.json` under the plugin section:
231
261
 
232
262
  | Key | Type | Default | Description |
233
263
  |---|---|---|---|
234
- | `fs.allowedRoots` | `string[]` | `["~/.openclaw"]` | Restrict filesystem operations to these directories. Hardcoded blocks on `credentials/`, `devices/`, `identity/`, `relay/squad-relay.json`, and `.bak` files always apply regardless. |
264
+ | `fs.allowedRoots` | `string[]` | `[resolved OpenClaw state dir]` | Restrict filesystem operations to these directories. Hardcoded blocks on `credentials/`, `devices/`, `identity/`, `relay/squad-relay.json`, and `.bak` files always apply regardless. |
235
265
 
236
266
  ## Source Code
237
267
 
package/dist/index.js CHANGED
@@ -105,7 +105,7 @@ function registerAgentMethods(api) {
105
105
  // src/entities.ts
106
106
  import { Type as T } from "@sinclair/typebox";
107
107
  import path4 from "path";
108
- import fs3 from "fs";
108
+ import fs4 from "fs";
109
109
 
110
110
  // src/watcher.ts
111
111
  import path from "path";
@@ -360,14 +360,31 @@ function startWatcher(configDir, onFsChange) {
360
360
  }
361
361
 
362
362
  // src/filesystem.ts
363
- import fs2 from "fs";
363
+ import fs3 from "fs";
364
364
  import path3 from "path";
365
365
 
366
366
  // src/paths.ts
367
367
  import path2 from "path";
368
368
  import os from "os";
369
+ import fs2 from "fs";
369
370
  function getOpenclawStateDir() {
370
- return process.env.OPENCLAW_STATE_DIR || path2.join(os.homedir(), ".openclaw");
371
+ if (process.env.OPENCLAW_STATE_DIR) {
372
+ return process.env.OPENCLAW_STATE_DIR;
373
+ }
374
+ if (process.env.OPENCLAW_CONFIG_PATH) {
375
+ return path2.dirname(process.env.OPENCLAW_CONFIG_PATH);
376
+ }
377
+ const legacyDir = process.env.OPENCLAW_DIR;
378
+ if (legacyDir) {
379
+ const resolvedLegacyDir = path2.resolve(legacyDir);
380
+ const configPath = path2.join(resolvedLegacyDir, "openclaw.json");
381
+ const hasStateMarkers = fs2.existsSync(configPath) || fs2.existsSync(path2.join(resolvedLegacyDir, "agents")) || fs2.existsSync(path2.join(resolvedLegacyDir, "workspace"));
382
+ const looksLikeStateDir = resolvedLegacyDir.endsWith(`${path2.sep}.openclaw`);
383
+ if (hasStateMarkers || looksLikeStateDir) {
384
+ return resolvedLegacyDir;
385
+ }
386
+ }
387
+ return path2.join(os.homedir(), ".openclaw");
371
388
  }
372
389
 
373
390
  // src/filesystem.ts
@@ -491,7 +508,7 @@ function err(message) {
491
508
  };
492
509
  }
493
510
  function listDir(dirPath, opts) {
494
- const dirents = fs2.readdirSync(dirPath, { withFileTypes: true });
511
+ const dirents = fs3.readdirSync(dirPath, { withFileTypes: true });
495
512
  const results = [];
496
513
  for (const dirent of dirents) {
497
514
  if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
@@ -502,7 +519,7 @@ function listDir(dirPath, opts) {
502
519
  else if (dirent.isSymbolicLink()) type = "symlink";
503
520
  const entry = { name: dirent.name, path: entryPath, type };
504
521
  try {
505
- const stat = fs2.statSync(entryPath);
522
+ const stat = fs3.statSync(entryPath);
506
523
  entry.size = stat.size;
507
524
  entry.modified = stat.mtime.toISOString();
508
525
  } catch {
@@ -526,7 +543,7 @@ function filterSensitiveEntries(entries) {
526
543
  });
527
544
  }
528
545
  function registerFilesystemTools(api) {
529
- const DEFAULT_ALLOWED_ROOTS = ["~/.openclaw"];
546
+ const DEFAULT_ALLOWED_ROOTS = [OPENCLAW_DIR];
530
547
  const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? DEFAULT_ALLOWED_ROOTS;
531
548
  api.registerTool({
532
549
  name: "fs_read",
@@ -551,8 +568,8 @@ function registerFilesystemTools(api) {
551
568
  try {
552
569
  const filePath = validateAndBlockSensitive(params.path, allowedRoots);
553
570
  const encoding = params.encoding ?? "utf-8";
554
- let content = fs2.readFileSync(filePath, encoding);
555
- const stat = fs2.statSync(filePath);
571
+ let content = fs3.readFileSync(filePath, encoding);
572
+ const stat = fs3.statSync(filePath);
556
573
  if (isOpenclawJson(filePath) && encoding === "utf-8") {
557
574
  content = redactOpenclawJson(content);
558
575
  }
@@ -602,10 +619,10 @@ function registerFilesystemTools(api) {
602
619
  const encoding = params.encoding ?? "utf-8";
603
620
  const mkdir = params.mkdir !== false;
604
621
  if (mkdir) {
605
- fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
622
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
606
623
  }
607
- fs2.writeFileSync(filePath, content, encoding);
608
- const stat = fs2.statSync(filePath);
624
+ fs3.writeFileSync(filePath, content, encoding);
625
+ const stat = fs3.statSync(filePath);
609
626
  return ok({
610
627
  path: filePath,
611
628
  size: stat.size,
@@ -674,7 +691,7 @@ function registerFilesystemTools(api) {
674
691
  async execute(_id, params) {
675
692
  try {
676
693
  const targetPath = validateWritePath(params.path, allowedRoots);
677
- fs2.mkdirSync(targetPath, { recursive: true });
694
+ fs3.mkdirSync(targetPath, { recursive: true });
678
695
  return ok({
679
696
  path: targetPath,
680
697
  created: true
@@ -707,7 +724,7 @@ function registerFilesystemTools(api) {
707
724
  try {
708
725
  const resolvedOld = validateWritePath(params.oldPath, allowedRoots);
709
726
  const resolvedNew = validateWritePath(params.newPath, allowedRoots);
710
- fs2.renameSync(resolvedOld, resolvedNew);
727
+ fs3.renameSync(resolvedOld, resolvedNew);
711
728
  return ok({
712
729
  oldPath: resolvedOld,
713
730
  newPath: resolvedNew,
@@ -736,12 +753,12 @@ function registerFilesystemTools(api) {
736
753
  async execute(_id, params) {
737
754
  try {
738
755
  const targetPath = validateWritePath(params.path, allowedRoots);
739
- const stat = fs2.statSync(targetPath);
756
+ const stat = fs3.statSync(targetPath);
740
757
  const wasDirectory = stat.isDirectory();
741
758
  if (wasDirectory) {
742
- fs2.rmSync(targetPath, { recursive: true });
759
+ fs3.rmSync(targetPath, { recursive: true });
743
760
  } else {
744
- fs2.unlinkSync(targetPath);
761
+ fs3.unlinkSync(targetPath);
745
762
  }
746
763
  return ok({
747
764
  path: targetPath,
@@ -793,7 +810,7 @@ function scanAgents(configDir) {
793
810
  const now = Date.now();
794
811
  let entries;
795
812
  try {
796
- entries = fs3.readdirSync(configDir, { withFileTypes: true });
813
+ entries = fs4.readdirSync(configDir, { withFileTypes: true });
797
814
  } catch {
798
815
  return;
799
816
  }
@@ -807,7 +824,7 @@ function scanAgents(configDir) {
807
824
  const metadata = { workspacePath };
808
825
  const identityPath = path4.join(workspacePath, "IDENTITY.md");
809
826
  try {
810
- const content = fs3.readFileSync(identityPath, "utf-8");
827
+ const content = fs4.readFileSync(identityPath, "utf-8");
811
828
  const parsed = parseIdentityName(content);
812
829
  if (parsed) name = parsed;
813
830
  } catch {
@@ -815,7 +832,7 @@ function scanAgents(configDir) {
815
832
  if (name === agentId) {
816
833
  const agentJsonPath = path4.join(workspacePath, "agent.json");
817
834
  try {
818
- const raw = fs3.readFileSync(agentJsonPath, "utf-8");
835
+ const raw = fs4.readFileSync(agentJsonPath, "utf-8");
819
836
  const config = JSON.parse(raw);
820
837
  if (config.displayName) name = config.displayName;
821
838
  if (config.model) metadata.model = config.model;
@@ -844,7 +861,7 @@ function scanSkills(configDir) {
844
861
  scanSkillsDir(globalSkillsDir, "global", now);
845
862
  let entries;
846
863
  try {
847
- entries = fs3.readdirSync(configDir, { withFileTypes: true });
864
+ entries = fs4.readdirSync(configDir, { withFileTypes: true });
848
865
  } catch {
849
866
  return;
850
867
  }
@@ -860,7 +877,7 @@ function scanSkills(configDir) {
860
877
  function scanSkillsDir(skillsDir, scope, now) {
861
878
  let entries;
862
879
  try {
863
- entries = fs3.readdirSync(skillsDir, { withFileTypes: true });
880
+ entries = fs4.readdirSync(skillsDir, { withFileTypes: true });
864
881
  } catch {
865
882
  return;
866
883
  }
@@ -871,7 +888,7 @@ function scanSkillsDir(skillsDir, scope, now) {
871
888
  let name = skillKey;
872
889
  for (const manifestName of ["manifest.json", "package.json"]) {
873
890
  try {
874
- const raw = fs3.readFileSync(
891
+ const raw = fs4.readFileSync(
875
892
  path4.join(skillPath, manifestName),
876
893
  "utf-8"
877
894
  );
@@ -902,7 +919,7 @@ function scanPlugins2(configDir) {
902
919
  const extensionsDir = path4.join(configDir, "extensions");
903
920
  let entries;
904
921
  try {
905
- entries = fs3.readdirSync(extensionsDir, { withFileTypes: true });
922
+ entries = fs4.readdirSync(extensionsDir, { withFileTypes: true });
906
923
  } catch {
907
924
  return;
908
925
  }
@@ -911,7 +928,7 @@ function scanPlugins2(configDir) {
911
928
  const pluginDir = path4.join(extensionsDir, dir.name);
912
929
  const manifestPath = path4.join(pluginDir, "openclaw.plugin.json");
913
930
  try {
914
- const raw = fs3.readFileSync(manifestPath, "utf-8");
931
+ const raw = fs4.readFileSync(manifestPath, "utf-8");
915
932
  const manifest = JSON.parse(raw);
916
933
  const pluginId = manifest.id || dir.name;
917
934
  const name = manifest.name || pluginId;
@@ -934,7 +951,7 @@ function scanPlugins2(configDir) {
934
951
  function scanTools(configDir) {
935
952
  const now = Date.now();
936
953
  try {
937
- const raw = fs3.readFileSync(
954
+ const raw = fs4.readFileSync(
938
955
  path4.join(configDir, "openclaw.json"),
939
956
  "utf-8"
940
957
  );
@@ -997,7 +1014,7 @@ function scanMedia(configDir) {
997
1014
  function scanMediaDir(dirPath, now) {
998
1015
  let entries;
999
1016
  try {
1000
- entries = fs3.readdirSync(dirPath, { withFileTypes: true });
1017
+ entries = fs4.readdirSync(dirPath, { withFileTypes: true });
1001
1018
  } catch {
1002
1019
  return;
1003
1020
  }
@@ -1024,7 +1041,7 @@ function scanMediaDir(dirPath, now) {
1024
1041
  let size;
1025
1042
  let mtime = now;
1026
1043
  try {
1027
- const stat = fs3.statSync(entryPath);
1044
+ const stat = fs4.statSync(entryPath);
1028
1045
  size = stat.size;
1029
1046
  mtime = stat.mtimeMs;
1030
1047
  } catch {
@@ -1142,7 +1159,7 @@ function registerEntityTools(api, onFsChange) {
1142
1159
  // src/sql.ts
1143
1160
  import { execFile } from "child_process";
1144
1161
  import path5 from "path";
1145
- import fs4 from "fs";
1162
+ import fs5 from "fs";
1146
1163
  import { Type as T2 } from "@sinclair/typebox";
1147
1164
  var HOME_DIR2 = process.env.HOME ?? "/root";
1148
1165
  var ALLOWED_DATA_DIR = path5.join(getOpenclawStateDir(), "squad-ceo-data");
@@ -1158,7 +1175,7 @@ function validateDbPath(dbPath) {
1158
1175
  );
1159
1176
  }
1160
1177
  try {
1161
- const stat = fs4.statSync(resolved);
1178
+ const stat = fs5.statSync(resolved);
1162
1179
  if (!stat.isFile()) {
1163
1180
  throw new Error(`Not a file: ${dbPath}`);
1164
1181
  }
@@ -1225,7 +1242,7 @@ function registerSqlTools(api) {
1225
1242
 
1226
1243
  // src/version.ts
1227
1244
  import { execSync as execSync2 } from "child_process";
1228
- import fs5 from "fs";
1245
+ import fs6 from "fs";
1229
1246
  import path6 from "path";
1230
1247
  import { fileURLToPath } from "url";
1231
1248
  var PACKAGE_NAME = "squad-openclaw";
@@ -1236,7 +1253,7 @@ var VERIFY_INTERVAL_MS = 500;
1236
1253
  var RESTART_BUFFER_MS = 5e3;
1237
1254
  function readInstalledVersionFromConfig() {
1238
1255
  try {
1239
- const raw = fs5.readFileSync(CONFIG_PATH, "utf-8");
1256
+ const raw = fs6.readFileSync(CONFIG_PATH, "utf-8");
1240
1257
  const cfg = JSON.parse(raw);
1241
1258
  const v = cfg?.plugins?.installs?.[PACKAGE_NAME]?.version;
1242
1259
  return typeof v === "string" ? v : null;
@@ -1247,7 +1264,7 @@ function readInstalledVersionFromConfig() {
1247
1264
  function reconcileInstallMetadata(verification) {
1248
1265
  if (!verification.installPath || !verification.packageVersion) return;
1249
1266
  try {
1250
- const raw = fs5.readFileSync(CONFIG_PATH, "utf-8");
1267
+ const raw = fs6.readFileSync(CONFIG_PATH, "utf-8");
1251
1268
  const config = JSON.parse(raw);
1252
1269
  if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
1253
1270
  if (!config.plugins.installs || typeof config.plugins.installs !== "object") {
@@ -1270,7 +1287,7 @@ function reconcileInstallMetadata(verification) {
1270
1287
  ...entry,
1271
1288
  enabled: true
1272
1289
  };
1273
- fs5.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1290
+ fs6.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1274
1291
  } catch {
1275
1292
  }
1276
1293
  }
@@ -1278,7 +1295,7 @@ function getCurrentVersion() {
1278
1295
  const thisFile = fileURLToPath(import.meta.url);
1279
1296
  const pkgPath = path6.resolve(path6.dirname(thisFile), "..", "package.json");
1280
1297
  try {
1281
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1298
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
1282
1299
  return pkg.version ?? "0.0.0";
1283
1300
  } catch {
1284
1301
  return "0.0.0";
@@ -1323,7 +1340,7 @@ function compareVersions(a, b) {
1323
1340
  function verifyInstalledPluginState() {
1324
1341
  let configRaw;
1325
1342
  try {
1326
- configRaw = fs5.readFileSync(CONFIG_PATH, "utf-8");
1343
+ configRaw = fs6.readFileSync(CONFIG_PATH, "utf-8");
1327
1344
  } catch (err2) {
1328
1345
  const msg = err2 instanceof Error ? err2.message : String(err2);
1329
1346
  return {
@@ -1367,7 +1384,7 @@ function verifyInstalledPluginState() {
1367
1384
  path6.join(installPath, "openclaw.plugin.json"),
1368
1385
  path6.join(installPath, "dist", "index.js")
1369
1386
  ];
1370
- const requiredFilesMissing = requiredFiles.filter((p) => !fs5.existsSync(p));
1387
+ const requiredFilesMissing = requiredFiles.filter((p) => !fs6.existsSync(p));
1371
1388
  if (requiredFilesMissing.length > 0) {
1372
1389
  return {
1373
1390
  ok: false,
@@ -1381,7 +1398,7 @@ function verifyInstalledPluginState() {
1381
1398
  let installedPackage;
1382
1399
  try {
1383
1400
  installedPackage = JSON.parse(
1384
- fs5.readFileSync(path6.join(installPath, "package.json"), "utf-8")
1401
+ fs6.readFileSync(path6.join(installPath, "package.json"), "utf-8")
1385
1402
  );
1386
1403
  } catch (err2) {
1387
1404
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -1396,7 +1413,7 @@ function verifyInstalledPluginState() {
1396
1413
  }
1397
1414
  try {
1398
1415
  JSON.parse(
1399
- fs5.readFileSync(path6.join(installPath, "openclaw.plugin.json"), "utf-8")
1416
+ fs6.readFileSync(path6.join(installPath, "openclaw.plugin.json"), "utf-8")
1400
1417
  );
1401
1418
  } catch (err2) {
1402
1419
  const msg = err2 instanceof Error ? err2.message : String(err2);
@@ -1486,7 +1503,7 @@ function registerVersionMethods(api) {
1486
1503
  let updateOutput = "";
1487
1504
  let configBackup = null;
1488
1505
  try {
1489
- configBackup = fs5.readFileSync(CONFIG_PATH, "utf-8");
1506
+ configBackup = fs6.readFileSync(CONFIG_PATH, "utf-8");
1490
1507
  } catch {
1491
1508
  }
1492
1509
  runDoctorFixSilently();
@@ -1505,7 +1522,7 @@ function registerVersionMethods(api) {
1505
1522
  } catch (installErr) {
1506
1523
  if (configBackup) {
1507
1524
  try {
1508
- fs5.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1525
+ fs6.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1509
1526
  } catch {
1510
1527
  }
1511
1528
  }
@@ -1523,7 +1540,7 @@ function registerVersionMethods(api) {
1523
1540
  if (!verification.ok) {
1524
1541
  if (configBackup) {
1525
1542
  try {
1526
- fs5.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1543
+ fs6.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1527
1544
  } catch {
1528
1545
  }
1529
1546
  }
@@ -1581,7 +1598,7 @@ function registerVersionMethods(api) {
1581
1598
  // src/relay-client.ts
1582
1599
  import { WebSocket as NodeWebSocket } from "ws";
1583
1600
  import crypto3 from "crypto";
1584
- import fs7 from "fs";
1601
+ import fs8 from "fs";
1585
1602
  import path8 from "path";
1586
1603
 
1587
1604
  // src/e2e-crypto.ts
@@ -1663,24 +1680,24 @@ var E2ECrypto = class {
1663
1680
 
1664
1681
  // src/device-keys.ts
1665
1682
  import crypto2 from "crypto";
1666
- import fs6 from "fs";
1683
+ import fs7 from "fs";
1667
1684
  import path7 from "path";
1668
1685
  var RELAY_DATA_DIR = path7.join(getOpenclawStateDir(), "squad-ceo-data", "relay");
1669
1686
  var RELAY_STATE_PATH = path7.join(RELAY_DATA_DIR, "squad-relay.json");
1670
1687
  var PENDING_APPROVAL_PATH = path7.join(RELAY_DATA_DIR, "pending-approval.json");
1671
1688
  function readRelayState() {
1672
1689
  try {
1673
- const raw = fs6.readFileSync(RELAY_STATE_PATH, "utf-8");
1690
+ const raw = fs7.readFileSync(RELAY_STATE_PATH, "utf-8");
1674
1691
  return JSON.parse(raw);
1675
1692
  } catch {
1676
1693
  return {};
1677
1694
  }
1678
1695
  }
1679
1696
  function writeRelayState(state) {
1680
- if (!fs6.existsSync(RELAY_DATA_DIR)) {
1681
- fs6.mkdirSync(RELAY_DATA_DIR, { recursive: true });
1697
+ if (!fs7.existsSync(RELAY_DATA_DIR)) {
1698
+ fs7.mkdirSync(RELAY_DATA_DIR, { recursive: true });
1682
1699
  }
1683
- fs6.writeFileSync(RELAY_STATE_PATH, JSON.stringify(state, null, 2), { mode: 384 });
1700
+ fs7.writeFileSync(RELAY_STATE_PATH, JSON.stringify(state, null, 2), { mode: 384 });
1684
1701
  }
1685
1702
  function toBase64Url(buf) {
1686
1703
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -1712,7 +1729,7 @@ function writeDeviceInfoFile(keys) {
1712
1729
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1713
1730
  };
1714
1731
  try {
1715
- fs6.writeFileSync(infoPath, JSON.stringify(info, null, 2));
1732
+ fs7.writeFileSync(infoPath, JSON.stringify(info, null, 2));
1716
1733
  } catch (err2) {
1717
1734
  console.error("[device-keys] Failed to write relay-device-info.json:", err2);
1718
1735
  }
@@ -1723,7 +1740,7 @@ function readOperatorToken() {
1723
1740
  const stateDir = getOpenclawStateDir();
1724
1741
  const configPath = path8.join(stateDir, "openclaw.json");
1725
1742
  try {
1726
- const raw = fs7.readFileSync(configPath, "utf-8");
1743
+ const raw = fs8.readFileSync(configPath, "utf-8");
1727
1744
  const config = JSON.parse(raw);
1728
1745
  return config?.gateway?.auth?.token ?? config?.gateway?.remote?.token ?? config?.gateway?.token ?? null;
1729
1746
  } catch {
@@ -1739,7 +1756,7 @@ function readGatewayLocalWsConfig() {
1739
1756
  const stateDir = getOpenclawStateDir();
1740
1757
  const configPath = path8.join(stateDir, "openclaw.json");
1741
1758
  try {
1742
- const raw = fs7.readFileSync(configPath, "utf-8");
1759
+ const raw = fs8.readFileSync(configPath, "utf-8");
1743
1760
  const config = JSON.parse(raw);
1744
1761
  const parsedPort = Number(config?.gateway?.port);
1745
1762
  if (Number.isFinite(parsedPort) && parsedPort > 0) {
@@ -2285,6 +2302,88 @@ function broadcastToUsers(event, payload) {
2285
2302
  relayClient?.broadcastToUsers(event, payload);
2286
2303
  }
2287
2304
 
2305
+ // src/layout.ts
2306
+ import fs9 from "fs";
2307
+ import path9 from "path";
2308
+ function resolveMaybeRelativePath(stateDir, p) {
2309
+ if (path9.isAbsolute(p)) return path9.resolve(p);
2310
+ return path9.resolve(stateDir, p);
2311
+ }
2312
+ function listWorkspaceFallbacks(stateDir) {
2313
+ let entries;
2314
+ try {
2315
+ entries = fs9.readdirSync(stateDir, { withFileTypes: true });
2316
+ } catch {
2317
+ return [];
2318
+ }
2319
+ return entries.filter((entry) => entry.isDirectory() && (entry.name === "workspace" || entry.name.startsWith("workspace-"))).map((entry) => {
2320
+ const agentId = entry.name === "workspace" ? "main" : entry.name.replace("workspace-", "");
2321
+ const workspacePath = path9.join(stateDir, entry.name);
2322
+ return {
2323
+ agentId,
2324
+ path: workspacePath,
2325
+ source: "filesystem",
2326
+ exists: true
2327
+ };
2328
+ });
2329
+ }
2330
+ function readOpenclawConfig(configPath) {
2331
+ try {
2332
+ const raw = fs9.readFileSync(configPath, "utf-8");
2333
+ return JSON.parse(raw);
2334
+ } catch {
2335
+ return null;
2336
+ }
2337
+ }
2338
+ function resolveGatewayLayout() {
2339
+ const stateDir = getOpenclawStateDir();
2340
+ const configPath = path9.join(stateDir, "openclaw.json");
2341
+ const config = readOpenclawConfig(configPath);
2342
+ const workspaces = [];
2343
+ if (config?.agents?.main?.workspace || config?.agents?.main?.workspacePath) {
2344
+ const rawPath = config.agents.main.workspace ?? config.agents.main.workspacePath;
2345
+ if (rawPath) {
2346
+ const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
2347
+ workspaces.push({
2348
+ agentId: "main",
2349
+ path: resolvedPath,
2350
+ source: "config",
2351
+ exists: fs9.existsSync(resolvedPath)
2352
+ });
2353
+ }
2354
+ }
2355
+ for (const agent of config?.agents?.list ?? []) {
2356
+ const agentId = typeof agent.id === "string" && agent.id.trim() ? agent.id : null;
2357
+ const rawPath = agent.workspace ?? agent.workspacePath;
2358
+ if (!agentId || !rawPath) continue;
2359
+ const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
2360
+ workspaces.push({
2361
+ agentId,
2362
+ path: resolvedPath,
2363
+ source: "config",
2364
+ exists: fs9.existsSync(resolvedPath)
2365
+ });
2366
+ }
2367
+ const deduped = /* @__PURE__ */ new Map();
2368
+ for (const ws of [...workspaces, ...listWorkspaceFallbacks(stateDir)]) {
2369
+ if (!deduped.has(ws.agentId)) {
2370
+ deduped.set(ws.agentId, ws);
2371
+ }
2372
+ }
2373
+ const resolvedWorkspaces = Array.from(deduped.values());
2374
+ const mainWorkspace = resolvedWorkspaces.find((ws) => ws.agentId === "main");
2375
+ const defaultFileBrowserRoot = mainWorkspace?.path ?? stateDir;
2376
+ return {
2377
+ stateDir,
2378
+ configPath,
2379
+ mediaDir: path9.join(stateDir, "media"),
2380
+ skillsDir: path9.join(stateDir, "skills"),
2381
+ extensionsDir: path9.join(stateDir, "extensions"),
2382
+ defaultFileBrowserRoot,
2383
+ workspaces: resolvedWorkspaces
2384
+ };
2385
+ }
2386
+
2288
2387
  // src/index.ts
2289
2388
  function squadAppPlugin(api) {
2290
2389
  const toolExecutors = /* @__PURE__ */ new Map();
@@ -2367,6 +2466,17 @@ function squadAppPlugin(api) {
2367
2466
  respond(true, { tools: [...coreTools, ...groups, ...pluginTools] });
2368
2467
  }
2369
2468
  );
2469
+ api.registerGatewayMethod(
2470
+ "squad.layout.get",
2471
+ async ({ respond }) => {
2472
+ try {
2473
+ respond(true, resolveGatewayLayout());
2474
+ } catch (err2) {
2475
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2476
+ respond(false, { errorMessage: msg });
2477
+ }
2478
+ }
2479
+ );
2370
2480
  const relayState = readRelayState();
2371
2481
  const relayEnabled = !!(relayState.claimToken || relayState.roomId);
2372
2482
  if (relayEnabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squad-openclaw",
3
- "version": "2026.2.2017",
3
+ "version": "2026.2.2019",
4
4
  "description": "Entity registry, filesystem tools, and version management plugin for OpenClaw gateway",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",