@alfe.ai/integrations 0.0.29 → 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.
package/dist/index.d.ts CHANGED
@@ -245,6 +245,10 @@ interface ConfigSchemaField {
245
245
  select_options?: SelectOption[];
246
246
  /** OAuth provider identifier for oauth_connect fields */
247
247
  oauth_provider?: string;
248
+ /** Force specific scope groups for OAuth (e.g. ["chat"]) — hides other scopes in the dashboard */
249
+ oauth_scopes?: string[];
250
+ /** Integration ID to patch on OAuth callback (when using a shared OAuth provider) */
251
+ oauth_integration_id?: string;
248
252
  /** If true, this field is not shown in the dashboard UI (install wizard or configure modal) */
249
253
  hidden?: boolean;
250
254
  /** Only show this field if another field has a truthy value (string) or matches a specific value (object) */
@@ -748,16 +752,30 @@ declare function runHookWithContext(integrationPath: string, hookScript: string,
748
752
  //#endregion
749
753
  //#region src/appliers/openclaw-applier.d.ts
750
754
  interface OpenClawApplierOptions {
751
- /** Path to the OpenClaw workspace directory (e.g. ~/.openclaw) */
752
- workspace: string;
755
+ /**
756
+ * Path to the OpenClaw home directory (e.g. ~/.openclaw) — where openclaw.json,
757
+ * plugins/, and extensions/ live.
758
+ */
759
+ home?: string;
760
+ /**
761
+ * Path to the agent workspace directory (e.g. ~/.openclaw/workspace) — where
762
+ * the agent's CWD lives, including SOUL.md / AGENTS.md / skills/. Defaults to
763
+ * `${home}/workspace` (OpenClaw's default for `agents.defaults.workspace`).
764
+ */
765
+ agentWorkspace?: string;
766
+ /**
767
+ * @deprecated Use `home` instead. Kept for back-compat — when set, used as `home`.
768
+ */
769
+ workspace?: string;
753
770
  /** Override skills directory (defaults to ~/.alfe/skills/) */
754
771
  skillsDir?: string;
755
- /** Path to the integration tracking file (defaults to {workspace}/config.json) */
772
+ /** Path to the integration tracking file (defaults to {home}/config.json) */
756
773
  configPath?: string;
757
774
  }
758
775
  declare class OpenClawApplier implements RuntimeApplier {
759
776
  readonly runtime = "openclaw";
760
- private workspace;
777
+ private home;
778
+ private agentWorkspace;
761
779
  private skillsDir;
762
780
  private trackingPath;
763
781
  constructor(options: OpenClawApplierOptions);
@@ -770,8 +788,12 @@ declare class OpenClawApplier implements RuntimeApplier {
770
788
  */
771
789
  private ensurePluginsAllow;
772
790
  /**
773
- * Check if a plugin is already installed in ~/.openclaw/extensions/.
774
- * OpenClaw names extension dirs as {pkg-name-with-dashes}-{hash}.
791
+ * Check if a plugin is already installed.
792
+ *
793
+ * OpenClaw 2026.4 stored plugins under `~/.openclaw/extensions/{pkg-name-with-dashes}-{hash}`.
794
+ * 2026.5+ added an npm registry at `~/.openclaw/npm/node_modules/<pkg>` for npm-source
795
+ * plugins. We must check both — the extensions-only check missed npm-installed
796
+ * plugins under 2026.5+, which made force-mode skip its uninstall step.
775
797
  */
776
798
  private isPluginInstalled;
777
799
  removePlugin(pkg: string): Promise<void>;
package/dist/index.js CHANGED
@@ -1509,28 +1509,77 @@ 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
+ });
1531
+ }
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);
1522
1552
  }
1523
- return pairs;
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";
1527
- workspace;
1572
+ home;
1573
+ agentWorkspace;
1528
1574
  skillsDir;
1529
1575
  trackingPath;
1530
1576
  constructor(options) {
1531
- this.workspace = options.workspace;
1577
+ const home = options.home ?? options.workspace;
1578
+ if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
1579
+ this.home = home;
1580
+ this.agentWorkspace = options.agentWorkspace ?? join(home, "workspace");
1532
1581
  this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
1533
- this.trackingPath = options.configPath ?? join(this.workspace, "config.json");
1582
+ this.trackingPath = options.configPath ?? join(this.home, "config.json");
1534
1583
  }
1535
1584
  async applyPlugin(pkg, _installPath, opts) {
1536
1585
  await this.ensurePluginsAllow(pkg);
@@ -1546,25 +1595,22 @@ var OpenClawApplier = class {
1546
1595
  }
1547
1596
  }
