@alfe.ai/integrations 0.0.30 → 0.0.32

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.
package/dist/index.d.ts CHANGED
@@ -796,6 +796,16 @@ declare class OpenClawApplier implements RuntimeApplier {
796
796
  * plugins under 2026.5+, which made force-mode skip its uninstall step.
797
797
  */
798
798
  private isPluginInstalled;
799
+ /**
800
+ * Remove an extensions/ install that has no matching record in
801
+ * plugins/installs.json. OpenClaw 2026.5+ tracks installs via the records
802
+ * file — a leftover dir from 2026.4 is invisible to `openclaw plugins
803
+ * uninstall` (it errors with "not managed by plugins config/install
804
+ * records") and its pre-migration manifest blocks the new effective-plugin-
805
+ * set loader from importing it. Resetting the dir makes the next install
806
+ * write through the npm path with a proper tracking record.
807
+ */
808
+ private cleanupUntrackedExtensionInstall;
799
809
  removePlugin(pkg: string): Promise<void>;
800
810
  applySkill(name: string, srcPath: string): Promise<void>;
801
811
  applyClawHubSkill(slug: string): Promise<void>;
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";
@@ -1538,6 +1583,7 @@ var OpenClawApplier = class {
1538
1583
  }
1539
1584
  async applyPlugin(pkg, _installPath, opts) {
1540
1585
  await this.ensurePluginsAllow(pkg);
1586
+ this.cleanupUntrackedExtensionInstall(pkg);
1541
1587
  if (opts?.force && this.isPluginInstalled(pkg)) {
1542
1588
  log.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1543
1589
  try {
@@ -1623,12 +1669,57 @@ var OpenClawApplier = class {
1623
1669
  * plugins under 2026.5+, which made force-mode skip its uninstall step.
1624
1670
  */
1625
1671
  isPluginInstalled(pkg) {
1626
- if (existsSync(join(homedir(), ".openclaw", "npm", "node_modules", ...pkg.split("/")))) return true;
1627
- const extensionsDir = join(homedir(), ".openclaw", "extensions");
1672
+ if (existsSync(join(this.home, "npm", "node_modules", ...pkg.split("/")))) return true;
1673
+ const extensionsDir = join(this.home, "extensions");
1628
1674
  if (!existsSync(extensionsDir)) return false;
1629
1675
  const prefix = pkg.replaceAll("/", "-") + "-";
1630
1676
  return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
1631
1677
  }
1678
+ /**
1679
+ * Remove an extensions/ install that has no matching record in
1680
+ * plugins/installs.json. OpenClaw 2026.5+ tracks installs via the records
1681
+ * file — a leftover dir from 2026.4 is invisible to `openclaw plugins
1682
+ * uninstall` (it errors with "not managed by plugins config/install
1683
+ * records") and its pre-migration manifest blocks the new effective-plugin-
1684
+ * set loader from importing it. Resetting the dir makes the next install
1685
+ * write through the npm path with a proper tracking record.
1686
+ */
1687
+ cleanupUntrackedExtensionInstall(pkg) {
1688
+ const installsPath = join(this.home, "plugins", "installs.json");
1689
+ if (!existsSync(installsPath)) return;
1690
+ let tracked = false;
1691
+ try {
1692
+ const raw = readFileSync(installsPath, "utf8");
1693
+ const data = JSON.parse(raw);
1694
+ tracked = Object.prototype.hasOwnProperty.call(data.installRecords ?? {}, pkg);
1695
+ } catch {
1696
+ return;
1697
+ }
1698
+ if (tracked) return;
1699
+ const extensionsDir = join(this.home, "extensions");
1700
+ if (!existsSync(extensionsDir)) return;
1701
+ const prefix = pkg.replaceAll("/", "-") + "-";
1702
+ const stale = readdirSync(extensionsDir).filter((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
1703
+ for (const dir of stale) {
1704
+ const fullPath = join(extensionsDir, dir);
1705
+ try {
1706
+ rmSync(fullPath, {
1707
+ recursive: true,
1708
+ force: true
1709
+ });
1710
+ log.info({
1711
+ pkg,
1712
+ removed: fullPath
1713
+ }, "Removed untracked extensions/ install — will reinstall via npm path");
1714
+ } catch (err) {
1715
+ log.warn({
1716
+ pkg,
1717
+ removed: fullPath,
1718
+ err: err instanceof Error ? err.message : String(err)
1719
+ }, "Failed to remove untracked extensions/ install");
1720
+ }
1721
+ }
1722
+ }
1632
1723
  async removePlugin(pkg) {
1633
1724
  await execFileAsync("openclaw", [
1634
1725
  "plugins",
@@ -1695,21 +1786,36 @@ var OpenClawApplier = class {
1695
1786
  integrations[integrationId] = config;
1696
1787
  tracking._integrations = integrations;
1697
1788
  this.writeTracking(tracking);
1698
- const batch = flattenConfig(config).map(([path, value]) => ({
1699
- path,
1700
- value
1701
- }));
1702
- try {
1789
+ const { leaves, subtreesByParent } = partitionEntries(flattenConfig(config));
1790
+ for (const [parentPath, dottedKvs] of subtreesByParent) {
1791
+ const merged = { ...await readParentObject(parentPath) };
1792
+ for (const [k, v] of dottedKvs) merged[k] = v;
1793
+ try {
1794
+ await execFileAsync("openclaw", [
1795
+ "config",
1796
+ "set",
1797
+ parentPath,
1798
+ JSON.stringify(merged)
1799
+ ], { timeout: 1e4 });
1800
+ } catch (err) {
1801
+ log.error({
1802
+ err: err instanceof Error ? err.message : String(err),
1803
+ parentPath
1804
+ }, "Failed to set config subtree via openclaw config set");
1805
+ throw err;
1806
+ }
1807
+ }
1808
+ if (leaves.length > 0) try {
1703
1809
  await execFileAsync("openclaw", [
1704
1810
  "config",
1705
1811
  "set",
1706
1812
  "--batch-json",
1707
- JSON.stringify(batch)
1813
+ JSON.stringify(leaves)
1708
1814
  ], { timeout: 1e4 });
1709
1815
  } catch (err) {
1710
1816
  log.error({
1711
1817
  err: err instanceof Error ? err.message : String(err),
1712
- batch
1818
+ batch: leaves
1713
1819
  }, "Failed to set config via openclaw config set --batch-json");
1714
1820
  throw err;
1715
1821
  }
@@ -1725,8 +1831,8 @@ var OpenClawApplier = class {
1725
1831
  const integrations = tracking._integrations ?? {};
1726
1832
  if (!(integrationId in integrations)) return;
1727
1833
  const integrationConfig = integrations[integrationId];
1728
- const pairs = flattenConfig(integrationConfig);
1729
- for (const [path] of pairs) try {
1834
+ const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
1835
+ for (const { path } of leaves) try {
1730
1836
  await execFileAsync("openclaw", [
1731
1837
  "config",
1732
1838
  "unset",
@@ -1738,6 +1844,29 @@ var OpenClawApplier = class {
1738
1844
  path
1739
1845
  }, "Failed to unset config via openclaw config unset");
1740
1846
  }
1847
+ for (const [parentPath, dottedKvs] of subtreesByParent) {
1848
+ const existing = await readParentObject(parentPath);
1849
+ const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
1850
+ if (Object.keys(existing).length === 0) continue;
1851
+ try {
1852
+ if (Object.keys(remaining).length === 0) await execFileAsync("openclaw", [
1853
+ "config",
1854
+ "unset",
1855
+ parentPath
1856
+ ], { timeout: 1e4 });
1857
+ else await execFileAsync("openclaw", [
1858
+ "config",
1859
+ "set",
1860
+ parentPath,
1861
+ JSON.stringify(remaining)
1862
+ ], { timeout: 1e4 });
1863
+ } catch (err) {
1864
+ log.warn({
1865
+ err: err instanceof Error ? err.message : String(err),
1866
+ parentPath
1867
+ }, "Failed to update parent config during remove");
1868
+ }
1869
+ }
1741
1870
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1742
1871
  this.writeTracking(tracking);
1743
1872
  }
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.32",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",