@alfe.ai/integrations 0.2.1 → 0.2.3
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 +25 -18
- package/dist/index.js +96 -60
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -913,9 +913,11 @@ declare class HermesApplier implements RuntimeApplier {
|
|
|
913
913
|
readonly runtime = "hermes";
|
|
914
914
|
private home;
|
|
915
915
|
private trackingPath;
|
|
916
|
+
/** `~/.hermes/config.yaml` — read-merge-written on removal (no `config unset`). */
|
|
917
|
+
private configYamlPath;
|
|
916
918
|
private configSetRetries;
|
|
917
919
|
private configSetRetryDelayMs;
|
|
918
|
-
/** Serializes all `
|
|
920
|
+
/** Serializes all config mutations (CLI `set` + config.yaml delete) so they never interleave. */
|
|
919
921
|
private configSetQueue;
|
|
920
922
|
constructor(options?: HermesApplierOptions);
|
|
921
923
|
/**
|
|
@@ -929,8 +931,9 @@ declare class HermesApplier implements RuntimeApplier {
|
|
|
929
931
|
/**
|
|
930
932
|
* Remove config previously applied by an integration.
|
|
931
933
|
*
|
|
932
|
-
* Reads the tracking file to find which keys this integration set, then
|
|
933
|
-
*
|
|
934
|
+
* Reads the tracking file to find which dotted keys this integration set, then
|
|
935
|
+
* DELETES each from `~/.hermes/config.yaml` via a read-merge-write (there is no
|
|
936
|
+
* `hermes config unset` verb — see the file header). Clears the tracking entry.
|
|
934
937
|
*/
|
|
935
938
|
removeConfig(integrationId: string): Promise<void>;
|
|
936
939
|
/**
|
|
@@ -967,21 +970,25 @@ declare class HermesApplier implements RuntimeApplier {
|
|
|
967
970
|
/** Convenience: `hermes config set <args>`, serialized + retried. */
|
|
968
971
|
private runConfigSet;
|
|
969
972
|
/**
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
*
|
|
973
|
+
* Delete a set of dotted config keys from `~/.hermes/config.yaml`. There is no
|
|
974
|
+
* `hermes config unset` verb (see file header), so removal is a read-merge-write
|
|
975
|
+
* of the YAML via the Document API — preserving user-authored keys, comments,
|
|
976
|
+
* and formatting. Serialized against every other config mutation on the same
|
|
977
|
+
* queue so a delete can't interleave with an in-flight `hermes config set`.
|
|
978
|
+
*/
|
|
979
|
+
private deleteConfigKeys;
|
|
980
|
+
/**
|
|
981
|
+
* Synchronous config.yaml read-merge-write: parse the doc, delete each dotted
|
|
982
|
+
* key (`model.base_url` → `deleteIn(['model','base_url'])`), and write back only
|
|
983
|
+
* if the text actually changed. No-op if config.yaml doesn't exist yet.
|
|
984
|
+
*/
|
|
985
|
+
private deleteConfigKeysSync;
|
|
986
|
+
/**
|
|
987
|
+
* Run `hermes config <args>` (only `set` is used — removal goes via
|
|
988
|
+
* {@link deleteConfigKeys}), serialized against every other config mutation and
|
|
989
|
+
* retried with backoff. Throws an Error whose (scrubbed) message includes stderr
|
|
990
|
+
* after retries are exhausted, so the real cause propagates to the integration
|
|
991
|
+
* errorMessage without leaking the value.
|
|
985
992
|
*/
|
|
986
993
|
private runConfigCommand;
|
|
987
994
|
private readTracking;
|
package/dist/index.js
CHANGED
|
@@ -1713,27 +1713,38 @@ const delay$1 = (ms) => new Promise((resolve) => {
|
|
|
1713
1713
|
setTimeout(resolve, ms);
|
|
1714
1714
|
});
|
|
1715
1715
|
/**
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1716
|
+
* Asymmetric structural containment for verify-after-write: is every field we
|
|
1717
|
+
* INTENDED present in the ACTUAL read-back with our value? `actual` may carry
|
|
1718
|
+
* EXTRA keys that `intended` does not.
|
|
1719
|
+
*
|
|
1720
|
+
* Verify-after-write only needs to answer "did our write land?", and OpenClaw
|
|
1721
|
+
* NORMALIZES config on load — most notably it enriches every
|
|
1722
|
+
* `models.providers.*.models` entry with schema defaults (`reasoning`, `input`,
|
|
1723
|
+
* `cost`, `contextWindow`, `maxTokens`, …) that the applier never wrote. A
|
|
1724
|
+
* strict `deepEqual` against the enriched read-back is therefore GUARANTEED to
|
|
1725
|
+
* fail for model lists (extra keys → unequal key counts), which strands the
|
|
1726
|
+
* `alfe` activation in `error` forever even though the write succeeded. A subset
|
|
1727
|
+
* check tolerates enrichment while still catching a genuine failure (a value
|
|
1728
|
+
* OpenClaw never stored, or stored differently).
|
|
1729
|
+
*
|
|
1730
|
+
* Arrays stay positional and length-EXACT (enrichment adds object fields, never
|
|
1731
|
+
* elements, and never reorders); each element is compared by subset. Objects
|
|
1732
|
+
* require every `intended` key to be present-and-subset-matching in `actual`;
|
|
1733
|
+
* primitives compare by `Object.is`.
|
|
1720
1734
|
*/
|
|
1721
|
-
function
|
|
1722
|
-
if (Object.is(
|
|
1723
|
-
if (typeof
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
1727
|
-
if (
|
|
1728
|
-
if (
|
|
1729
|
-
return
|
|
1730
|
-
}
|
|
1731
|
-
const
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1734
|
-
const bKeys = Object.keys(bo);
|
|
1735
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
1736
|
-
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]));
|
|
1735
|
+
function deepSubset(intended, actual) {
|
|
1736
|
+
if (Object.is(intended, actual)) return true;
|
|
1737
|
+
if (typeof intended !== "object" || typeof actual !== "object" || intended === null || actual === null) return false;
|
|
1738
|
+
const iIsArr = Array.isArray(intended);
|
|
1739
|
+
const aIsArr = Array.isArray(actual);
|
|
1740
|
+
if (iIsArr !== aIsArr) return false;
|
|
1741
|
+
if (iIsArr && aIsArr) {
|
|
1742
|
+
if (intended.length !== actual.length) return false;
|
|
1743
|
+
return intended.every((v, i) => deepSubset(v, actual[i]));
|
|
1744
|
+
}
|
|
1745
|
+
const io = intended;
|
|
1746
|
+
const ao = actual;
|
|
1747
|
+
return Object.keys(io).every((k) => Object.prototype.hasOwnProperty.call(ao, k) && deepSubset(io[k], ao[k]));
|
|
1737
1748
|
}
|
|
1738
1749
|
/**
|
|
1739
1750
|
* Describe a `config set` target WITHOUT leaking values. The value argument is
|
|
@@ -1844,7 +1855,7 @@ var OpenClawApplier = class {
|
|
|
1844
1855
|
], { timeout: 1e4 });
|
|
1845
1856
|
const actual = JSON.parse(stdout.trim());
|
|
1846
1857
|
if (actual === OPENCLAW_REDACTED) return true;
|
|
1847
|
-
return
|
|
1858
|
+
return deepSubset(expected, actual);
|
|
1848
1859
|
} catch {
|
|
1849
1860
|
return false;
|
|
1850
1861
|
}
|
|
@@ -2094,7 +2105,7 @@ var OpenClawApplier = class {
|
|
|
2094
2105
|
} catch (err) {
|
|
2095
2106
|
if (await this.verifyApplied(async () => {
|
|
2096
2107
|
const after = await readParentObject(parentPath);
|
|
2097
|
-
return [...dottedKvs.entries()].every(([k, v]) =>
|
|
2108
|
+
return [...dottedKvs.entries()].every(([k, v]) => deepSubset(v, after[k]));
|
|
2098
2109
|
})) {
|
|
2099
2110
|
log$3.warn({ parentPath }, "openclaw config set exited non-zero but config landed — continuing");
|
|
2100
2111
|
continue;
|
|
@@ -2193,11 +2204,17 @@ var OpenClawApplier = class {
|
|
|
2193
2204
|
* HermesApplier — applies integration config (and, later, native plugins) to
|
|
2194
2205
|
* the Hermes runtime (Nous Research's Python agent).
|
|
2195
2206
|
*
|
|
2196
|
-
* Hermes config lives in `~/.hermes/config.yaml` (YAML)
|
|
2197
|
-
* `hermes config set
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2207
|
+
* Hermes config lives in `~/.hermes/config.yaml` (YAML). We APPLY config via the
|
|
2208
|
+
* `hermes config set <dotted.key> <value>` CLI (letting the runtime own its
|
|
2209
|
+
* write path, mirroring the OpenClaw rule). REMOVAL is different: `hermes config
|
|
2210
|
+
* unset` does NOT exist — CONFIRMED against Hermes source, whose `config`
|
|
2211
|
+
* subcommands are only show/set/edit/path/env-path/check/migrate. So teardown is
|
|
2212
|
+
* a config.yaml read-merge-write that deletes the Alfe-applied dotted keys via
|
|
2213
|
+
* the `yaml` Document API (`parseDocument` + `deleteIn`), preserving
|
|
2214
|
+
* user-authored keys, comments, and formatting (the same approach
|
|
2215
|
+
* `hermes-mcp-sync.ts` uses for `mcp_servers`). Per-integration contributions are
|
|
2216
|
+
* tracked in a separate file (`~/.hermes/.alfe-integrations.json`) so we know
|
|
2217
|
+
* exactly which keys to delete.
|
|
2201
2218
|
*
|
|
2202
2219
|
* Scope (Phase 1, MCP-first hybrid):
|
|
2203
2220
|
* - config: SUPPORTED — consumes `installs.runtimes.hermes.config` (the AI-proxy
|
|
@@ -2243,9 +2260,9 @@ function stringifyConfigValue(value) {
|
|
|
2243
2260
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2244
2261
|
}
|
|
2245
2262
|
/**
|
|
2246
|
-
* Describe a `config set
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2263
|
+
* Describe a `config set` target WITHOUT leaking values. The value argument can
|
|
2264
|
+
* be a secret (`model.api_key`), so it must never reach the integration's
|
|
2265
|
+
* user-facing `errorMessage`. Keep only the program + verb + key.
|
|
2249
2266
|
*/
|
|
2250
2267
|
function redactConfigTarget(args) {
|
|
2251
2268
|
return `hermes config ${args.slice(0, 2).join(" ")}`.trim();
|
|
@@ -2270,13 +2287,16 @@ var HermesApplier = class {
|
|
|
2270
2287
|
runtime = "hermes";
|
|
2271
2288
|
home;
|
|
2272
2289
|
trackingPath;
|
|
2290
|
+
/** `~/.hermes/config.yaml` — read-merge-written on removal (no `config unset`). */
|
|
2291
|
+
configYamlPath;
|
|
2273
2292
|
configSetRetries;
|
|
2274
2293
|
configSetRetryDelayMs;
|
|
2275
|
-
/** Serializes all `
|
|
2294
|
+
/** Serializes all config mutations (CLI `set` + config.yaml delete) so they never interleave. */
|
|
2276
2295
|
configSetQueue = Promise.resolve();
|
|
2277
2296
|
constructor(options = {}) {
|
|
2278
2297
|
this.home = options.home ?? options.workspace ?? DEFAULT_HERMES_HOME$1;
|
|
2279
2298
|
this.trackingPath = options.configPath ?? join(this.home, ".alfe-integrations.json");
|
|
2299
|
+
this.configYamlPath = join(this.home, "config.yaml");
|
|
2280
2300
|
this.configSetRetries = options.configSetRetries ?? CONFIG_SET_RETRIES;
|
|
2281
2301
|
this.configSetRetryDelayMs = options.configSetRetryDelayMs ?? CONFIG_SET_RETRY_DELAY_MS;
|
|
2282
2302
|
}
|
|
@@ -2311,8 +2331,9 @@ var HermesApplier = class {
|
|
|
2311
2331
|
/**
|
|
2312
2332
|
* Remove config previously applied by an integration.
|
|
2313
2333
|
*
|
|
2314
|
-
* Reads the tracking file to find which keys this integration set, then
|
|
2315
|
-
*
|
|
2334
|
+
* Reads the tracking file to find which dotted keys this integration set, then
|
|
2335
|
+
* DELETES each from `~/.hermes/config.yaml` via a read-merge-write (there is no
|
|
2336
|
+
* `hermes config unset` verb — see the file header). Clears the tracking entry.
|
|
2316
2337
|
*/
|
|
2317
2338
|
async removeConfig(integrationId) {
|
|
2318
2339
|
const tracking = this.readTracking();
|
|
@@ -2320,13 +2341,13 @@ var HermesApplier = class {
|
|
|
2320
2341
|
if (!(integrationId in integrations)) return;
|
|
2321
2342
|
const integrationConfig = integrations[integrationId];
|
|
2322
2343
|
const { leaves } = partitionEntries(flattenConfig(integrationConfig));
|
|
2323
|
-
|
|
2324
|
-
await this.
|
|
2344
|
+
try {
|
|
2345
|
+
await this.deleteConfigKeys(leaves.map(({ path }) => path));
|
|
2325
2346
|
} catch (err) {
|
|
2326
2347
|
log$2.warn({
|
|
2327
2348
|
err: err instanceof Error ? err.message : String(err),
|
|
2328
|
-
|
|
2329
|
-
}, "Failed to
|
|
2349
|
+
integrationId
|
|
2350
|
+
}, "Failed to delete Alfe config keys from ~/.hermes/config.yaml");
|
|
2330
2351
|
}
|
|
2331
2352
|
tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
|
|
2332
2353
|
this.writeTracking(tracking);
|
|
@@ -2410,23 +2431,40 @@ var HermesApplier = class {
|
|
|
2410
2431
|
return this.runConfigCommand(["set", ...setArgs]);
|
|
2411
2432
|
}
|
|
2412
2433
|
/**
|
|
2413
|
-
*
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2434
|
+
* Delete a set of dotted config keys from `~/.hermes/config.yaml`. There is no
|
|
2435
|
+
* `hermes config unset` verb (see file header), so removal is a read-merge-write
|
|
2436
|
+
* of the YAML via the Document API — preserving user-authored keys, comments,
|
|
2437
|
+
* and formatting. Serialized against every other config mutation on the same
|
|
2438
|
+
* queue so a delete can't interleave with an in-flight `hermes config set`.
|
|
2439
|
+
*/
|
|
2440
|
+
deleteConfigKeys(paths) {
|
|
2441
|
+
const run = () => {
|
|
2442
|
+
this.deleteConfigKeysSync(paths);
|
|
2443
|
+
return Promise.resolve();
|
|
2444
|
+
};
|
|
2445
|
+
const result = this.configSetQueue.then(run, run);
|
|
2446
|
+
this.configSetQueue = result.catch(() => void 0);
|
|
2447
|
+
return result;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Synchronous config.yaml read-merge-write: parse the doc, delete each dotted
|
|
2451
|
+
* key (`model.base_url` → `deleteIn(['model','base_url'])`), and write back only
|
|
2452
|
+
* if the text actually changed. No-op if config.yaml doesn't exist yet.
|
|
2421
2453
|
*/
|
|
2422
|
-
|
|
2423
|
-
|
|
2454
|
+
deleteConfigKeysSync(paths) {
|
|
2455
|
+
if (paths.length === 0 || !existsSync(this.configYamlPath)) return;
|
|
2456
|
+
const doc = parseDocument(readFileSync(this.configYamlPath, "utf-8"));
|
|
2457
|
+
const before = doc.toString();
|
|
2458
|
+
for (const path of paths) doc.deleteIn(path.split("."));
|
|
2459
|
+
const after = doc.toString();
|
|
2460
|
+
if (before !== after) writeFileSync(this.configYamlPath, after, "utf-8");
|
|
2424
2461
|
}
|
|
2425
2462
|
/**
|
|
2426
|
-
* Run `hermes config <args>` (set
|
|
2427
|
-
*
|
|
2428
|
-
*
|
|
2429
|
-
*
|
|
2463
|
+
* Run `hermes config <args>` (only `set` is used — removal goes via
|
|
2464
|
+
* {@link deleteConfigKeys}), serialized against every other config mutation and
|
|
2465
|
+
* retried with backoff. Throws an Error whose (scrubbed) message includes stderr
|
|
2466
|
+
* after retries are exhausted, so the real cause propagates to the integration
|
|
2467
|
+
* errorMessage without leaking the value.
|
|
2430
2468
|
*/
|
|
2431
2469
|
runConfigCommand(args) {
|
|
2432
2470
|
const run = async () => {
|
|
@@ -2502,15 +2540,13 @@ var HermesApplier = class {
|
|
|
2502
2540
|
const log$1 = createLogger("HermesMcpSync");
|
|
2503
2541
|
const DEFAULT_HERMES_HOME = join(homedir(), ".hermes");
|
|
2504
2542
|
/**
|
|
2505
|
-
*
|
|
2506
|
-
*
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2509
|
-
*
|
|
2510
|
-
*
|
|
2511
|
-
*
|
|
2512
|
-
* into each `mcp_servers.<id>.env` and skip the `.env` file entirely — change
|
|
2513
|
-
* `withAlfeApiKey()` + `ensureEnvApiKey()` together in that one case.
|
|
2543
|
+
* SEAM #1 — `${VAR}` interpolation from `~/.hermes/.env`. CONFIRMED (Hermes
|
|
2544
|
+
* source `config.py:5790` `_expand_env_vars`): Hermes loads `~/.hermes/.env` via
|
|
2545
|
+
* dotenv into `os.environ`, then recursively expands `${VAR}` refs across the
|
|
2546
|
+
* ENTIRE merged config — including `mcp_servers.<id>.env.*` (and `model.api_key`
|
|
2547
|
+
* / `model.base_url`). So injecting `ALFE_API_KEY=${ALFE_API_KEY}` into every
|
|
2548
|
+
* Alfe-owned stdio server's env and writing the real value to `~/.hermes/.env`
|
|
2549
|
+
* resolves at spawn time. The inline-literal fallback is therefore not needed.
|
|
2514
2550
|
*/
|
|
2515
2551
|
const ALFE_API_KEY_ENV_REF = "${ALFE_API_KEY}";
|
|
2516
2552
|
const DEFAULT_DEBOUNCE_MS = 250;
|
package/package.json
CHANGED