1548
1597
  if (!this.isPluginInstalled(pkg)) {
1598
+ const baseArgs = [
1599
+ "plugins",
1600
+ "install",
1601
+ pkg,
1602
+ "--force"
1603
+ ];
1604
+ const useUnsafeFlag = pkg.startsWith("@alfe.ai/");
1605
+ const args = useUnsafeFlag ? [...baseArgs, "--dangerously-force-unsafe-install"] : baseArgs;
1549
1606
  try {
1550
- const args = [
1551
- "plugins",
1552
- "install",
1553
- pkg
1554
- ];
1555
- const useUnsafeFlag = pkg.startsWith("@alfe.ai/");
1556
- if (useUnsafeFlag) args.push("--dangerously-force-unsafe-install");
1557
1607
  try {
1558
1608
  await execFileAsync("openclaw", args, { timeout: 6e4 });
1559
1609
  } catch (err) {
1560
1610
  const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
1561
1611
  if (useUnsafeFlag && errText.includes("unknown option")) {
1562
1612
  log.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
1563
- await execFileAsync("openclaw", [
1564
- "plugins",
1565
- "install",
1566
- pkg
1567
- ], { timeout: 6e4 });
1613
+ await execFileAsync("openclaw", baseArgs, { timeout: 6e4 });
1568
1614
  } else throw err;
1569
1615
  }
1570
1616
  } catch (err) {
@@ -1614,11 +1660,16 @@ var OpenClawApplier = class {
1614
1660
  }
1615
1661
  }
1616
1662
  /**
1617
- * Check if a plugin is already installed in ~/.openclaw/extensions/.
1618
- * OpenClaw names extension dirs as {pkg-name-with-dashes}-{hash}.
1663
+ * Check if a plugin is already installed.
1664
+ *
1665
+ * OpenClaw 2026.4 stored plugins under `~/.openclaw/extensions/{pkg-name-with-dashes}-{hash}`.
1666
+ * 2026.5+ added an npm registry at `~/.openclaw/npm/node_modules/<pkg>` for npm-source
1667
+ * plugins. We must check both — the extensions-only check missed npm-installed
1668
+ * plugins under 2026.5+, which made force-mode skip its uninstall step.
1619
1669
  */
1620
1670
  isPluginInstalled(pkg) {
1621
- 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");
1622
1673
  if (!existsSync(extensionsDir)) return false;
1623
1674
  const prefix = pkg.replaceAll("/", "-") + "-";
1624
1675
  return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
@@ -1659,7 +1710,7 @@ var OpenClawApplier = class {
1659
1710
  }
1660
1711
  }
1661
1712
  removeClawHubSkill(slug) {
1662
- const workspaceSkillsDir = join(this.workspace, "skills", slug);
1713
+ const workspaceSkillsDir = join(this.agentWorkspace, "skills", slug);
1663
1714
  if (existsSync(workspaceSkillsDir)) {
1664
1715
  rmSync(workspaceSkillsDir, {
1665
1716
  recursive: true,
@@ -1689,21 +1740,36 @@ var OpenClawApplier = class {
1689
1740
  integrations[integrationId] = config;
1690
1741
  tracking._integrations = integrations;
1691
1742
  this.writeTracking(tracking);
1692
- const batch = flattenConfig(config).map(([path, value]) => ({
1693
- path,
1694
- value
1695
- }));
1696
- 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 {
1697
1763
  await execFileAsync("openclaw", [
1698
1764
  "config",
1699
1765
  "set",
1700
1766
  "--batch-json",
1701
- JSON.stringify(batch)
1767
+ JSON.stringify(leaves)
1702
1768
  ], { timeout: 1e4 });
1703
1769
  } catch (err) {
1704
1770
  log.error({
1705
1771
  err: err instanceof Error ? err.message : String(err),
1706
- batch
1772
+ batch: leaves
1707
1773
  }, "Failed to set config via openclaw config set --batch-json");
1708
1774
  throw err;
1709
1775
  }
@@ -1719,8 +1785,8 @@ var OpenClawApplier = class {
1719
1785
  const integrations = tracking._integrations ?? {};
1720
1786
  if (!(integrationId in integrations)) return;
1721
1787
  const integrationConfig = integrations[integrationId];
1722
- const pairs = flattenConfig(integrationConfig);
1723
- for (const [path] of pairs) try {
1788
+ const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
1789
+ for (const { path } of leaves) try {
1724
1790
  await execFileAsync("openclaw", [
1725
1791
  "config",
1726
1792
  "unset",
@@ -1732,6 +1798,29 @@ var OpenClawApplier = class {
1732
1798
  path
1733
1799
  }, "Failed to unset config via openclaw config unset");
1734
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
+ }
1735
1824
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
1736
1825
  this.writeTracking(tracking);
1737
1826
  }
@@ -1812,7 +1901,7 @@ var OpenClawApplier = class {
1812
1901
  this.writeTracking(tracking);
1813
1902
  }
1814
1903
  isAvailable() {
1815
- return Promise.resolve(existsSync(this.workspace));
1904
+ return Promise.resolve(existsSync(this.home));
1816
1905
  }
1817
1906
  readTracking() {
1818
1907
  if (!existsSync(this.trackingPath)) return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.29",
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",