@alfe.ai/integrations 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -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
  }
@@ -697,6 +713,13 @@ declare class IntegrationManager {
697
713
  clear(): void;
698
714
  private checkHealth;
699
715
  private err;
716
+ /**
717
+ * Build a hook-failure message. A hook we SIGKILLed at its timeout is rendered
718
+ * as `(timed out after <ms>ms)` so it is visually distinguishable in Sentry
719
+ * from a genuine non-zero exit `(exit <code>)` — the two have very different
720
+ * root causes (Sentry AGENT-DAEMON-6).
721
+ */
722
+ private hookFailureMessage;
700
723
  /**
701
724
  * Compute the plugin (bare package) names and skill names still claimed by
702
725
  * SOME integration in the CURRENT lock state, keyed by runtime. Both
@@ -786,31 +809,16 @@ declare class StateManager {
786
809
  }
787
810
  //#endregion
788
811
  //#region src/hooks.d.ts
789
- /**
790
- * Hook Runner — executes integration lifecycle hook scripts.
791
- *
792
- * Hooks are scripts defined in the integration manifest. The runner
793
- * auto-detects the interpreter from the script's shebang line or file
794
- * extension (.js/.mjs → node, .py → python3, default → bash).
795
- *
796
- * Scripts run as child processes with a 30-second timeout.
797
- * stdout/stderr are captured and returned.
798
- *
799
- * The runner injects standard environment variables:
800
- * - ALFE_INTEGRATION_DIR — path to the integration install directory
801
- * - ALFE_STATE_DIR — path to the integration state directory (created if needed)
802
- * - ALFE_<NAME>_<KEY> — config values (non-secret AND secret, both injected as env vars)
803
- *
804
- * Secrets are acceptable as env vars because:
805
- * - Child process env is ephemeral (dies with the process)
806
- * - Same security model as Docker secrets / systemd credentials
807
- * - Never written to disk
808
- */
809
812
  interface HookResult {
810
813
  exitCode: number;
811
814
  stdout: string;
812
815
  stderr: string;
813
816
  timedOut: boolean;
817
+ /**
818
+ * When `timedOut` is true, the timeout ceiling (ms) that fired the SIGKILL.
819
+ * Lets callers render a timeout distinctly from a genuine non-zero exit.
820
+ */
821
+ timedOutAfterMs?: number;
814
822
  }
815
823
  interface HookEnvOptions {
816
824
  /** Integration name (e.g. "voice") */
@@ -857,9 +865,12 @@ declare function buildHookEnv(options: HookEnvOptions, additionalEnv?: Record<st
857
865
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
858
866
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
859
867
  * @param env - Additional environment variables to pass to the script
868
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
869
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
870
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
860
871
  * @returns Hook execution result
861
872
  */
862
- declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string>): Promise<HookResult>;
873
+ declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string>, timeoutMs?: number): Promise<HookResult>;
863
874
  /**
864
875
  * Run a hook script with full integration context (config + secrets as env vars).
865
876
  *
@@ -870,9 +881,12 @@ declare function runHook(integrationPath: string, hookScript: string, env?: Reco
870
881
  * @param integrationPath - Base path of the integration
871
882
  * @param hookScript - Relative path to the hook script
872
883
  * @param options - Integration name, config, and secrets
884
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
885
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
886
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
873
887
  * @returns Hook execution result
874
888
  */
875
- declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions): Promise<HookResult>;
889
+ declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions, timeoutMs?: number): Promise<HookResult>;
876
890
  //#endregion
877
891
  //#region src/openclaw-cli-lock.d.ts
878
892
  /**
@@ -1220,6 +1234,21 @@ declare class OpenClawApplier implements RuntimeApplier {
1220
1234
  * config writes trigger.
1221
1235
  */
1222
1236
  setConfigRaw(key: string, value: string): Promise<void>;
1237
+ /**
1238
+ * Read a single config key back — `openclaw config get <key>` — and normalize
1239
+ * to the scalar string that `setConfigRaw` would have written. `config get`
1240
+ * emits JSON, so a scalar comes back quoted (`"claude-sonnet-4-6"`); parse it
1241
+ * to the raw value so a `=== want` compare in the daemon's config-reconcile
1242
+ * pass is exact.
1243
+ *
1244
+ * Returns `undefined` when the key is unset, the command fails, or the runtime
1245
+ * REDACTS the value (a sensitive key we can't compare — treat as "unknown" so
1246
+ * the caller doesn't spuriously diff). Under the shared CLI lock so it can't
1247
+ * interleave with a concurrent mutation. NOT self-healing on a malformed state
1248
+ * DB — a read failure returns `undefined`, and the caller's set path (which IS
1249
+ * healing) recovers.
1250
+ */
1251
+ getConfigRaw(key: string): Promise<string | undefined>;
1223
1252
  isAvailable(): Promise<boolean>;
