@alfe.ai/integrations 0.0.30 → 0.0.31

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/dist/index.js +102 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1509,18 +1509,63 @@ var IntegrationManager = class {
1509
1509
  const execFileAsync = promisify(execFile);
1510
1510
  const log = createLogger("OpenClawApplier");
1511
1511
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
1512
- /**
1513
- * Flatten a nested config object into dot-path key-value pairs.
1514
- * e.g. { gateway: { bind: "lan" } } → [["gateway.bind", "lan"]]
1515
- */
1516
1512
  function flattenConfig(obj, prefix = "") {
1517
- const pairs = [];
1513
+ const entries = [];
1518
1514
  for (const [key, val] of Object.entries(obj)) {
1515
+ if (key.includes(".")) {
1516
+ entries.push({
1517
+ kind: "subtree",
1518
+ parentPath: prefix,
1519
+ dottedKey: key,
1520
+ value: val
1521
+ });
1522
+ continue;
1523
+ }
1519
1524
  const path = prefix ? `${prefix}.${key}` : key;
1520
- if (val !== null && typeof val === "object" && !Array.isArray(val)) pairs.push(...flattenConfig(val, path));
1521
- else pairs.push([path, val]);
1525
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) entries.push(...flattenConfig(val, path));
1526
+ else entries.push({
1527
+ kind: "leaf",
1528
+ path,
1529
+ value: val
1530
+ });
1522
1531
  }
1523
- return pairs;
1532
+ return entries;
1533
+ }
1534
+ function partitionEntries(entries) {
1535
+ const leaves = [];
1536
+ const subtreesByParent = /* @__PURE__ */ new Map();
1537
+ for (const entry of entries) {
1538
+ if (entry.kind === "leaf") {
1539
+ leaves.push({
1540
+ path: entry.path,
1541
+ value: entry.value
1542
+ });
1543
+ continue;
1544
+ }
1545
+ if (!entry.parentPath) throw new Error(`OpenClawApplier: top-level config keys containing dots are not supported (key: "${entry.dottedKey}")`);
1546
+ let bucket = subtreesByParent.get(entry.parentPath);
1547
+ if (!bucket) {
1548
+ bucket = /* @__PURE__ */ new Map();
1549
+ subtreesByParent.set(entry.parentPath, bucket);
1550
+ }
1551
+ bucket.set(entry.dottedKey, entry.value);
1552
+ }
1553
+ return {
1554
+ leaves,
1555
+ subtreesByParent
1556
+ };
1557
+ }
1558
+ async function readParentObject(parentPath) {
1559
+ try {
1560
+ const { stdout } = await execFileAsync("openclaw", [
1561
+ "config",
1562
+ "get",
1563
+ parentPath
1564
+ ], { timeout: 1e4 });
1565
+ const parsed = JSON.parse(stdout.trim());
1566
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1567
+ } catch {}
1568
+ return {};
1524
1569
  }
1525
1570
  var OpenClawApplier = class {
1526
1571
  runtime = "openclaw";
@@ -1623,8 +1668,8 @@ var OpenClawApplier = class {
1623
1668
  * plugins under 2026.5+, which made force-mode skip its uninstall step.
1624
1669
  */
1625
1670
  isPluginInstalled(pkg) {
1626
- if (existsSync(join(homedir(), ".openclaw", "npm", "node_modules", ...pkg.split("/")))) return true;
1627
- const extensionsDir = join(homedir(), ".openclaw", "extensions");
1671
+ if (existsSync(join(this.home, "npm", "node_modules", ...pkg.split("/")))) return true;
1672
+ const extensionsDir = join(this.home, "extensions");
1628
1673
  if (!existsSync(extensionsDir)) return false;
1629
1674
  const prefix = pkg.replaceAll("/", "-") + "-";
1630
1675
  return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
@@ -1695,21 +1740,36 @@ var OpenClawApplier = class {
1695
1740
  integrations[integrationId] = config;
1696
1741
  tracking._integrations = integrations;
1697
1742
  this.writeTracking(tracking);
1698
- const batch = flattenConfig(config).map(([path, value]) => ({
1699
- path,
1700
- value
1701
- }));
1702
- try {
1743
+ const { leaves, subtreesByParent } = partitionEntries(flattenConfig(config));
1744
+ for (const [parentPath, dottedKvs] of subtreesByParent) {
1745
+ const merged = { ...await readParentObject(parentPath) };
1746
+ for (const [k, v] of dottedKvs) merged[k] = v;
1747
+ try {
1748
+ await execFileAsync("openclaw", [
1749
+ "config",
1750
+ "set",
1751
+ parentPath,
1752
+ JSON.stringify(merged)
1753
+ ], { timeout: 1e4 });
1754
+ } catch (err) {
1755
+ log.error({
1756
+ err: err instanceof Error ? err.message : String(err),
1757
+ parentPath
1758
+ }, "Failed to set config subtree via openclaw config set");
1759
+ throw err;
1760
+ }
1761
+ }
1762
+ if (leaves.length > 0) try {
1703
1763
  await execFileAsync("openclaw", [
1704
1764
  "config",
1705
1765
  "set",
1706
1766
  "--batch-json",
1707
- JSON.stringify(batch)
1767
+ JSON.stringify(leaves)
1708
1768
  ], { timeout: 1e4 });
1709
1769
  } catch (err) {
1710
1770
  log.error({
1711
1771
  err: err instanceof Error ? err.message : String(err),
1712
- batch
1772
+ batch: leaves
1713
1773
  }, "Failed to set config via openclaw config set --batch-json");
1714
1774
  throw err;
1715
1775
  }
@@ -1725,8 +1785,8 @@ var OpenClawApplier = class {
1725
1785
  const integrations = tracking._integrations ?? {};
1726
1786
  if (!(integrationId in integrations)) return;
1727
1787
  const integrationConfig = integrations[integrationId];
1728
- const pairs = flattenConfig(integrationConfig);
1729
- for (const [path] of pairs) try {
1788
+ const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
1789
+ for (const { path } of leaves) try {
1730
1790
  await execFileAsync("openclaw", [
1731
1791
  "config",
1732
1792
  "unset",
@@ -1738,6 +1798,29 @@ var OpenClawApplier = class {
1738
1798
  path
1739
1799
  }, "Failed to unset config via openclaw config unset");
1740
1800
  }
1801
+ for (const [parentPath, dottedKvs] of subtreesByParent) {
1802
+ const existing = await readParentObject(parentPath);
1803
+ const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
1804
+ if (Object.keys(existing).length === 0) continue;
1805
+ try {
1806
+ if (Object.keys(remaining).length === 0) await execFileAsync("openclaw", [
1807
+ "config",
1808
+ "unset",
1809
+ parentPath
1810
+ ], { timeout: 1e4 });
1811
+ else await execFileAsync("openclaw", [
1812
+ "config",
1813
+ "set",
1814
+ parentPath,
1815
+ JSON.stringify(remaining)
1816
+ ], { timeout: 1e4 });
1817
+ } catch (err) {
1818
+ log.warn({
1819
+ err: err instanceof Error ? err.message : String(err),
1820
+ parentPath
1821
+ }, "Failed to update parent config during remove");
1822
+ }
1823
+ }
1741
1824
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1742
1825
  this.writeTracking(tracking);
1743
1826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",