@alfe.ai/integrations 0.2.5 → 0.2.6

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/dist/index.d.ts +127 -13
  2. package/dist/index.js +128 -63
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -746,6 +746,79 @@ declare function runHook(integrationPath: string, hookScript: string, env?: Reco
746
746
  */
747
747
  declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions): Promise<HookResult>;
748
748
  //#endregion
749
+ //#region src/openclaw-cli-lock.d.ts
750
+ /**
751
+ * OpenClawCliLock — one process-wide async mutex that every openclaw
752
+ * config/state-mutating CLI invocation acquires.
753
+ *
754
+ * ## Why this exists
755
+ *
756
+ * OpenClaw's config store uses OPTIMISTIC CONCURRENCY — a `config set` /
757
+ * `plugins install` reads the current config version, applies its change, and
758
+ * writes back only if the version is unchanged. Two openclaw CLI processes that
759
+ * mutate the store concurrently therefore lose the race: the second write fails
760
+ * with `ConfigMutationConflictError: config changed since last load` (exit 1).
761
+ * And when the long-running runtime (`openclaw gateway run`) is ALSO writing the
762
+ * same `~/.openclaw/state/openclaw.sqlite`, two independent SQLite writers with
763
+ * no shared locking corrupt the DB ("database disk image is malformed").
764
+ *
765
+ * During a DESIRED_STATE reconcile the daemon fires a SWARM of openclaw CLI
766
+ * processes — integration `config set` / `plugins install/uninstall`, the
767
+ * mcp-cleanup `config unset`, the `alfe.config_set` raw write — all mutating the
768
+ * one config store at the same time. This lock serializes ALL of them into a
769
+ * single strict queue so no two openclaw mutations ever overlap.
770
+ *
771
+ * This is the second of two layers. The first is the gateway's `RuntimeGate`,
772
+ * which suspends the runtime CHILD around mutating reconcile branches (so the
773
+ * runtime isn't writing while a CLI subprocess is). This lock covers the daemon's
774
+ * OWN subprocesses racing EACH OTHER — including cross-subsystem races the
775
+ * RuntimeGate doesn't see (mcp-cleanup vs a reconcile plugins-install, a raw
776
+ * `alfe.config_set` vs an in-flight integration apply). Both layers are needed.
777
+ *
778
+ * ## Read-modify-write atomicity
779
+ *
780
+ * A `config get` whose value is then written back (e.g. read `plugins.allow`,
781
+ * union in a plugin, write it back) MUST hold the lock across the WHOLE get→set,
782
+ * not lock the get and set independently — otherwise another openclaw process can
783
+ * mutate the same key between the read and the write and the write clobbers it.
784
+ * Callers pass the entire read-modify-write as one `lock.run(async () => { … })`.
785
+ *
786
+ * ## Re-entrancy
787
+ *
788
+ * This is a plain promise-chain mutex — it is NOT re-entrant. A `run()` callback
789
+ * MUST NOT call `run()` again (it would deadlock: the inner call chains behind
790
+ * the outer, which can't complete until the inner does). Consumers keep their
791
+ * locked-section internals as UNLOCKED helpers and acquire the lock exactly once
792
+ * at the public entry point.
793
+ */
794
+ interface OpenClawCliLock {
795
+ /**
796
+ * Run `fn` with exclusive access to the openclaw config/state store. Calls are
797
+ * serialized in invocation order; each waits for the previous to settle. The
798
+ * caller's result (or rejection) is returned faithfully — a rejection does NOT
799
+ * poison the queue for the next caller.
800
+ */
801
+ run<T>(fn: () => Promise<T>): Promise<T>;
802
+ }
803
+ /**
804
+ * Promise-chain mutex. The tail is kept as a swallowed continuation so a rejected
805
+ * critical section never fails the NEXT unrelated caller, while the current caller
806
+ * still sees the real result/rejection.
807
+ */
808
+ declare class SerialOpenClawCliLock implements OpenClawCliLock {
809
+ private tail;
810
+ run<T>(fn: () => Promise<T>): Promise<T>;
811
+ }
812
+ /**
813
+ * A lock that provides NO serialization — every `run` executes immediately. For
814
+ * runtimes/contexts with no concurrent-openclaw-writer hazard, and for tests that
815
+ * don't need serialization. Keeps consumers from having to special-case an absent
816
+ * lock.
817
+ */
818
+ declare class NoopOpenClawCliLock implements OpenClawCliLock {
819
+ run<T>(fn: () => Promise<T>): Promise<T>;
820
+ }
821
+ //#endregion
749
822
  //#region src/appliers/openclaw-applier.d.ts