1224
1253
  private readTracking;
1225
1254
  private writeTracking;
@@ -1281,6 +1310,14 @@ declare class HermesApplier implements RuntimeApplier {
1281
1310
  * per-integration removal accounting). Reuses the serialized queue.
1282
1311
  */
1283
1312
  setConfigRaw(key: string, value: string): Promise<void>;
1313
+ /**
1314
+ * Read a single config key back — `hermes config get <key>` — normalized to
1315
+ * the scalar string `setConfigRaw` would have written (used by the daemon's
1316
+ * config-reconcile value-diff + verify, plan A4). Returns `undefined` when the
1317
+ * key is unset or the command fails. Serialized onto the same config queue so
1318
+ * it can't interleave with an in-flight `hermes config set`.
1319
+ */
1320
+ getConfigRaw(key: string): Promise<string | undefined>;
1284
1321
  /**
1285
1322
  * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
1286
1323
  * (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
package/dist/index.js CHANGED
@@ -746,7 +746,19 @@ var LockManager = class {
746
746
  * - Same security model as Docker secrets / systemd credentials
747
747
  * - Never written to disk
748
748
  */
749
+ /**
750
+ * Default timeout for lifecycle hooks (activate/health_check/uninstall). These
751
+ * are expected to be fast; a long-running one is a bug and should be killed.
752
+ */
749
753
  const HOOK_TIMEOUT_MS = 3e4;
754
+ /**
755
+ * Timeout for install-phase hooks (pre_install/post_install). These legitimately
756
+ * take minutes — they clone repos, run `npm install`, download binaries — so the
757
+ * fast 30s ceiling would SIGKILL a healthy install mid-flight (Sentry
758
+ * AGENT-DAEMON-6). Callers opt into this longer ceiling explicitly per hook type;
759
+ * it is NOT the global default so lifecycle hooks stay fast.
760
+ */
761
+ const INSTALL_HOOK_TIMEOUT_MS = 6e5;
750
762
  const INTEGRATIONS_BASE_DIR = join(homedir(), ".alfe", "integrations");
751
763
  const STATE_BASE_DIR = join(homedir(), ".alfe", "state");
752
764
  /**
@@ -848,9 +860,12 @@ function resolveInterpreter(scriptPath) {
848
860
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
849
861
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
850
862
  * @param env - Additional environment variables to pass to the script
863
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
864
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
865
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
851
866
  * @returns Hook execution result
852
867
  */
