@alfe.ai/integrations 0.2.5 → 0.2.7
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 +137 -13
- package/dist/index.js +264 -69
- 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
|
-
/**
|
|
784
|
-
|
|
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
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
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
|
|
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
|
|
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
|
-
* `
|
|
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
|
/**
|
|
@@ -845,6 +951,16 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
845
951
|
* plugins under 2026.5+, which made force-mode skip its uninstall step.
|
|
846
952
|
*/
|
|
847
953
|
private isPluginInstalled;
|
|
954
|
+
/**
|
|
955
|
+
* Best-effort read of the installed version of a plugin, or `undefined` when
|
|
956
|
+
* it can't be determined. Reads the version from the installed package's
|
|
957
|
+
* package.json under the npm registry path OpenClaw 2026.5+ installs to
|
|
958
|
+
* (`~/.openclaw/npm/node_modules/<pkg>/package.json`) — the same root
|
|
959
|
+
* `isPluginInstalled` checks. 2026.4 extensions-dir installs have no
|
|
960
|
+
* stable package.json version here and return `undefined` (callers fail
|
|
961
|
+
* open to the safe reinstall path).
|
|
962
|
+
*/
|
|
963
|
+
private installedPluginVersion;
|
|
848
964
|
/**
|
|
849
965
|
* Remove an extensions/ install that has no matching record in
|
|
850
966
|
* plugins/installs.json. OpenClaw 2026.5+ tracks installs via the records
|
|
@@ -856,6 +972,12 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
856
972
|
*/
|
|
857
973
|
private cleanupUntrackedExtensionInstall;
|
|
858
974
|
removePlugin(spec: string): Promise<void>;
|
|
975
|
+
/**
|
|
976
|
+
* `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
|
|
977
|
+
* by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
|
|
978
|
+
* path). Never call outside a locked section.
|
|
979
|
+
*/
|
|
980
|
+
private removePluginUnlocked;
|
|
859
981
|
applySkill(name: string, srcPath: string): Promise<void>;
|
|
860
982
|
applyClawHubSkill(slug: string): Promise<void>;
|
|
861
983
|
removeClawHubSkill(slug: string): Promise<void>;
|
|
@@ -867,6 +989,7 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
867
989
|
* (config.json) so it can be cleanly removed later.
|
|
868
990
|
*/
|
|
869
991
|
applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
|
|
992
|
+
private applyConfigLocked;
|
|
870
993
|
/**
|
|
871
994
|
* Remove config previously applied by an integration.
|
|
872
995
|
*
|
|
@@ -874,6 +997,7 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
874
997
|
* then removes them via `openclaw config unset`.
|
|
875
998
|
*/
|
|
876
999
|
removeConfig(integrationId: string): Promise<void>;
|
|
1000
|
+
private removeConfigLocked;
|
|
877
1001
|
/**
|
|
878
1002
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
879
1003
|
*
|
|
@@ -1261,4 +1385,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
|
|
|
1261
1385
|
resetReinstallAttempts(integrationId: string): void;
|
|
1262
1386
|
}
|
|
1263
1387
|
//#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 };
|
|
1388
|
+
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
|
@@ -856,6 +856,22 @@ function stripPluginVersion(spec) {
|
|
|
856
856
|
if (at <= 0) return spec;
|
|
857
857
|
return spec.slice(0, at);
|
|
858
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Return the pinned version from a plugin spec, or `undefined` when the spec
|
|
861
|
+
* carries no version (bare package name). Mirror of `stripPluginVersion`: the
|
|
862
|
+
* version is everything after the LAST '@' when that '@' sits at index > 0
|
|
863
|
+
* (i.e. it's a real version delimiter, not the scope's leading '@').
|
|
864
|
+
*
|
|
865
|
+
* Used by the force-mode reinstall path to compare the pinned version against
|
|
866
|
+
* the installed one so an already-current plugin can skip a ~45s
|
|
867
|
+
* uninstall+reinstall cycle.
|
|
868
|
+
*/
|
|
869
|
+
function pluginSpecVersion(spec) {
|
|
870
|
+
const at = spec.lastIndexOf("@");
|
|
871
|
+
if (at <= 0) return void 0;
|
|
872
|
+
const version = spec.slice(at + 1);
|
|
873
|
+
return version.length > 0 ? version : void 0;
|
|
874
|
+
}
|
|
859
875
|
//#endregion
|
|
860
876
|
//#region src/integration-manager.ts
|
|
861
877
|
/**
|
|
@@ -1617,6 +1633,32 @@ var IntegrationManager = class {
|
|
|
1617
1633
|
}
|
|
1618
1634
|
};
|
|
1619
1635
|
//#endregion
|
|
1636
|
+
//#region src/openclaw-cli-lock.ts
|
|
1637
|
+
/**
|
|
1638
|
+
* Promise-chain mutex. The tail is kept as a swallowed continuation so a rejected
|
|
1639
|
+
* critical section never fails the NEXT unrelated caller, while the current caller
|
|
1640
|
+
* still sees the real result/rejection.
|
|
1641
|
+
*/
|
|
1642
|
+
var SerialOpenClawCliLock = class {
|
|
1643
|
+
tail = Promise.resolve();
|
|
1644
|
+
run(fn) {
|
|
1645
|
+
const result = this.tail.then(fn, fn);
|
|
1646
|
+
this.tail = result.then(() => void 0, () => void 0);
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
/**
|
|
1651
|
+
* A lock that provides NO serialization — every `run` executes immediately. For
|
|
1652
|
+
* runtimes/contexts with no concurrent-openclaw-writer hazard, and for tests that
|
|
1653
|
+
* don't need serialization. Keeps consumers from having to special-case an absent
|
|
1654
|
+
* lock.
|
|
1655
|
+
*/
|
|
1656
|
+
var NoopOpenClawCliLock = class {
|
|
1657
|
+
run(fn) {
|
|
1658
|
+
return fn();
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
//#endregion
|
|
1620
1662
|
//#region src/appliers/config-flatten.ts
|
|
1621
1663
|
function flattenConfig(obj, prefix = "") {
|
|
1622
1664
|
const entries = [];
|
|
@@ -1703,6 +1745,15 @@ const BUNDLED_BASELINE_ALLOW = ["browser"];
|
|
|
1703
1745
|
const CONFIG_SET_RETRIES$1 = 3;
|
|
1704
1746
|
const CONFIG_SET_RETRY_DELAY_MS$1 = 750;
|
|
1705
1747
|
/**
|
|
1748
|
+
* Max `openclaw config get` spawns in flight at once during the batch
|
|
1749
|
+
* verify-after-write. A naive `Promise.all(leaves.map(...))` fires one CLI
|
|
1750
|
+
* process per leaf simultaneously — a 36-leaf model-provider batch on a 2-vCPU
|
|
1751
|
+
* box means 36 concurrent `config get`s, each needing ~1.9s of CPU, so every
|
|
1752
|
+
* process blows its own 10s timeout and the verify reports false even when the
|
|
1753
|
+
* write actually landed. A small pool keeps the read-backs honest under load.
|
|
1754
|
+
*/
|
|
1755
|
+
const VERIFY_GET_CONCURRENCY = 4;
|
|
1756
|
+
/**
|
|
1706
1757
|
* Sentinel OpenClaw returns from `config get` in place of a sensitive value —
|
|
1707
1758
|
* the key IS set, OpenClaw is just hiding it. Verify-after-write must treat a
|
|
1708
1759
|
* read-back of this as "present/matches" rather than a mismatch (see
|
|
@@ -1713,6 +1764,29 @@ const delay$1 = (ms) => new Promise((resolve) => {
|
|
|
1713
1764
|
setTimeout(resolve, ms);
|
|
1714
1765
|
});
|
|
1715
1766
|
/**
|
|
1767
|
+
* Run `fn` over `items` with at most `limit` invocations in flight at once,
|
|
1768
|
+
* preserving result order. A tiny local pool (no new dependency) so the batch
|
|
1769
|
+
* verify-after-write doesn't stampede one `openclaw config get` per leaf — see
|
|
1770
|
+
* VERIFY_GET_CONCURRENCY.
|
|
1771
|
+
*
|
|
1772
|
+
* NOTE: relies on `fn` never rejecting (configValueMatches catches internally).
|
|
1773
|
+
* A throwing `fn` would reject the pool while sibling workers keep draining
|
|
1774
|
+
* items in the background — don't wire a throwing `fn` in here.
|
|
1775
|
+
*/
|
|
1776
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1777
|
+
const results = new Array(items.length);
|
|
1778
|
+
let cursor = 0;
|
|
1779
|
+
const worker = async () => {
|
|
1780
|
+
while (cursor < items.length) {
|
|
1781
|
+
const index = cursor++;
|
|
1782
|
+
results[index] = await fn(items[index]);
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
1786
|
+
await Promise.all(workers);
|
|
1787
|
+
return results;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1716
1790
|
* Asymmetric structural containment for verify-after-write: is every field we
|
|
1717
1791
|
* INTENDED present in the ACTUAL read-back with our value? `actual` may carry
|
|
1718
1792
|
* EXTRA keys that `intended` does not.
|
|
@@ -1755,19 +1829,70 @@ function deepSubset(intended, actual) {
|
|
|
1755
1829
|
function redactConfigSetTarget(args) {
|
|
1756
1830
|
return `openclaw config ${args.slice(0, 2).join(" ")}`.trim();
|
|
1757
1831
|
}
|
|
1832
|
+
/** Cap a captured stream so a runaway OpenClaw dump can't bloat errorMessage. */
|
|
1833
|
+
const ERROR_STREAM_MAX = 400;
|
|
1834
|
+
function truncateForError(s) {
|
|
1835
|
+
return s.length > ERROR_STREAM_MAX ? `${s.slice(0, ERROR_STREAM_MAX)}… (truncated)` : s;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Collect the value strings a `config set` argv carries so they can be scrubbed
|
|
1839
|
+
* out of OpenClaw's own output. OpenClaw validation errors can echo the
|
|
1840
|
+
* offending VALUE back in their diagnostic text, so stdout/stderr must be
|
|
1841
|
+
* treated as value-bearing, not just the argv. For `{path, value}` batch
|
|
1842
|
+
* entries only the `value` side is collected — redacting the path out of
|
|
1843
|
+
* "invalid path models.providers…" would gut the diagnostic.
|
|
1844
|
+
*/
|
|
1845
|
+
function collectArgvValues(args) {
|
|
1846
|
+
const out = [];
|
|
1847
|
+
const collect = (v) => {
|
|
1848
|
+
if (typeof v === "string") {
|
|
1849
|
+
if (v.length >= 4) out.push(v);
|
|
1850
|
+
} else if (Array.isArray(v)) v.forEach(collect);
|
|
1851
|
+
else if (v !== null && typeof v === "object") {
|
|
1852
|
+
const o = v;
|
|
1853
|
+
if (typeof o.path === "string" && "value" in o) collect(o.value);
|
|
1854
|
+
else Object.values(o).forEach(collect);
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
for (const token of args.slice(2)) {
|
|
1858
|
+
if (token.startsWith("--")) continue;
|
|
1859
|
+
try {
|
|
1860
|
+
collect(JSON.parse(token));
|
|
1861
|
+
} catch {
|
|
1862
|
+
collect(token);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return out.sort((a, b) => b.length - a.length);
|
|
1866
|
+
}
|
|
1867
|
+
/** Replace every occurrence of the argv-derived values in a captured stream. */
|
|
1868
|
+
function scrubStream(s, values) {
|
|
1869
|
+
let out = s;
|
|
1870
|
+
for (const v of values) if (out.includes(v)) out = out.split(v).join("[REDACTED]");
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1758
1873
|
/**
|
|
1759
1874
|
* Build a diagnosable BUT scrubbed message from an execFile rejection. Node's
|
|
1760
1875
|
* execFile error `message` is "Command failed: <full argv>" — which for
|
|
1761
1876
|
* `config set` embeds the value payload — so we never surface it. We keep the
|
|
1762
|
-
* redacted target, the exit code,
|
|
1877
|
+
* redacted target, the exit code, `stderr`, and `stdout`.
|
|
1878
|
+
*
|
|
1879
|
+
* OpenClaw prints its own error text to STDOUT (not stderr), so on a genuine
|
|
1880
|
+
* failure `stderr` is often empty and the real cause is in stdout — surfacing
|
|
1881
|
+
* only stderr left days of bare "(exit 1)" in the logs. We include a truncated
|
|
1882
|
+
* stdout too — but scrubbed first: OpenClaw's error text can echo the offending
|
|
1883
|
+
* value, so both streams are run through `scrubStream` with the argv values
|
|
1884
|
+
* before anything reaches the dashboard-visible errorMessage.
|
|
1763
1885
|
*/
|
|
1764
1886
|
function configSetErrorMessage(err, args) {
|
|
1765
1887
|
const target = redactConfigSetTarget(args);
|
|
1766
1888
|
if (err instanceof Error) {
|
|
1767
1889
|
const e = err;
|
|
1768
|
-
const
|
|
1890
|
+
const values = collectArgvValues(args);
|
|
1891
|
+
const stderr = typeof e.stderr === "string" ? scrubStream(e.stderr.trim(), values) : "";
|
|
1892
|
+
const stdout = typeof e.stdout === "string" ? scrubStream(e.stdout.trim(), values) : "";
|
|
1769
1893
|
const code = e.code !== void 0 ? ` (exit ${e.code})` : "";
|
|
1770
|
-
|
|
1894
|
+
const details = [stderr, stdout ? `stdout: ${truncateForError(stdout)}` : ""].filter(Boolean).join("; ");
|
|
1895
|
+
return details ? `${target} failed${code}: ${details}` : `${target} failed${code}`;
|
|
1771
1896
|
}
|
|
1772
1897
|
return `${target} failed: ${String(err)}`;
|
|
1773
1898
|
}
|
|
@@ -1791,8 +1916,15 @@ var OpenClawApplier = class {
|
|
|
1791
1916
|
trackingPath;
|
|
1792
1917
|
configSetRetries;
|
|
1793
1918
|
configSetRetryDelayMs;
|
|
1794
|
-
/**
|
|
1795
|
-
|
|
1919
|
+
/**
|
|
1920
|
+
* Shared process-wide mutex serializing ALL openclaw config/state-mutating CLI
|
|
1921
|
+
* calls. Replaces the old per-applier `configSetQueue` promise-chain — a single
|
|
1922
|
+
* lock so config-set can't interleave with plugins-install (or the gateway's
|
|
1923
|
+
* mcp-cleanup / alfe.config_set), which is what corrupts the config store under
|
|
1924
|
+
* OpenClaw's optimistic concurrency. NOT re-entrant: every locked public method
|
|
1925
|
+
* acquires it ONCE at the top and calls only `*Unlocked` helpers inside.
|
|
1926
|
+
*/
|
|
1927
|
+
cliLock;
|
|
1796
1928
|
constructor(options) {
|
|
1797
1929
|
const home = options.home ?? options.workspace;
|
|
1798
1930
|
if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
|
|
@@ -1802,37 +1934,42 @@ var OpenClawApplier = class {
|
|
|
1802
1934
|
this.trackingPath = options.configPath ?? join(this.home, "config.json");
|
|
1803
1935
|
this.configSetRetries = options.configSetRetries ?? CONFIG_SET_RETRIES$1;
|
|
1804
1936
|
this.configSetRetryDelayMs = options.configSetRetryDelayMs ?? CONFIG_SET_RETRY_DELAY_MS$1;
|
|
1937
|
+
this.cliLock = options.cliLock ?? new SerialOpenClawCliLock();
|
|
1805
1938
|
}
|
|
1806
|
-
/**
|
|
1807
|
-
|
|
1808
|
-
|
|
1939
|
+
/**
|
|
1940
|
+
* Convenience: `openclaw config set <args>`, UNLOCKED + retried.
|
|
1941
|
+
*
|
|
1942
|
+
* Assumes the shared CLI lock is already held by the calling public method.
|
|
1943
|
+
* Never call this from outside a `cliLock.run(...)` section.
|
|
1944
|
+
*/
|
|
1945
|
+
runConfigSetUnlocked(setArgs, opts = {}) {
|
|
1946
|
+
return this.runConfigCommandUnlocked(["set", ...setArgs], opts);
|
|
1809
1947
|
}
|
|
1810
1948
|
/**
|
|
1811
|
-
* Run `openclaw config <args>` (set/unset),
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1949
|
+
* Run `openclaw config <args>` (set/unset), UNLOCKED, retried with backoff. See
|
|
1950
|
+
* CONFIG_SET_RETRIES for why the retry loop exists: each write triggers a
|
|
1951
|
+
* runtime hot-reload that rewrites openclaw.json, and a follow-up command that
|
|
1952
|
+
* races the reload fails with a bare "Command failed".
|
|
1953
|
+
*
|
|
1954
|
+
* Serialization across every openclaw mutation is provided by `cliLock`, held
|
|
1955
|
+
* by the public entry point — this helper assumes it and MUST NOT acquire the
|
|
1956
|
+
* lock itself (that would deadlock the non-re-entrant promise-chain mutex).
|
|
1815
1957
|
*
|
|
1816
1958
|
* Throws an Error whose (scrubbed) message includes stderr after retries are
|
|
1817
1959
|
* exhausted, so the real cause propagates to the integration errorMessage
|
|
1818
1960
|
* without leaking the value payload.
|
|
1819
1961
|
*/
|
|
1820
|
-
|
|
1962
|
+
async runConfigCommandUnlocked(args, opts = {}) {
|
|
1821
1963
|
const timeout = opts.timeout ?? 1e4;
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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;
|
|
1964
|
+
let lastErr;
|
|
1965
|
+
for (let attempt = 0; attempt <= this.configSetRetries; attempt++) try {
|
|
1966
|
+
await execFileAsync$1("openclaw", ["config", ...args], { timeout });
|
|
1967
|
+
return;
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
lastErr = err;
|
|
1970
|
+
if (attempt < this.configSetRetries && this.configSetRetryDelayMs > 0) await delay$1(this.configSetRetryDelayMs * 2 ** attempt);
|
|
1971
|
+
}
|
|
1972
|
+
throw new Error(configSetErrorMessage(lastErr, args));
|
|
1836
1973
|
}
|
|
1837
1974
|
/**
|
|
1838
1975
|
* Read back a single config path and compare it to the value we tried to
|
|
@@ -1873,17 +2010,32 @@ var OpenClawApplier = class {
|
|
|
1873
2010
|
}
|
|
1874
2011
|
return false;
|
|
1875
2012
|
}
|
|
1876
|
-
|
|
2013
|
+
applyPlugin(spec, _installPath, opts) {
|
|
2014
|
+
return this.cliLock.run(() => this.applyPluginLocked(spec, opts));
|
|
2015
|
+
}
|
|
2016
|
+
async applyPluginLocked(spec, opts) {
|
|
1877
2017
|
const pkg = stripPluginVersion(spec);
|
|
1878
|
-
await this.
|
|
2018
|
+
await this.ensurePluginsAllowUnlocked(pkg);
|
|
1879
2019
|
this.cleanupUntrackedExtensionInstall(pkg);
|
|
1880
2020
|
if (opts?.force && this.isPluginInstalled(pkg)) {
|
|
2021
|
+
const pinnedVersion = pluginSpecVersion(spec);
|
|
2022
|
+
const installedVersion = this.installedPluginVersion(pkg);
|
|
2023
|
+
if (pinnedVersion && installedVersion && installedVersion === pinnedVersion) {
|
|
2024
|
+
log$3.info({
|
|
2025
|
+
pkg,
|
|
2026
|
+
spec,
|
|
2027
|
+
version: installedVersion
|
|
2028
|
+
}, "Force mode — installed version already matches pinned spec, skipping uninstall+reinstall");
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
1881
2031
|
log$3.info({
|
|
1882
2032
|
pkg,
|
|
1883
|
-
spec
|
|
2033
|
+
spec,
|
|
2034
|
+
installedVersion,
|
|
2035
|
+
pinnedVersion
|
|
1884
2036
|
}, "Force mode — uninstalling plugin before reinstall");
|
|
1885
2037
|
try {
|
|
1886
|
-
await this.
|
|
2038
|
+
await this.removePluginUnlocked(pkg);
|
|
1887
2039
|
} catch (err) {
|
|
1888
2040
|
log$3.warn({
|
|
1889
2041
|
pkg,
|
|
@@ -1928,14 +2080,20 @@ var OpenClawApplier = class {
|
|
|
1928
2080
|
}
|
|
1929
2081
|
}
|
|
1930
2082
|
/**
|
|
1931
|
-
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
2083
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
|
|
1932
2084
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
|
1933
2085
|
*
|
|
2086
|
+
* This is a read-modify-write (`config get plugins.allow` → union → `config
|
|
2087
|
+
* set`). The caller MUST hold the shared CLI lock across the WHOLE call so no
|
|
2088
|
+
* other openclaw process mutates `plugins.allow` between the read and the write
|
|
2089
|
+
* (that would clobber the concurrent change). All callers wrap it in
|
|
2090
|
+
* `cliLock.run(...)` — never invoke it outside a locked section.
|
|
2091
|
+
*
|
|
1934
2092
|
* Prefer passing the FULL set of plugins for an integration in a single call
|
|
1935
2093
|
* (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
|
|
1936
2094
|
* one write for N plugins is one reload instead of N.
|
|
1937
2095
|
*/
|
|
1938
|
-
async
|
|
2096
|
+
async ensurePluginsAllowUnlocked(pkgs) {
|
|
1939
2097
|
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
1940
2098
|
let currentAllow = [];
|
|
1941
2099
|
try {
|
|
@@ -1951,7 +2109,7 @@ var OpenClawApplier = class {
|
|
|
1951
2109
|
if (missing.length === 0) return;
|
|
1952
2110
|
const updated = [...currentAllow, ...missing];
|
|
1953
2111
|
try {
|
|
1954
|
-
await this.
|
|
2112
|
+
await this.runConfigSetUnlocked([
|
|
1955
2113
|
"plugins.allow",
|
|
1956
2114
|
JSON.stringify(updated),
|
|
1957
2115
|
"--merge"
|
|
@@ -1968,11 +2126,13 @@ var OpenClawApplier = class {
|
|
|
1968
2126
|
* write, before any are installed. Called once by the manager ahead of the
|
|
1969
2127
|
* per-plugin install loop so activation triggers one hot-reload for the
|
|
1970
2128
|
* allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
|
|
1971
|
-
* `
|
|
2129
|
+
* `ensurePluginsAllowUnlocked` then finds nothing missing and is a no-op.
|
|
2130
|
+
*
|
|
2131
|
+
* Acquires the shared CLI lock for the whole read-modify-write.
|
|
1972
2132
|
*/
|
|
1973
|
-
|
|
1974
|
-
if (specs.length === 0) return;
|
|
1975
|
-
|
|
2133
|
+
ensurePluginsAllowed(specs) {
|
|
2134
|
+
if (specs.length === 0) return Promise.resolve();
|
|
2135
|
+
return this.cliLock.run(() => this.ensurePluginsAllowUnlocked(specs.map(stripPluginVersion)));
|
|
1976
2136
|
}
|
|
1977
2137
|
/**
|
|
1978
2138
|
* Check if a plugin is already installed.
|
|
@@ -1990,6 +2150,25 @@ var OpenClawApplier = class {
|
|
|
1990
2150
|
return readdirSync(extensionsDir).some((dir) => dir.startsWith(prefix) && /^[0-9a-f]+$/i.test(dir.slice(prefix.length)));
|
|
1991
2151
|
}
|
|
1992
2152
|
/**
|
|
2153
|
+
* Best-effort read of the installed version of a plugin, or `undefined` when
|
|
2154
|
+
* it can't be determined. Reads the version from the installed package's
|
|
2155
|
+
* package.json under the npm registry path OpenClaw 2026.5+ installs to
|
|
2156
|
+
* (`~/.openclaw/npm/node_modules/<pkg>/package.json`) — the same root
|
|
2157
|
+
* `isPluginInstalled` checks. 2026.4 extensions-dir installs have no
|
|
2158
|
+
* stable package.json version here and return `undefined` (callers fail
|
|
2159
|
+
* open to the safe reinstall path).
|
|
2160
|
+
*/
|
|
2161
|
+
installedPluginVersion(pkg) {
|
|
2162
|
+
const pkgJsonPath = join(this.home, "npm", "node_modules", ...pkg.split("/"), "package.json");
|
|
2163
|
+
if (!existsSync(pkgJsonPath)) return void 0;
|
|
2164
|
+
try {
|
|
2165
|
+
const data = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
2166
|
+
return typeof data.version === "string" ? data.version : void 0;
|
|
2167
|
+
} catch {
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
1993
2172
|
* Remove an extensions/ install that has no matching record in
|
|
1994
2173
|
* plugins/installs.json. OpenClaw 2026.5+ tracks installs via the records
|
|
1995
2174
|
* file — a leftover dir from 2026.4 is invisible to `openclaw plugins
|
|
@@ -2034,7 +2213,15 @@ var OpenClawApplier = class {
|
|
|
2034
2213
|
}
|
|
2035
2214
|
}
|
|
2036
2215
|
}
|
|
2037
|
-
|
|
2216
|
+
removePlugin(spec) {
|
|
2217
|
+
return this.cliLock.run(() => this.removePluginUnlocked(spec));
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
|
|
2221
|
+
* by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
|
|
2222
|
+
* path). Never call outside a locked section.
|
|
2223
|
+
*/
|
|
2224
|
+
async removePluginUnlocked(spec) {
|
|
2038
2225
|
await execFileAsync$1("openclaw", [
|
|
2039
2226
|
"plugins",
|
|
2040
2227
|
"uninstall",
|
|
@@ -2048,26 +2235,28 @@ var OpenClawApplier = class {
|
|
|
2048
2235
|
cpSync(srcPath, join(this.skillsDir, name), { recursive: true });
|
|
2049
2236
|
return Promise.resolve();
|
|
2050
2237
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
"
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2238
|
+
applyClawHubSkill(slug) {
|
|
2239
|
+
return this.cliLock.run(async () => {
|
|
2240
|
+
log$3.info({ slug }, "Installing skill from ClawHub");
|
|
2241
|
+
try {
|
|
2242
|
+
await execFileAsync$1("openclaw", [
|
|
2243
|
+
"skills",
|
|
2244
|
+
"install",
|
|
2245
|
+
slug
|
|
2246
|
+
], { timeout: 6e4 });
|
|
2247
|
+
log$3.info({ slug }, "ClawHub skill installed");
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2250
|
+
if (msg.includes("already exists") || msg.includes("Skill already exists")) {
|
|
2251
|
+
log$3.info({ slug }, "ClawHub skill already installed (skipped)");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
log$3.error({
|
|
2255
|
+
slug,
|
|
2256
|
+
err: msg
|
|
2257
|
+
}, "ClawHub skill install failed");
|
|
2065
2258
|
}
|
|
2066
|
-
|
|
2067
|
-
slug,
|
|
2068
|
-
err: msg
|
|
2069
|
-
}, "ClawHub skill install failed");
|
|
2070
|
-
}
|
|
2259
|
+
});
|
|
2071
2260
|
}
|
|
2072
2261
|
removeClawHubSkill(slug) {
|
|
2073
2262
|
const workspaceSkillsDir = join(this.agentWorkspace, "skills", slug);
|
|
@@ -2094,7 +2283,10 @@ var OpenClawApplier = class {
|
|
|
2094
2283
|
* Each integration's config contribution is tracked in the tracking file
|
|
2095
2284
|
* (config.json) so it can be cleanly removed later.
|
|
2096
2285
|
*/
|
|
2097
|
-
|
|
2286
|
+
applyConfig(integrationId, config) {
|
|
2287
|
+
return this.cliLock.run(() => this.applyConfigLocked(integrationId, config));
|
|
2288
|
+
}
|
|
2289
|
+
async applyConfigLocked(integrationId, config) {
|
|
2098
2290
|
const tracking = this.readTracking();
|
|
2099
2291
|
const integrations = tracking._integrations ?? {};
|
|
2100
2292
|
integrations[integrationId] = config;
|
|
@@ -2105,7 +2297,7 @@ var OpenClawApplier = class {
|
|
|
2105
2297
|
const merged = { ...await readParentObject(parentPath) };
|
|
2106
2298
|
for (const [k, v] of dottedKvs) merged[k] = v;
|
|
2107
2299
|
try {
|
|
2108
|
-
await this.
|
|
2300
|
+
await this.runConfigSetUnlocked([
|
|
2109
2301
|
parentPath,
|
|
2110
2302
|
JSON.stringify(merged),
|
|
2111
2303
|
"--merge"
|
|
@@ -2126,14 +2318,14 @@ var OpenClawApplier = class {
|
|
|
2126
2318
|
}
|
|
2127
2319
|
}
|
|
2128
2320
|
if (leaves.length > 0) try {
|
|
2129
|
-
await this.
|
|
2321
|
+
await this.runConfigSetUnlocked([
|
|
2130
2322
|
"--batch-json",
|
|
2131
2323
|
JSON.stringify(leaves),
|
|
2132
2324
|
"--replace"
|
|
2133
|
-
]);
|
|
2325
|
+
], { timeout: Math.min(Math.max(3e4, leaves.length * 2e3), 12e4) });
|
|
2134
2326
|
} catch (err) {
|
|
2135
2327
|
if (await this.verifyApplied(async () => {
|
|
2136
|
-
return (await
|
|
2328
|
+
return (await mapWithConcurrency(leaves, VERIFY_GET_CONCURRENCY, (l) => this.configValueMatches(l.path, l.value))).every(Boolean);
|
|
2137
2329
|
})) log$3.warn({ count: leaves.length }, "openclaw config set --batch-json exited non-zero but config landed — continuing");
|
|
2138
2330
|
else {
|
|
2139
2331
|
log$3.error({
|
|
@@ -2150,14 +2342,17 @@ var OpenClawApplier = class {
|
|
|
2150
2342
|
* Reads the tracking file to find which config keys this integration set,
|
|
2151
2343
|
* then removes them via `openclaw config unset`.
|
|
2152
2344
|
*/
|
|
2153
|
-
|
|
2345
|
+
removeConfig(integrationId) {
|
|
2346
|
+
return this.cliLock.run(() => this.removeConfigLocked(integrationId));
|
|
2347
|
+
}
|
|
2348
|
+
async removeConfigLocked(integrationId) {
|
|
2154
2349
|
const tracking = this.readTracking();
|
|
2155
2350
|
const integrations = tracking._integrations ?? {};
|
|
2156
2351
|
if (!(integrationId in integrations)) return;
|
|
2157
2352
|
const integrationConfig = integrations[integrationId];
|
|
2158
2353
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
2159
2354
|
for (const { path } of leaves) try {
|
|
2160
|
-
await this.
|
|
2355
|
+
await this.runConfigCommandUnlocked(["unset", path]);
|
|
2161
2356
|
} catch (err) {
|
|
2162
2357
|
log$3.warn({
|
|
2163
2358
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -2169,8 +2364,8 @@ var OpenClawApplier = class {
|
|
|
2169
2364
|
const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
|
|
2170
2365
|
if (Object.keys(existing).length === 0) continue;
|
|
2171
2366
|
try {
|
|
2172
|
-
if (Object.keys(remaining).length === 0) await this.
|
|
2173
|
-
else await this.
|
|
2367
|
+
if (Object.keys(remaining).length === 0) await this.runConfigCommandUnlocked(["unset", parentPath]);
|
|
2368
|
+
else await this.runConfigSetUnlocked([
|
|
2174
2369
|
parentPath,
|
|
2175
2370
|
JSON.stringify(remaining),
|
|
2176
2371
|
"--replace"
|
|
@@ -2196,7 +2391,7 @@ var OpenClawApplier = class {
|
|
|
2196
2391
|
* config writes trigger.
|
|
2197
2392
|
*/
|
|
2198
2393
|
setConfigRaw(key, value) {
|
|
2199
|
-
return this.
|
|
2394
|
+
return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
|
|
2200
2395
|
}
|
|
2201
2396
|
isAvailable() {
|
|
2202
2397
|
return Promise.resolve(existsSync(this.home));
|
|
@@ -2948,4 +3143,4 @@ var IntegrationManagerAdapter = class {
|
|
|
2948
3143
|
}
|
|
2949
3144
|
};
|
|
2950
3145
|
//#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 };
|
|
3146
|
+
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