@alfe.ai/integrations 0.3.2 → 0.4.1
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 +62 -0
- package/dist/index.js +164 -20
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -359,6 +359,22 @@ interface RuntimeApplier {
|
|
|
359
359
|
* its absence and degrade gracefully.
|
|
360
360
|
*/
|
|
361
361
|
setConfigRaw?(key: string, value: string): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Optional: read a single raw config key back from the runtime, returning the
|
|
364
|
+
* scalar value as a string (or `undefined` when the key is unset / can't be
|
|
365
|
+
* read). Used by the daemon's config-reconcile pass (plan A4) to value-diff
|
|
366
|
+
* before writing (skip when equal) and to verify a write landed.
|
|
367
|
+
*
|
|
368
|
+
* MUST normalize the runtime's `config get` output to the same scalar string
|
|
369
|
+
* that `setConfigRaw` was called with (e.g. unwrap the JSON quoting OpenClaw's
|
|
370
|
+
* `config get` adds), so a `getConfigRaw(k) === want` compare is exact.
|
|
371
|
+
*
|
|
372
|
+
* Read-back can be a false negative under load (a 2-vCPU box running a
|
|
373
|
+
* `config get` while the runtime hot-reloads) — callers MUST retry with the
|
|
374
|
+
* same cadence as a write verify, never one-shot. Appliers that can't read
|
|
375
|
+
* config omit this method; callers degrade gracefully (blind set, no diff).
|
|
376
|
+
*/
|
|
377
|
+
getConfigRaw?(key: string): Promise<string | undefined>;
|
|
362
378
|
/** Check if this runtime is available (e.g. workspace directory exists) */
|
|
363
379
|
isAvailable(): Promise<boolean>;
|
|
364
380
|
}
|
|
@@ -1207,6 +1223,29 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
1207
1223
|
* the lock is NOT re-entrant, so this stays an `*Unlocked` internal.
|
|
1208
1224
|
*/
|
|
1209
1225
|
private dropSubtreeKeysUnlocked;
|
|
1226
|
+
/**
|
|
1227
|
+
* `openclaw config unset <leafPath>`, UNLOCKED, with a fallback for custom
|
|
1228
|
+
* model providers. Unsetting a single leaf under a custom provider (e.g.
|
|
1229
|
+
* `models.providers.zhipu.baseUrl`) is REJECTED by schema validation — a
|
|
1230
|
+
* custom provider without baseUrl is an invalid partial — so the leaf is
|
|
1231
|
+
* never removed and stale/broken config leaks on every removal cycle. On that
|
|
1232
|
+
* specific validation failure, fall back to unsetting the IMMEDIATE parent
|
|
1233
|
+
* subtree (`models.providers.zhipu`), which validates cleanly and removes the
|
|
1234
|
+
* provider whole. The rejection is itself the signal that our key is
|
|
1235
|
+
* STRUCTURAL to the provider (the object can't validly survive without it), so
|
|
1236
|
+
* removing the provider node is the correct terminal state — and we only ever
|
|
1237
|
+
* escalate to the immediate parent, never a wider path.
|
|
1238
|
+
*
|
|
1239
|
+
* `unsetParents` dedupes across the sibling leaves of one removal pass: once
|
|
1240
|
+
* the provider subtree is unset for `models.providers.zhipu.baseUrl`, the
|
|
1241
|
+
* follow-up `.apiKey` / `.models` leaves skip re-issuing the parent unset.
|
|
1242
|
+
*
|
|
1243
|
+
* Warn-tolerant (a failed unset of an already-gone key must never fail the
|
|
1244
|
+
* caller) and assumes the shared CLI lock is held — stays an `*Unlocked`
|
|
1245
|
+
* internal since the lock is NOT re-entrant. Shared by `removeConfig`
|
|
1246
|
+
* (whole-integration teardown) and `applyConfig`'s stale-leaf diff.
|
|
1247
|
+
*/
|
|
1248
|
+
private unsetLeafPathUnlocked;
|
|
1210
1249
|
/**
|
|
1211
1250
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
1212
1251
|
*
|
|
@@ -1218,6 +1257,21 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
1218
1257
|
* config writes trigger.
|
|
1219
1258
|
*/
|
|
1220
1259
|
setConfigRaw(key: string, value: string): Promise<void>;
|
|
1260
|
+
/**
|
|
1261
|
+
* Read a single config key back — `openclaw config get <key>` — and normalize
|
|
1262
|
+
* to the scalar string that `setConfigRaw` would have written. `config get`
|
|
1263
|
+
* emits JSON, so a scalar comes back quoted (`"claude-sonnet-4-6"`); parse it
|
|
1264
|
+
* to the raw value so a `=== want` compare in the daemon's config-reconcile
|
|
1265
|
+
* pass is exact.
|
|
1266
|
+
*
|
|
1267
|
+
* Returns `undefined` when the key is unset, the command fails, or the runtime
|
|
1268
|
+
* REDACTS the value (a sensitive key we can't compare — treat as "unknown" so
|
|
1269
|
+
* the caller doesn't spuriously diff). Under the shared CLI lock so it can't
|
|
1270
|
+
* interleave with a concurrent mutation. NOT self-healing on a malformed state
|
|
1271
|
+
* DB — a read failure returns `undefined`, and the caller's set path (which IS
|
|
1272
|
+
* healing) recovers.
|
|
1273
|
+
*/
|
|
1274
|
+
getConfigRaw(key: string): Promise<string | undefined>;
|
|
1221
1275
|
isAvailable(): Promise<boolean>;
|
|
1222
1276
|
private readTracking;
|
|
1223
1277
|
private writeTracking;
|
|
@@ -1279,6 +1333,14 @@ declare class HermesApplier implements RuntimeApplier {
|
|
|
1279
1333
|
* per-integration removal accounting). Reuses the serialized queue.
|
|
1280
1334
|
*/
|
|
1281
1335
|
setConfigRaw(key: string, value: string): Promise<void>;
|
|
1336
|
+
/**
|
|
1337
|
+
* Read a single config key back — `hermes config get <key>` — normalized to
|
|
1338
|
+
* the scalar string `setConfigRaw` would have written (used by the daemon's
|
|
1339
|
+
* config-reconcile value-diff + verify, plan A4). Returns `undefined` when the
|
|
1340
|
+
* key is unset or the command fails. Serialized onto the same config queue so
|
|
1341
|
+
* it can't interleave with an in-flight `hermes config set`.
|
|
1342
|
+
*/
|
|
1343
|
+
getConfigRaw(key: string): Promise<string | undefined>;
|
|
1282
1344
|
/**
|
|
1283
1345
|
* Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
|
|
1284
1346
|
* (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
|
package/dist/index.js
CHANGED
|
@@ -2166,6 +2166,22 @@ function isMalformedStateDbError(err) {
|
|
|
2166
2166
|
const e = err;
|
|
2167
2167
|
return `${e.message}\n${e.stderr ?? ""}\n${e.stdout ?? ""}`.toLowerCase().includes(MALFORMED_STATE_DB_PHRASE);
|
|
2168
2168
|
}
|
|
2169
|
+
/**
|
|
2170
|
+
* OpenClaw rejects `config unset` of a single leaf under a CUSTOM model provider
|
|
2171
|
+
* when the removal would leave a schema-invalid partial — e.g. unsetting
|
|
2172
|
+
* `models.providers.zhipu.baseUrl` fails with "custom model providers must
|
|
2173
|
+
* declare baseUrl; provider overlays without baseUrl are only supported for
|
|
2174
|
+
* bundled providers." Unsetting the WHOLE provider subtree
|
|
2175
|
+
* (`models.providers.zhipu`) validates cleanly. Detect this failure so the
|
|
2176
|
+
* removal path can fall back to unsetting the parent subtree instead of leaving
|
|
2177
|
+
* stale/broken config behind. The phrase is stable and specific enough to match
|
|
2178
|
+
* directly (case-insensitive); it covers baseUrl and any other required
|
|
2179
|
+
* structural field ("...must declare <field>").
|
|
2180
|
+
*/
|
|
2181
|
+
const INVALID_PARTIAL_PROVIDER_PHRASE = "custom model providers must declare";
|
|
2182
|
+
function isInvalidPartialProviderUnset(err) {
|
|
2183
|
+
return (err instanceof Error ? err.message : String(err)).toLowerCase().includes(INVALID_PARTIAL_PROVIDER_PHRASE);
|
|
2184
|
+
}
|
|
2169
2185
|
const delay$1 = (ms) => new Promise((resolve) => {
|
|
2170
2186
|
setTimeout(resolve, ms);
|
|
2171
2187
|
});
|
|
@@ -2650,23 +2666,39 @@ var OpenClawApplier = class {
|
|
|
2650
2666
|
async ensurePluginsAllowUnlocked(pkgs) {
|
|
2651
2667
|
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
2652
2668
|
let currentAllow = [];
|
|
2669
|
+
let readTrustworthy = true;
|
|
2653
2670
|
try {
|
|
2654
2671
|
const { stdout } = await execFileAsync$1("openclaw", [
|
|
2655
2672
|
"config",
|
|
2656
2673
|
"get",
|
|
2657
2674
|
"plugins.allow"
|
|
2658
2675
|
], { timeout: 1e4 });
|
|
2659
|
-
const
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2676
|
+
const trimmed = stdout.trim();
|
|
2677
|
+
if (trimmed === "") {} else try {
|
|
2678
|
+
const parsed = JSON.parse(trimmed);
|
|
2679
|
+
if (Array.isArray(parsed)) currentAllow = parsed;
|
|
2680
|
+
else if (parsed === null) {} else readTrustworthy = false;
|
|
2681
|
+
} catch {
|
|
2682
|
+
readTrustworthy = false;
|
|
2683
|
+
}
|
|
2684
|
+
} catch {
|
|
2685
|
+
readTrustworthy = false;
|
|
2686
|
+
}
|
|
2662
2687
|
const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
|
|
2663
2688
|
if (missing.length === 0) return;
|
|
2689
|
+
if (!readTrustworthy) {
|
|
2690
|
+
log$3.warn({
|
|
2691
|
+
pkgs: wanted,
|
|
2692
|
+
missing
|
|
2693
|
+
}, "plugins.allow read was not trustworthy (non-empty parse failure or command error) — skipping the allow-list write to avoid replacing a value not proven to be a superset");
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2664
2696
|
const updated = [...currentAllow, ...missing];
|
|
2665
2697
|
try {
|
|
2666
2698
|
await this.runConfigSetUnlocked([
|
|
2667
2699
|
"plugins.allow",
|
|
2668
2700
|
JSON.stringify(updated),
|
|
2669
|
-
"--
|
|
2701
|
+
"--replace"
|
|
2670
2702
|
]);
|
|
2671
2703
|
} catch (err) {
|
|
2672
2704
|
log$3.warn({
|
|
@@ -2851,14 +2883,8 @@ var OpenClawApplier = class {
|
|
|
2851
2883
|
const prev = partitionEntries(flattenConfig(previous));
|
|
2852
2884
|
const nextLeafPaths = new Set(leaves.map((l) => l.path));
|
|
2853
2885
|
const staleLeaves = prev.leaves.filter((l) => !nextLeafPaths.has(l.path));
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
} catch (err) {
|
|
2857
|
-
log$3.warn({
|
|
2858
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2859
|
-
path
|
|
2860
|
-
}, "Failed to unset stale config leaf during applyConfig diff");
|
|
2861
|
-
}
|
|
2886
|
+
const unsetParents = /* @__PURE__ */ new Set();
|
|
2887
|
+
for (const { path } of staleLeaves) await this.unsetLeafPathUnlocked(path, unsetParents);
|
|
2862
2888
|
for (const [parentPath, prevKvs] of prev.subtreesByParent) {
|
|
2863
2889
|
const nextKvs = subtreesByParent.get(parentPath);
|
|
2864
2890
|
const goneKeys = [...prevKvs.keys()].filter((k) => !nextKvs?.has(k));
|
|
@@ -2926,14 +2952,8 @@ var OpenClawApplier = class {
|
|
|
2926
2952
|
if (!(integrationId in integrations)) return;
|
|
2927
2953
|
const integrationConfig = integrations[integrationId];
|
|
2928
2954
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
} catch (err) {
|
|
2932
|
-
log$3.warn({
|
|
2933
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2934
|
-
path
|
|
2935
|
-
}, "Failed to unset config via openclaw config unset");
|
|
2936
|
-
}
|
|
2955
|
+
const unsetParents = /* @__PURE__ */ new Set();
|
|
2956
|
+
for (const { path } of leaves) await this.unsetLeafPathUnlocked(path, unsetParents);
|
|
2937
2957
|
for (const [parentPath, dottedKvs] of subtreesByParent) await this.dropSubtreeKeysUnlocked(parentPath, new Set(dottedKvs.keys()));
|
|
2938
2958
|
tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
|
|
2939
2959
|
this.writeTracking(tracking);
|
|
@@ -2968,6 +2988,56 @@ var OpenClawApplier = class {
|
|
|
2968
2988
|
}
|
|
2969
2989
|
}
|
|
2970
2990
|
/**
|
|
2991
|
+
* `openclaw config unset <leafPath>`, UNLOCKED, with a fallback for custom
|
|
2992
|
+
* model providers. Unsetting a single leaf under a custom provider (e.g.
|
|
2993
|
+
* `models.providers.zhipu.baseUrl`) is REJECTED by schema validation — a
|
|
2994
|
+
* custom provider without baseUrl is an invalid partial — so the leaf is
|
|
2995
|
+
* never removed and stale/broken config leaks on every removal cycle. On that
|
|
2996
|
+
* specific validation failure, fall back to unsetting the IMMEDIATE parent
|
|
2997
|
+
* subtree (`models.providers.zhipu`), which validates cleanly and removes the
|
|
2998
|
+
* provider whole. The rejection is itself the signal that our key is
|
|
2999
|
+
* STRUCTURAL to the provider (the object can't validly survive without it), so
|
|
3000
|
+
* removing the provider node is the correct terminal state — and we only ever
|
|
3001
|
+
* escalate to the immediate parent, never a wider path.
|
|
3002
|
+
*
|
|
3003
|
+
* `unsetParents` dedupes across the sibling leaves of one removal pass: once
|
|
3004
|
+
* the provider subtree is unset for `models.providers.zhipu.baseUrl`, the
|
|
3005
|
+
* follow-up `.apiKey` / `.models` leaves skip re-issuing the parent unset.
|
|
3006
|
+
*
|
|
3007
|
+
* Warn-tolerant (a failed unset of an already-gone key must never fail the
|
|
3008
|
+
* caller) and assumes the shared CLI lock is held — stays an `*Unlocked`
|
|
3009
|
+
* internal since the lock is NOT re-entrant. Shared by `removeConfig`
|
|
3010
|
+
* (whole-integration teardown) and `applyConfig`'s stale-leaf diff.
|
|
3011
|
+
*/
|
|
3012
|
+
async unsetLeafPathUnlocked(path, unsetParents) {
|
|
3013
|
+
try {
|
|
3014
|
+
await this.runConfigCommandUnlocked(["unset", path]);
|
|
3015
|
+
} catch (err) {
|
|
3016
|
+
const parentPath = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : void 0;
|
|
3017
|
+
if (parentPath && isInvalidPartialProviderUnset(err)) {
|
|
3018
|
+
if (unsetParents.has(parentPath)) return;
|
|
3019
|
+
unsetParents.add(parentPath);
|
|
3020
|
+
try {
|
|
3021
|
+
await this.runConfigCommandUnlocked(["unset", parentPath]);
|
|
3022
|
+
log$3.warn({
|
|
3023
|
+
path,
|
|
3024
|
+
parentPath
|
|
3025
|
+
}, "Leaf unset rejected as an invalid partial custom provider — unset the parent subtree instead");
|
|
3026
|
+
} catch (parentErr) {
|
|
3027
|
+
log$3.warn({
|
|
3028
|
+
err: parentErr instanceof Error ? parentErr.message : String(parentErr),
|
|
3029
|
+
parentPath
|
|
3030
|
+
}, "Failed to unset parent subtree after an invalid-partial leaf unset");
|
|
3031
|
+
}
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
log$3.warn({
|
|
3035
|
+
err: err instanceof Error ? err.message : String(err),
|
|
3036
|
+
path
|
|
3037
|
+
}, "Failed to unset config via openclaw config unset");
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
2971
3041
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
2972
3042
|
*
|
|
2973
3043
|
* Deliberately bypasses the `_integrations` tracking that `applyConfig`
|
|
@@ -2980,6 +3050,46 @@ var OpenClawApplier = class {
|
|
|
2980
3050
|
setConfigRaw(key, value) {
|
|
2981
3051
|
return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
|
|
2982
3052
|
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Read a single config key back — `openclaw config get <key>` — and normalize
|
|
3055
|
+
* to the scalar string that `setConfigRaw` would have written. `config get`
|
|
3056
|
+
* emits JSON, so a scalar comes back quoted (`"claude-sonnet-4-6"`); parse it
|
|
3057
|
+
* to the raw value so a `=== want` compare in the daemon's config-reconcile
|
|
3058
|
+
* pass is exact.
|
|
3059
|
+
*
|
|
3060
|
+
* Returns `undefined` when the key is unset, the command fails, or the runtime
|
|
3061
|
+
* REDACTS the value (a sensitive key we can't compare — treat as "unknown" so
|
|
3062
|
+
* the caller doesn't spuriously diff). Under the shared CLI lock so it can't
|
|
3063
|
+
* interleave with a concurrent mutation. NOT self-healing on a malformed state
|
|
3064
|
+
* DB — a read failure returns `undefined`, and the caller's set path (which IS
|
|
3065
|
+
* healing) recovers.
|
|
3066
|
+
*/
|
|
3067
|
+
getConfigRaw(key) {
|
|
3068
|
+
return this.cliLock.run(async () => {
|
|
3069
|
+
try {
|
|
3070
|
+
const { stdout } = await execFileAsync$1("openclaw", [
|
|
3071
|
+
"config",
|
|
3072
|
+
"get",
|
|
3073
|
+
key
|
|
3074
|
+
], { timeout: 1e4 });
|
|
3075
|
+
const trimmed = stdout.trim();
|
|
3076
|
+
if (!trimmed) return void 0;
|
|
3077
|
+
let parsed;
|
|
3078
|
+
try {
|
|
3079
|
+
parsed = JSON.parse(trimmed);
|
|
3080
|
+
} catch {
|
|
3081
|
+
return trimmed;
|
|
3082
|
+
}
|
|
3083
|
+
if (parsed === null || parsed === void 0) return void 0;
|
|
3084
|
+
if (parsed === OPENCLAW_REDACTED) return void 0;
|
|
3085
|
+
if (typeof parsed === "string") return parsed;
|
|
3086
|
+
if (typeof parsed === "number" || typeof parsed === "boolean") return String(parsed);
|
|
3087
|
+
return JSON.stringify(parsed);
|
|
3088
|
+
} catch {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
2983
3093
|
isAvailable() {
|
|
2984
3094
|
return Promise.resolve(existsSync(this.home));
|
|
2985
3095
|
}
|
|
@@ -3185,6 +3295,40 @@ var HermesApplier = class {
|
|
|
3185
3295
|
return this.runConfigSet([key, value]);
|
|
3186
3296
|
}
|
|
3187
3297
|
/**
|
|
3298
|
+
* Read a single config key back — `hermes config get <key>` — normalized to
|
|
3299
|
+
* the scalar string `setConfigRaw` would have written (used by the daemon's
|
|
3300
|
+
* config-reconcile value-diff + verify, plan A4). Returns `undefined` when the
|
|
3301
|
+
* key is unset or the command fails. Serialized onto the same config queue so
|
|
3302
|
+
* it can't interleave with an in-flight `hermes config set`.
|
|
3303
|
+
*/
|
|
3304
|
+
getConfigRaw(key) {
|
|
3305
|
+
const run = async () => {
|
|
3306
|
+
try {
|
|
3307
|
+
const { stdout } = await execFileAsync("hermes", [
|
|
3308
|
+
"config",
|
|
3309
|
+
"get",
|
|
3310
|
+
key
|
|
3311
|
+
], { timeout: 1e4 });
|
|
3312
|
+
const trimmed = stdout.trim();
|
|
3313
|
+
if (!trimmed) return void 0;
|
|
3314
|
+
try {
|
|
3315
|
+
const parsed = JSON.parse(trimmed);
|
|
3316
|
+
if (parsed === null || parsed === void 0) return void 0;
|
|
3317
|
+
if (typeof parsed === "string") return parsed;
|
|
3318
|
+
if (typeof parsed === "number" || typeof parsed === "boolean") return String(parsed);
|
|
3319
|
+
return JSON.stringify(parsed);
|
|
3320
|
+
} catch {
|
|
3321
|
+
return trimmed;
|
|
3322
|
+
}
|
|
3323
|
+
} catch {
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
};
|
|
3327
|
+
const result = this.configSetQueue.then(run, run);
|
|
3328
|
+
this.configSetQueue = result.catch(() => void 0);
|
|
3329
|
+
return result;
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3188
3332
|
* Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
|
|
3189
3333
|
* (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
|
|
3190
3334
|
* them. The manager catches per-plugin, so a skip here is a correct no-op.
|
package/package.json
CHANGED