853
- async function runHook(integrationPath, hookScript, env) {
868
+ async function runHook(integrationPath, hookScript, env, timeoutMs = HOOK_TIMEOUT_MS) {
854
869
  const scriptPath = join(integrationPath, hookScript);
855
870
  if (!existsSync(scriptPath)) return {
856
871
  exitCode: 0,
@@ -878,7 +893,7 @@ async function runHook(integrationPath, hookScript, env) {
878
893
  const timer = setTimeout(() => {
879
894
  timedOut = true;
880
895
  proc.kill("SIGKILL");
881
- }, HOOK_TIMEOUT_MS);
896
+ }, timeoutMs);
882
897
  proc.stdout.on("data", (data) => {
883
898
  stdout += data.toString();
884
899
  if (stdout.length > 1e5) stdout = stdout.slice(0, 1e5) + "\n[truncated]";
@@ -893,7 +908,8 @@ async function runHook(integrationPath, hookScript, env) {
893
908
  exitCode: code ?? 1,
894
909
  stdout: stdout.trim(),
895
910
  stderr: stderr.trim(),
896
- timedOut
911
+ timedOut,
912
+ timedOutAfterMs: timedOut ? timeoutMs : void 0
897
913
  });
898
914
  });
899
915
  proc.on("error", (err) => {
@@ -917,10 +933,13 @@ async function runHook(integrationPath, hookScript, env) {
917
933
  * @param integrationPath - Base path of the integration
918
934
  * @param hookScript - Relative path to the hook script
919
935
  * @param options - Integration name, config, and secrets
936
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
937
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
938
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
920
939
  * @returns Hook execution result
921
940
  */
922
- async function runHookWithContext(integrationPath, hookScript, options) {
923
- return runHook(integrationPath, hookScript, buildHookEnv(options));
941
+ async function runHookWithContext(integrationPath, hookScript, options, timeoutMs = HOOK_TIMEOUT_MS) {
942
+ return runHook(integrationPath, hookScript, buildHookEnv(options), timeoutMs);
924
943
  }
925
944
  //#endregion
926
945
  //#region src/plugin-spec.ts
@@ -1127,8 +1146,8 @@ var IntegrationManager = class {
1127
1146
  if (!installHooksSupported && (manifest.hooks.pre_install || manifest.hooks.post_install)) this.log.warn(`Integration "${name}" install hooks skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(manifest.supported_agents ?? []).join(", ")})`);
1128
1147
  if (installHooksSupported && manifest.hooks.pre_install) {
1129
1148
  this.log.info(`Running pre_install hook: ${manifest.hooks.pre_install}`);
1130
- const hookResult = await runHook(installPath, manifest.hooks.pre_install);
1131
- if (hookResult.exitCode !== 0) throw new Error(`pre_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1149
+ const hookResult = await runHook(installPath, manifest.hooks.pre_install, void 0, INSTALL_HOOK_TIMEOUT_MS);
1150
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("pre_install hook failed", hookResult));
1132
1151
  }
1133
1152
  if (installHooksSupported && manifest.hooks.post_install) {
1134
1153
  this.log.info(`Running post_install hook: ${manifest.hooks.post_install}`);
@@ -1137,8 +1156,8 @@ var IntegrationManager = class {
1137
1156
  config: config ?? {},
1138
1157
  secrets: this.secrets.get(name),
1139
1158
  runtimes: [...this.runtimeAppliers.keys()]
1140
- });
1141
- if (hookResult.exitCode !== 0) throw new Error(`post_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1159
+ }, INSTALL_HOOK_TIMEOUT_MS);
1160
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("post_install hook failed", hookResult));
1142
1161
  }
1143
1162
  this.state.set(name, {
1144
1163
  status: "installed",
@@ -1328,8 +1347,9 @@ var IntegrationManager = class {
1328
1347
  runtimes: [...this.runtimeAppliers.keys()]
1329
1348
  });
1330
1349
  if (hookResult.exitCode !== 0) {
1331
- this.state.setStatus(integrationId, "error", `post_activate hook failed: ${hookResult.stderr || hookResult.stdout}`);
1332
- return this.err("POST_ACTIVATE_FAILED", `post_activate hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1350
+ const message = this.hookFailureMessage("post_activate hook failed", hookResult);
1351
+ this.state.setStatus(integrationId, "error", message);
1352
+ return this.err("POST_ACTIVATE_FAILED", message);
1333
1353
  }
1334
1354
  }
1335
1355
  if (manifest.hooks.health_check && !runtimeSupported) this.log.warn(`Integration "${integrationId}" health_check hook skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(supportedAgents ?? []).join(", ")})`);
@@ -1342,8 +1362,9 @@ var IntegrationManager = class {
1342
1362
  runtimes: [...this.runtimeAppliers.keys()]
1343
1363
  });
1344
1364
  if (hookResult.exitCode !== 0) {
1345
- this.state.setStatus(integrationId, "error", `Health check failed: ${hookResult.stderr || hookResult.stdout}`);
1346
- return this.err("HEALTH_CHECK_FAILED", `Health check failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1365
+ const message = this.hookFailureMessage("Health check failed", hookResult);
1366
+ this.state.setStatus(integrationId, "error", message);
1367
+ return this.err("HEALTH_CHECK_FAILED", message);
1347
1368
  }
1348
1369
  }
1349
1370
  this.log.info(`Integration "${integrationId}" activated`);
@@ -1485,8 +1506,8 @@ var IntegrationManager = class {
1485
1506
  config: entry.config,
1486
1507
  secrets: this.secrets.get(name),
1487
1508
  runtimes: [...this.runtimeAppliers.keys()]
1488
- });
1489
- if (hookResult.exitCode !== 0) this.log.warn(`pre_uninstall hook failed (continuing): ${hookResult.stderr}`);
1509
+ }, INSTALL_HOOK_TIMEOUT_MS);
1510
+ if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("pre_uninstall hook failed (continuing)", hookResult));
1490
1511
  }