750
823
  interface OpenClawApplierOptions {
751
824
  /**
@@ -771,6 +844,14 @@ interface OpenClawApplierOptions {
771
844
  configSetRetries?: number;
772
845
  /** Backoff between `openclaw config set` retries, ms (default 750; set 0 in tests) */
773
846
  configSetRetryDelayMs?: number;
847
+ /**
848
+ * Shared process-wide lock serializing ALL openclaw config/state-mutating CLI
849
+ * calls (this applier's + the gateway's mcp-cleanup / alfe.config_set). Inject
850
+ * the daemon's single instance so every openclaw mutation across BOTH packages
851
+ * queues behind one mutex. Defaults to a private `SerialOpenClawCliLock` so a
852
+ * standalone applier (and existing tests) still serialize their own writes.
853
+ */
854
+ cliLock?: OpenClawCliLock;
774
855
  }
775
856
  declare class OpenClawApplier implements RuntimeApplier {
776
857
  readonly runtime = "openclaw";
@@ -780,22 +861,38 @@ declare class OpenClawApplier implements RuntimeApplier {
780
861
  private trackingPath;
781
862
  private configSetRetries;
782
863
  private configSetRetryDelayMs;
783
- /** Serializes all `openclaw config set` writes so they never interleave with each other or the hot-reload they trigger. */
784
- private configSetQueue;
864
+ /**
865
+ * Shared process-wide mutex serializing ALL openclaw config/state-mutating CLI
866
+ * calls. Replaces the old per-applier `configSetQueue` promise-chain — a single
867
+ * lock so config-set can't interleave with plugins-install (or the gateway's
868
+ * mcp-cleanup / alfe.config_set), which is what corrupts the config store under
869
+ * OpenClaw's optimistic concurrency. NOT re-entrant: every locked public method
870
+ * acquires it ONCE at the top and calls only `*Unlocked` helpers inside.
871
+ */
872
+ private cliLock;
785
873
  constructor(options: OpenClawApplierOptions);
786
- /** Convenience: `openclaw config set <args>`, serialized + retried. */
787
- private runConfigSet;
788
874
  /**
789
- * Run `openclaw config <args>` (set/unset), serialized against every other
790
- * config write and retried with backoff. See CONFIG_SET_RETRIES for why:
791
- * each write triggers a runtime hot-reload that rewrites openclaw.json, and a
792
- * follow-up command that races the reload fails with a bare "Command failed".
875
+ * Convenience: `openclaw config set <args>`, UNLOCKED + retried.
876
+ *
877
+ * Assumes the shared CLI lock is already held by the calling public method.
878
+ * Never call this from outside a `cliLock.run(...)` section.
879
+ */
880
+ private runConfigSetUnlocked;
881
+ /**
882
+ * Run `openclaw config <args>` (set/unset), UNLOCKED, retried with backoff. See
883
+ * CONFIG_SET_RETRIES for why the retry loop exists: each write triggers a
884
+ * runtime hot-reload that rewrites openclaw.json, and a follow-up command that
885
+ * races the reload fails with a bare "Command failed".
886
+ *
887
+ * Serialization across every openclaw mutation is provided by `cliLock`, held
888
+ * by the public entry point — this helper assumes it and MUST NOT acquire the
889
+ * lock itself (that would deadlock the non-re-entrant promise-chain mutex).
793
890
  *
794
891
  * Throws an Error whose (scrubbed) message includes stderr after retries are
795
892
  * exhausted, so the real cause propagates to the integration errorMessage
796
893
  * without leaking the value payload.
797
894
  */
798
- private runConfigCommand;
895
+ private runConfigCommandUnlocked;
799
896
  /**
800
897
  * Read back a single config path and compare it to the value we tried to
801
898
  * write. Used by `applyConfig`'s verify-after-write: a `config set` can exit
@@ -819,21 +916,30 @@ declare class OpenClawApplier implements RuntimeApplier {
819
916
  applyPlugin(spec: string, _installPath?: string, opts?: {
820
917
  force?: boolean;
821
918
  }): Promise<void>;
919
+ private applyPluginLocked;
822
920
  /**
823
- * Ensure one or more plugins are in plugins.allow in openclaw.json.
921
+ * Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
824
922
  * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
825
923
  *
924
+ * This is a read-modify-write (`config get plugins.allow` → union → `config
925
+ * set`). The caller MUST hold the shared CLI lock across the WHOLE call so no
926
+ * other openclaw process mutates `plugins.allow` between the read and the write
927
+ * (that would clobber the concurrent change). All callers wrap it in
928
+ * `cliLock.run(...)` — never invoke it outside a locked section.
929
+ *
826
930
  * Prefer passing the FULL set of plugins for an integration in a single call
827
931
  * (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
828
932
  * one write for N plugins is one reload instead of N.
829
933
  */
830
- private ensurePluginsAllow;
934
+ private ensurePluginsAllowUnlocked;
831
935
  /**
832
936
  * Pre-trust every plugin an integration ships in a SINGLE plugins.allow
833
937
  * write, before any are installed. Called once by the manager ahead of the
834
938
  * per-plugin install loop so activation triggers one hot-reload for the
835
939
  * allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
836
- * `ensurePluginsAllow` then finds nothing missing and is a no-op.
940
+ * `ensurePluginsAllowUnlocked` then finds nothing missing and is a no-op.
941
+ *
942
+ * Acquires the shared CLI lock for the whole read-modify-write.
837
943
  */
838
944
  ensurePluginsAllowed(specs: string[]): Promise<void>;
839
945
  /**
@@ -856,6 +962,12 @@ declare class OpenClawApplier implements RuntimeApplier {
856
962
  */
857
963
  private cleanupUntrackedExtensionInstall;
858
964
  removePlugin(spec: string): Promise<void>;
965
+ /**
966
+ * `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
967
+ * by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
968
+ * path). Never call outside a locked section.
969
+ */
970
+ private removePluginUnlocked;
859
971
  applySkill(name: string, srcPath: string): Promise<void>;
860
972
  applyClawHubSkill(slug: string): Promise<void>;
861
973
  removeClawHubSkill(slug: string): Promise<void>;
@@ -867,6 +979,7 @@ declare class OpenClawApplier implements RuntimeApplier {
867
979
  * (config.json) so it can be cleanly removed later.
868
980
  */
869
981
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
982
+ private applyConfigLocked;
870
983
  /**
871
984
  * Remove config previously applied by an integration.
872
985
  *
@@ -874,6 +987,7 @@ declare class OpenClawApplier implements RuntimeApplier {
874
987
  * then removes them via `openclaw config unset`.
875
988
  */
876
989
  removeConfig(integrationId: string): Promise<void>;
990
+ private removeConfigLocked;
877
991
  /**
878
992
  * Raw single-key config write — `openclaw config set <key> <value>`.
879
993
  *
@@ -1261,4 +1375,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
1261
1375
  resetReinstallAttempts(integrationId: string): void;
1262
1376
  }
1263
1377
  //#endregion
1264
- export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, type LoadOptions, LockManager, McpApplier, type McpApplierOptions, type McpStoreReader, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
1378
+ export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, type LoadOptions, LockManager, McpApplier, type McpApplierOptions, type McpStoreReader, NoopOpenClawCliLock, OpenClawApplier, type OpenClawApplierOptions, type OpenClawCliLock, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
package/dist/index.js CHANGED
@@ -1617,6 +1617,32 @@ var IntegrationManager = class {
1617
1617
  }
1618
1618
  };
1619
1619
  //#endregion
1620
+ //#region src/openclaw-cli-lock.ts
1621
+ /**
1622
+ * Promise-chain mutex. The tail is kept as a swallowed continuation so a rejected
1623
+ * critical section never fails the NEXT unrelated caller, while the current caller
1624
+ * still sees the real result/rejection.
1625
+ */
1626
+ var SerialOpenClawCliLock = class {
1627
+ tail = Promise.resolve();
1628
+ run(fn) {
1629
+ const result = this.tail.then(fn, fn);
1630
+ this.tail = result.then(() => void 0, () => void 0);
1631
+ return result;
1632
+ }
1633
+ };
1634
+ /**
1635
+ * A lock that provides NO serialization — every `run` executes immediately. For
1636
+ * runtimes/contexts with no concurrent-openclaw-writer hazard, and for tests that
1637
+ * don't need serialization. Keeps consumers from having to special-case an absent
1638
+ * lock.
1639
+ */
1640
+ var NoopOpenClawCliLock = class {
1641
+ run(fn) {
1642
+ return fn();
1643
+ }
1644
+ };
1645
+ //#endregion
1620
1646
  //#region src/appliers/config-flatten.ts
1621
1647
  function flattenConfig(obj, prefix = "") {
1622
1648
  const entries = [];
@@ -1791,8 +1817,15 @@ var OpenClawApplier = class {
1791
1817
  trackingPath;
1792
1818
  configSetRetries;
1793
1819
  configSetRetryDelayMs;
1794
- /** Serializes all `openclaw config set` writes so they never interleave with each other or the hot-reload they trigger. */
1795
- configSetQueue = Promise.resolve();
1820
+ /**
1821
+ * Shared process-wide mutex serializing ALL openclaw config/state-mutating CLI
1822
+ * calls. Replaces the old per-applier `configSetQueue` promise-chain — a single
1823
+ * lock so config-set can't interleave with plugins-install (or the gateway's
1824
+ * mcp-cleanup / alfe.config_set), which is what corrupts the config store under
1825
+ * OpenClaw's optimistic concurrency. NOT re-entrant: every locked public method
1826
+ * acquires it ONCE at the top and calls only `*Unlocked` helpers inside.
1827
+ */
1828
+ cliLock;
1796
1829
  constructor(options) {
1797
1830
  const home = options.home ?? options.workspace;
1798
1831
  if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
@@ -1802,37 +1835,42 @@ var OpenClawApplier = class {
1802
1835
  this.trackingPath = options.configPath ?? join(this.home, "config.json");
1803
1836
  this.configSetRetries = options.configSetRetries ?? CONFIG_SET_RETRIES$1;
1804
1837
  this.configSetRetryDelayMs = options.configSetRetryDelayMs ?? CONFIG_SET_RETRY_DELAY_MS$1;
1838
+ this.cliLock = options.cliLock ?? new SerialOpenClawCliLock();
1805
1839
  }
1806
- /** Convenience: `openclaw config set <args>`, serialized + retried. */
1807
- runConfigSet(setArgs, opts = {}) {
1808
- return this.runConfigCommand(["set", ...setArgs], opts);
1840
+ /**
1841
+ * Convenience: `openclaw config set <args>`, UNLOCKED + retried.
1842
+ *
1843
+ * Assumes the shared CLI lock is already held by the calling public method.
1844
+ * Never call this from outside a `cliLock.run(...)` section.
1845
+ */
1846
+ runConfigSetUnlocked(setArgs, opts = {}) {
1847
+ return this.runConfigCommandUnlocked(["set", ...setArgs], opts);
1809
1848
  }
1810
1849
  /**
1811
- * Run `openclaw config <args>` (set/unset), serialized against every other
1812
- * config write and retried with backoff. See CONFIG_SET_RETRIES for why:
1813
- * each write triggers a runtime hot-reload that rewrites openclaw.json, and a
1814
- * follow-up command that races the reload fails with a bare "Command failed".
1850
+ * Run `openclaw config <args>` (set/unset), UNLOCKED, retried with backoff. See
1851
+ * CONFIG_SET_RETRIES for why the retry loop exists: each write triggers a
1852
+ * runtime hot-reload that rewrites openclaw.json, and a follow-up command that
1853
+ * races the reload fails with a bare "Command failed".
1854
+ *
1855
+ * Serialization across every openclaw mutation is provided by `cliLock`, held
1856
+ * by the public entry point — this helper assumes it and MUST NOT acquire the
1857
+ * lock itself (that would deadlock the non-re-entrant promise-chain mutex).
1815
1858
  *
1816
1859
  * Throws an Error whose (scrubbed) message includes stderr after retries are
1817
1860
  * exhausted, so the real cause propagates to the integration errorMessage
1818
1861
  * without leaking the value payload.
1819
1862
  */
1820
- runConfigCommand(args, opts = {}) {
1863
+ async runConfigCommandUnlocked(args, opts = {}) {
1821
1864
  const timeout = opts.timeout ?? 1e4;
1822
- const run = async () => {
1823
- let lastErr;
1824
- for (let attempt = 0; attempt <= this.configSetRetries; attempt++) try {
1825
- await execFileAsync$1("openclaw", ["config", ...args], { timeout });
1826
- return;
1827
- } catch (err) {
1828
- lastErr = err;
1829
- if (attempt < this.configSetRetries && this.configSetRetryDelayMs > 0) await delay$1(this.configSetRetryDelayMs * 2 ** attempt);
1830
- }
1831
- throw new Error(configSetErrorMessage(lastErr, args));
1832
- };
1833
- const result = this.configSetQueue.then(run, run);
1834
- this.configSetQueue = result.catch(() => void 0);
1835
- return result;
1865
+ let lastErr;
1866
+ for (let attempt = 0; attempt <= this.configSetRetries; attempt++) try {
1867
+ await execFileAsync$1("openclaw", ["config", ...args], { timeout });
1868
+ return;
1869
+ } catch (err) {
1870
+ lastErr = err;
1871
+ if (attempt < this.configSetRetries && this.configSetRetryDelayMs > 0) await delay$1(this.configSetRetryDelayMs * 2 ** attempt);
1872
+ }
1873
+ throw new Error(configSetErrorMessage(lastErr, args));
1836
1874
  }
1837
1875
  /**
1838
1876
  * Read back a single config path and compare it to the value we tried to
@@ -1873,9 +1911,12 @@ var OpenClawApplier = class {
1873
1911
  }
1874
1912
  return false;
1875
1913
  }
1876
- async applyPlugin(spec, _installPath, opts) {
1914
+ applyPlugin(spec, _installPath, opts) {
1915
+ return this.cliLock.run(() => this.applyPluginLocked(spec, opts));
1916
+ }
1917
+ async applyPluginLocked(spec, opts) {
1877
1918
  const pkg = stripPluginVersion(spec);
1878
- await this.ensurePluginsAllow(pkg);
1919
+ await this.ensurePluginsAllowUnlocked(pkg);
1879
1920
  this.cleanupUntrackedExtensionInstall(pkg);
1880
1921
  if (opts?.force && this.isPluginInstalled(pkg)) {
1881
1922
  log$3.info({
@@ -1883,7 +1924,7 @@ var OpenClawApplier = class {
1883
1924
  spec
1884
1925
  }, "Force mode — uninstalling plugin before reinstall");
1885
1926
  try {
1886
- await this.removePlugin(pkg);
1927
+ await this.removePluginUnlocked(pkg);
1887
1928
  } catch (err) {
1888
1929
  log$3.warn({
1889
1930
  pkg,
@@ -1928,14 +1969,20 @@ var OpenClawApplier = class {
1928
1969
  }
1929
1970
  }
1930
1971
  /**
1931
- * Ensure one or more plugins are in plugins.allow in openclaw.json.
1972
+ * Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
1932
1973
  * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
1933
1974
  *
1975
+ * This is a read-modify-write (`config get plugins.allow` → union → `config
1976
+ * set`). The caller MUST hold the shared CLI lock across the WHOLE call so no
1977
+ * other openclaw process mutates `plugins.allow` between the read and the write
1978
+ * (that would clobber the concurrent change). All callers wrap it in
1979
+ * `cliLock.run(...)` — never invoke it outside a locked section.
1980
+ *
1934
1981
  * Prefer passing the FULL set of plugins for an integration in a single call
1935
1982
  * (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
1936
1983
  * one write for N plugins is one reload instead of N.
1937
1984
  */
1938
- async ensurePluginsAllow(pkgs) {
1985
+ async ensurePluginsAllowUnlocked(pkgs) {
1939
1986
  const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
1940
1987
  let currentAllow = [];
1941
1988
  try {
@@ -1951,7 +1998,7 @@ var OpenClawApplier = class {
1951
1998
  if (missing.length === 0) return;
1952
1999
  const updated = [...currentAllow, ...missing];
1953
2000
  try {
1954
- await this.runConfigSet([
2001
+ await this.runConfigSetUnlocked([
1955
2002
  "plugins.allow",
1956
2003
  JSON.stringify(updated),
1957
2004
  "--merge"
@@ -1968,11 +2015,13 @@ var OpenClawApplier = class {
1968
2015
  * write, before any are installed. Called once by the manager ahead of the
1969
2016
  * per-plugin install loop so activation triggers one hot-reload for the
1970
2017
  * allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
1971
- * `ensurePluginsAllow` then finds nothing missing and is a no-op.
2018
+ * `ensurePluginsAllowUnlocked` then finds nothing missing and is a no-op.
2019
+ *
2020
+ * Acquires the shared CLI lock for the whole read-modify-write.
1972
2021
  */
1973
- async ensurePluginsAllowed(specs) {
1974
- if (specs.length === 0) return;
1975
- await this.ensurePluginsAllow(specs.map(stripPluginVersion));
2022
+ ensurePluginsAllowed(specs) {
2023
+ if (specs.length === 0) return Promise.resolve();
2024
+ return this.cliLock.run(() => this.ensurePluginsAllowUnlocked(specs.map(stripPluginVersion)));
1976
2025
  }
1977
2026
  /**
1978
2027
  * Check if a plugin is already installed.
@@ -2034,7 +2083,15 @@ var OpenClawApplier = class {
2034
2083
  }
2035
2084
  }
2036
2085
  }
2037
- async removePlugin(spec) {
2086
+ removePlugin(spec) {
2087
+ return this.cliLock.run(() => this.removePluginUnlocked(spec));
2088
+ }
2089
+ /**
2090
+ * `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
2091
+ * by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
2092
+ * path). Never call outside a locked section.
2093
+ */
2094
+ async removePluginUnlocked(spec) {
2038
2095
  await execFileAsync$1("openclaw", [
2039
2096
  "plugins",
2040
2097
  "uninstall",
@@ -2048,26 +2105,28 @@ var OpenClawApplier = class {
2048
2105
  cpSync(srcPath, join(this.skillsDir, name), { recursive: true });
2049
2106
  return Promise.resolve();
2050
2107
  }
2051
- async applyClawHubSkill(slug) {
2052
- log$3.info({ slug }, "Installing skill from ClawHub");
2053
- try {
2054
- await execFileAsync$1("openclaw", [
2055
- "skills",
2056
- "install",
2057
- slug
2058
- ], { timeout: 6e4 });
2059
- log$3.info({ slug }, "ClawHub skill installed");
2060
- } catch (err) {
2061
- const msg = err instanceof Error ? err.message : String(err);
2062
- if (msg.includes("already exists") || msg.includes("Skill already exists")) {
2063
- log$3.info({ slug }, "ClawHub skill already installed (skipped)");
2064
- return;
2108
+ applyClawHubSkill(slug) {
2109
+ return this.cliLock.run(async () => {
2110
+ log$3.info({ slug }, "Installing skill from ClawHub");
2111
+ try {
2112
+ await execFileAsync$1("openclaw", [
2113
+ "skills",
2114
+ "install",
2115
+ slug
2116
+ ], { timeout: 6e4 });
2117
+ log$3.info({ slug }, "ClawHub skill installed");
2118
+ } catch (err) {
2119
+ const msg = err instanceof Error ? err.message : String(err);
2120
+ if (msg.includes("already exists") || msg.includes("Skill already exists")) {
2121
+ log$3.info({ slug }, "ClawHub skill already installed (skipped)");
2122
+ return;
2123
+ }
2124
+ log$3.error({
2125
+ slug,
2126
+ err: msg
2127
+ }, "ClawHub skill install failed");
2065
2128
  }
2066
- log$3.error({
2067
- slug,
2068
- err: msg
2069
- }, "ClawHub skill install failed");
2070
- }
2129
+ });
2071
2130
  }
2072
2131
  removeClawHubSkill(slug) {
2073
2132
  const workspaceSkillsDir = join(this.agentWorkspace, "skills", slug);
@@ -2094,7 +2153,10 @@ var OpenClawApplier = class {
2094
2153
  * Each integration's config contribution is tracked in the tracking file
2095
2154
  * (config.json) so it can be cleanly removed later.
2096
2155
  */
2097
- async applyConfig(integrationId, config) {
2156
+ applyConfig(integrationId, config) {
2157
+ return this.cliLock.run(() => this.applyConfigLocked(integrationId, config));
2158
+ }
2159
+ async applyConfigLocked(integrationId, config) {
2098
2160
  const tracking = this.readTracking();
2099
2161
  const integrations = tracking._integrations ?? {};
2100
2162
  integrations[integrationId] = config;
@@ -2105,7 +2167,7 @@ var OpenClawApplier = class {
2105
2167
  const merged = { ...await readParentObject(parentPath) };
2106
2168
  for (const [k, v] of dottedKvs) merged[k] = v;
2107
2169
  try {
2108
- await this.runConfigSet([
2170
+ await this.runConfigSetUnlocked([
2109
2171
  parentPath,
2110
2172
  JSON.stringify(merged),
2111
2173
  "--merge"
@@ -2126,7 +2188,7 @@ var OpenClawApplier = class {
2126
2188
  }
2127
2189
  }
2128
2190
  if (leaves.length > 0) try {
2129
- await this.runConfigSet([
2191
+ await this.runConfigSetUnlocked([
2130
2192
  "--batch-json",
2131
2193
  JSON.stringify(leaves),
2132
2194
  "--replace"
@@ -2150,14 +2212,17 @@ var OpenClawApplier = class {
2150
2212
  * Reads the tracking file to find which config keys this integration set,
2151
2213
  * then removes them via `openclaw config unset`.
2152
2214
  */
2153
- async removeConfig(integrationId) {
2215
+ removeConfig(integrationId) {
2216
+ return this.cliLock.run(() => this.removeConfigLocked(integrationId));
2217
+ }
2218
+ async removeConfigLocked(integrationId) {
2154
2219
  const tracking = this.readTracking();
2155
2220
  const integrations = tracking._integrations ?? {};
2156
2221
  if (!(integrationId in integrations)) return;
2157
2222
  const integrationConfig = integrations[integrationId];
2158
2223
  const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
2159
2224
  for (const { path } of leaves) try {
2160
- await this.runConfigCommand(["unset", path]);
2225
+ await this.runConfigCommandUnlocked(["unset", path]);
2161
2226
  } catch (err) {
2162
2227
  log$3.warn({
2163
2228
  err: err instanceof Error ? err.message : String(err),
@@ -2169,8 +2234,8 @@ var OpenClawApplier = class {
2169
2234
  const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
2170
2235
  if (Object.keys(existing).length === 0) continue;
2171
2236
  try {
2172
- if (Object.keys(remaining).length === 0) await this.runConfigCommand(["unset", parentPath]);
2173
- else await this.runConfigSet([
2237
+ if (Object.keys(remaining).length === 0) await this.runConfigCommandUnlocked(["unset", parentPath]);
2238
+ else await this.runConfigSetUnlocked([
2174
2239
  parentPath,
2175
2240
  JSON.stringify(remaining),
2176
2241
  "--replace"
@@ -2196,7 +2261,7 @@ var OpenClawApplier = class {
2196
2261
  * config writes trigger.
2197
2262
  */
2198
2263
  setConfigRaw(key, value) {
2199
- return this.runConfigSet([key, value]);
2264
+ return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
2200
2265
  }
2201
2266
  isAvailable() {
2202
2267
  return Promise.resolve(existsSync(this.home));
@@ -2948,4 +3013,4 @@ var IntegrationManagerAdapter = class {
2948
3013
  }
2949
3014
  };
2950
3015
  //#endregion
2951
- export { DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
3016
+ export { DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, NoopOpenClawCliLock, OpenClawApplier, Registry, RegistryResolveError, Resolver, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",