1491
1512
  if (uninstallHooksSupported && manifest?.hooks.post_uninstall) {
1492
1513
  this.log.info(`Running post_uninstall hook: ${manifest.hooks.post_uninstall}`);
@@ -1495,8 +1516,8 @@ var IntegrationManager = class {
1495
1516
  config: entry.config,
1496
1517
  secrets: this.secrets.get(name),
1497
1518
  runtimes: [...this.runtimeAppliers.keys()]
1498
- });
1499
- if (hookResult.exitCode !== 0) this.log.warn(`post_uninstall hook failed (non-fatal): ${hookResult.stderr}`);
1519
+ }, INSTALL_HOOK_TIMEOUT_MS);
1520
+ if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("post_uninstall hook failed (non-fatal)", hookResult));
1500
1521
  }
1501
1522
  if (this.installer.isInstalled(name)) {
1502
1523
  await this.installer.remove(name);
@@ -1647,8 +1668,8 @@ var IntegrationManager = class {
1647
1668
  if (!installHooksSupported && (newManifest.hooks.pre_install || newManifest.hooks.post_install)) this.log.warn(`Integration "${name}" upgrade install hooks skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(newManifest.supported_agents ?? []).join(", ")})`);
1648
1669
  if (installHooksSupported && newManifest.hooks.pre_install) {
1649
1670
  this.log.info(`Running pre_install hook: ${newManifest.hooks.pre_install}`);
1650
- const hookResult = await runHook(this.installer.getInstallPath(name), newManifest.hooks.pre_install);
1651
- if (hookResult.exitCode !== 0) throw new Error(`pre_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1671
+ const hookResult = await runHook(this.installer.getInstallPath(name), newManifest.hooks.pre_install, void 0, INSTALL_HOOK_TIMEOUT_MS);
1672
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("pre_install hook failed", hookResult));
1652
1673
  }
1653
1674
  if (installHooksSupported && newManifest.hooks.post_install) {
1654
1675
  this.log.info(`Running post_install hook: ${newManifest.hooks.post_install}`);
@@ -1657,8 +1678,8 @@ var IntegrationManager = class {
1657
1678
  config: config ?? existing.config,
1658
1679
  secrets: this.secrets.get(name),
1659
1680
  runtimes: [...this.runtimeAppliers.keys()]
1660
- });
1661
- if (hookResult.exitCode !== 0) throw new Error(`post_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1681
+ }, INSTALL_HOOK_TIMEOUT_MS);
1682
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("post_install hook failed", hookResult));
1662
1683
  }
1663
1684
  await this.applyUpgradeDiffRemovals(name, oldManifest, newManifest);
1664
1685
  return await this.activate(name, { forcePlugins: true });
@@ -1837,7 +1858,7 @@ var IntegrationManager = class {
1837
1858
  healthy: hookResult.exitCode === 0,
1838
1859
  status: entry.status,
1839
1860
  version: manifest.version,
1840
- message: hookResult.exitCode === 0 ? "Healthy" : `Health check failed (exit ${String(hookResult.exitCode)})`,
1861
+ message: hookResult.exitCode === 0 ? "Healthy" : hookResult.timedOut ? `Health check failed (timed out after ${String(hookResult.timedOutAfterMs ?? "?")}ms)` : `Health check failed (exit ${String(hookResult.exitCode)})`,
1841
1862
  stdout: hookResult.stdout || void 0,
1842
1863
  stderr: hookResult.stderr || void 0
1843
1864
  };
@@ -1861,6 +1882,17 @@ var IntegrationManager = class {
1861
1882
  };
1862
1883
  }
1863
1884
  /**
1885
+ * Build a hook-failure message. A hook we SIGKILLed at its timeout is rendered
1886
+ * as `(timed out after <ms>ms)` so it is visually distinguishable in Sentry
1887
+ * from a genuine non-zero exit `(exit <code>)` — the two have very different
1888
+ * root causes (Sentry AGENT-DAEMON-6).
1889
+ */
1890
+ hookFailureMessage(label, hookResult) {
1891
+ const reason = hookResult.timedOut ? `timed out after ${String(hookResult.timedOutAfterMs ?? "?")}ms` : `exit ${String(hookResult.exitCode)}`;
1892
+ const output = hookResult.stderr || hookResult.stdout;
1893
+ return output ? `${label} (${reason}): ${output}` : `${label} (${reason})`;
1894
+ }
1895
+ /**
1864
1896
  * Compute the plugin (bare package) names and skill names still claimed by
1865
1897
  * SOME integration in the CURRENT lock state, keyed by runtime. Both
1866
1898
  * `deactivate` and `upgrade`'s diff-removal call this AFTER
@@ -2948,6 +2980,46 @@ var OpenClawApplier = class {
2948
2980
  setConfigRaw(key, value) {
2949
2981
  return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
2950
2982
  }
2983
+ /**
2984
+ * Read a single config key back — `openclaw config get <key>` — and normalize
2985
+ * to the scalar string that `setConfigRaw` would have written. `config get`
2986
+ * emits JSON, so a scalar comes back quoted (`"claude-sonnet-4-6"`); parse it
2987
+ * to the raw value so a `=== want` compare in the daemon's config-reconcile
2988
+ * pass is exact.
2989
+ *
2990
+ * Returns `undefined` when the key is unset, the command fails, or the runtime
2991
+ * REDACTS the value (a sensitive key we can't compare — treat as "unknown" so
2992
+ * the caller doesn't spuriously diff). Under the shared CLI lock so it can't
2993
+ * interleave with a concurrent mutation. NOT self-healing on a malformed state
2994
+ * DB — a read failure returns `undefined`, and the caller's set path (which IS
2995
+ * healing) recovers.
2996
+ */
2997
+ getConfigRaw(key) {
2998
+ return this.cliLock.run(async () => {
2999
+ try {
3000
+ const { stdout } = await execFileAsync$1("openclaw", [
3001
+ "config",
3002
+ "get",
3003
+ key
3004
+ ], { timeout: 1e4 });
3005
+ const trimmed = stdout.trim();
3006
+ if (!trimmed) return void 0;
3007
+ let parsed;
3008
+ try {
3009
+ parsed = JSON.parse(trimmed);
3010
+ } catch {
3011
+ return trimmed;
3012
+ }
3013
+ if (parsed === null || parsed === void 0) return void 0;
3014
+ if (parsed === OPENCLAW_REDACTED) return void 0;
3015
+ if (typeof parsed === "string") return parsed;
3016
+ if (typeof parsed === "number" || typeof parsed === "boolean") return String(parsed);
3017
+ return JSON.stringify(parsed);
3018
+ } catch {
3019
+ return;
3020
+ }
3021
+ });
3022
+ }
2951
3023
  isAvailable() {
2952
3024
  return Promise.resolve(existsSync(this.home));
2953
3025
  }
@@ -3153,6 +3225,40 @@ var HermesApplier = class {
3153
3225
  return this.runConfigSet([key, value]);
3154
3226
  }
3155
3227
  /**
3228
+ * Read a single config key back — `hermes config get <key>` — normalized to
3229
+ * the scalar string `setConfigRaw` would have written (used by the daemon's
3230
+ * config-reconcile value-diff + verify, plan A4). Returns `undefined` when the
3231
+ * key is unset or the command fails. Serialized onto the same config queue so
3232
+ * it can't interleave with an in-flight `hermes config set`.
3233
+ */
3234
+ getConfigRaw(key) {
3235
+ const run = async () => {
3236
+ try {
3237
+ const { stdout } = await execFileAsync("hermes", [
3238
+ "config",
3239
+ "get",
3240
+ key
3241
+ ], { timeout: 1e4 });
3242
+ const trimmed = stdout.trim();
3243
+ if (!trimmed) return void 0;
3244
+ try {
3245
+ const parsed = JSON.parse(trimmed);
3246
+ if (parsed === null || parsed === void 0) return void 0;
3247
+ if (typeof parsed === "string") return parsed;
3248
+ if (typeof parsed === "number" || typeof parsed === "boolean") return String(parsed);
3249
+ return JSON.stringify(parsed);
3250
+ } catch {
3251
+ return trimmed;
3252
+ }
3253
+ } catch {
3254
+ return;
3255
+ }
3256
+ };
3257
+ const result = this.configSetQueue.then(run, run);
3258
+ this.configSetQueue = result.catch(() => void 0);
3259
+ return result;
3260
+ }
3261
+ /**
3156
3262
  * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
3157
3263
  * (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
3158
3264
  * them. The manager catches per-plugin, so a skip here is a correct no-op.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",