@alfe.ai/integrations 0.1.5 → 0.2.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +314 -4
  2. package/dist/index.js +742 -104
  3. package/package.json +3 -2
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IntegrationManifest, IntegrationStateEntry, IntegrationStatus, IntegrationsStateFile, McpServerDeclaration, PluginInstall, SkillInstall } from "@alfe.ai/integration-manifest";
2
- import { Manager } from "@alfe.ai/mcp-bundler";
2
+ import { Manager, StoredServerEntry } from "@alfe.ai/mcp-bundler";
3
3
 
4
4
  //#region src/registry.d.ts
5
5
 
@@ -287,6 +287,13 @@ interface RuntimeApplier {
287
287
  applyPlugin(pkg: string, integrationInstallPath: string, opts?: {
288
288
  force?: boolean;
289
289
  }): Promise<void>;
290
+ /**
291
+ * Optional: pre-trust an integration's full plugin set in one write before
292
+ * the per-plugin install loop. Runtimes whose config writes trigger a reload
293
+ * (OpenClaw) implement this to collapse N allowlist writes into one. Absent
294
+ * → the manager relies on `applyPlugin` to allowlist each plugin itself.
295
+ */
296
+ ensurePluginsAllowed?(pkgs: string[]): Promise<void>;
290
297
  /** Remove a plugin package from this runtime */
291
298
  removePlugin(pkg: string): Promise<void>;
292
299
  /** Copy a skill directory into this runtime's skills location */
@@ -301,6 +308,18 @@ interface RuntimeApplier {
301
308
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
302
309
  /** Remove config previously applied by an integration */
303
310
  removeConfig(integrationId: string): Promise<void>;
311
+ /**
312
+ * Optional: write a single raw config key/value directly to the runtime,
313
+ * bypassing the per-integration removal accounting that `applyConfig`
314
+ * maintains. Used by the daemon's `alfe.config_set` cloud-command so it can
315
+ * route per-runtime instead of shelling the runtime CLI directly.
316
+ *
317
+ * MUST NOT funnel through `applyConfig` — a raw single-key set is untracked
318
+ * and must not corrupt an integration's tracked config contribution.
319
+ * Appliers that can't honour a raw write omit this method; callers detect
320
+ * its absence and degrade gracefully.
321
+ */
322
+ setConfigRaw?(key: string, value: string): Promise<void>;
304
323
  /** Check if this runtime is available (e.g. workspace directory exists) */
305
324
  isAvailable(): Promise<boolean>;
306
325
  }
@@ -748,6 +767,10 @@ interface OpenClawApplierOptions {
748
767
  skillsDir?: string;
749
768
  /** Path to the integration tracking file (defaults to {home}/config.json) */
750
769
  configPath?: string;
770
+ /** Max retries for a transient `openclaw config set` failure (default 3) */
771
+ configSetRetries?: number;
772
+ /** Backoff between `openclaw config set` retries, ms (default 750; set 0 in tests) */
773
+ configSetRetryDelayMs?: number;
751
774
  }
752
775
  declare class OpenClawApplier implements RuntimeApplier {
753
776
  readonly runtime = "openclaw";
@@ -755,15 +778,44 @@ declare class OpenClawApplier implements RuntimeApplier {
755
778
  private agentWorkspace;
756
779
  private skillsDir;
757
780
  private trackingPath;
781
+ private configSetRetries;
782
+ private configSetRetryDelayMs;
783
+ /** Serializes all `openclaw config set` writes so they never interleave with each other or the hot-reload they trigger. */
784
+ private configSetQueue;
758
785
  constructor(options: OpenClawApplierOptions);
786
+ /** Convenience: `openclaw config set <args>`, serialized + retried. */
787
+ private runConfigSet;
788
+ /**
789
+ * Run `openclaw config <args>` (set/unset), serialized against every other
790
+ * config write and retried with backoff. See CONFIG_SET_RETRIES for why:
791
+ * each write triggers a runtime hot-reload that rewrites openclaw.json, and a
792
+ * follow-up command that races the reload fails with a bare "Command failed".
793
+ *
794
+ * Throws an Error whose (scrubbed) message includes stderr after retries are
795
+ * exhausted, so the real cause propagates to the integration errorMessage
796
+ * without leaking the value payload.
797
+ */
798
+ private runConfigCommand;
759
799
  applyPlugin(spec: string, _installPath?: string, opts?: {
760
800
  force?: boolean;
761
801
  }): Promise<void>;
762
802
  /**
763
- * Ensure the plugin is in plugins.allow in openclaw.json.
803
+ * Ensure one or more plugins are in plugins.allow in openclaw.json.
764
804
  * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
805
+ *
806
+ * Prefer passing the FULL set of plugins for an integration in a single call
807
+ * (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
808
+ * one write for N plugins is one reload instead of N.
765
809
  */
766
810
  private ensurePluginsAllow;
811
+ /**
812
+ * Pre-trust every plugin an integration ships in a SINGLE plugins.allow
813
+ * write, before any are installed. Called once by the manager ahead of the
814
+ * per-plugin install loop so activation triggers one hot-reload for the
815
+ * allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
816
+ * `ensurePluginsAllow` then finds nothing missing and is a no-op.
817
+ */
818
+ ensurePluginsAllowed(specs: string[]): Promise<void>;
767
819
  /**
768
820
  * Check if a plugin is already installed.
769
821
  *
@@ -802,11 +854,238 @@ declare class OpenClawApplier implements RuntimeApplier {
802
854
  * then removes them via `openclaw config unset`.
803
855
  */
804
856
  removeConfig(integrationId: string): Promise<void>;
857
+ /**
858
+ * Raw single-key config write — `openclaw config set <key> <value>`.
859
+ *
860
+ * Deliberately bypasses the `_integrations` tracking that `applyConfig`
861
+ * maintains: this is a fire-and-forget write for the `alfe.config_set`
862
+ * cloud-command, not an integration contribution, so it must NOT be
863
+ * recorded for later `removeConfig` teardown. Reuses the serialized +
864
+ * retried queue so it can't interleave with the hot-reload that integration
865
+ * config writes trigger.
866
+ */
867
+ setConfigRaw(key: string, value: string): Promise<void>;
868
+ isAvailable(): Promise<boolean>;
869
+ private readTracking;
870
+ private writeTracking;
871
+ }
872
+ //#endregion
873
+ //#region src/appliers/hermes-applier.d.ts
874
+ interface HermesApplierOptions {
875
+ /**
876
+ * Path to the Hermes home directory (e.g. ~/.hermes) — where config.yaml,
877
+ * .env, and plugins/ live. For Hermes, home == workspace.
878
+ */
879
+ home?: string;
880
+ /**
881
+ * @deprecated Use `home`. Kept for parity with OpenClawApplier's option shape —
882
+ * when set, used as `home`.
883
+ */
884
+ workspace?: string;
885
+ /** Path to the per-integration tracking file (defaults to {home}/.alfe-integrations.json) */
886
+ configPath?: string;
887
+ /** Max retries for a transient `hermes config set/unset` failure (default 3) */
888
+ configSetRetries?: number;
889
+ /** Backoff between `hermes config` retries, ms (default 750; set 0 in tests) */
890
+ configSetRetryDelayMs?: number;
891
+ }
892
+ declare class HermesApplier implements RuntimeApplier {
893
+ readonly runtime = "hermes";
894
+ private home;
895
+ private trackingPath;
896
+ private configSetRetries;
897
+ private configSetRetryDelayMs;
898
+ /** Serializes all `hermes config` writes so they never interleave. */
899
+ private configSetQueue;
900
+ constructor(options?: HermesApplierOptions);
901
+ /**
902
+ * Apply integration config to the Hermes runtime via `hermes config set`.
903
+ *
904
+ * Each leaf is applied as `hermes config set <dotted.key> <value>`. Each
905
+ * integration's config contribution is tracked in the tracking file so it can
906
+ * be cleanly removed later (mirrors OpenClawApplier's `_integrations`).
907
+ */
908
+ applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
909
+ /**
910
+ * Remove config previously applied by an integration.
911
+ *
912
+ * Reads the tracking file to find which keys this integration set, then
913
+ * removes each via `hermes config unset`. Clears the tracking entry.
914
+ */
915
+ removeConfig(integrationId: string): Promise<void>;
916
+ /**
917
+ * Raw single-key config write — `hermes config set <key> <value>`.
918
+ *
919
+ * Deliberately bypasses the `_integrations` tracking that `applyConfig`
920
+ * maintains: this is a fire-and-forget write for the `alfe.config_set`
921
+ * cloud-command, NOT an integration contribution, so it must not be recorded
922
+ * for later `removeConfig` teardown (doing so would corrupt the
923
+ * per-integration removal accounting). Reuses the serialized queue.
924
+ */
925
+ setConfigRaw(key: string, value: string): Promise<void>;
926
+ /**
927
+ * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
928
+ * (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
929
+ * them. The manager catches per-plugin, so a skip here is a correct no-op.
930
+ *
931
+ * A genuine Hermes-native plugin spec (git URL / pip / local dir) would
932
+ * `hermes plugins install` then `hermes plugins enable`; no manifest declares
933
+ * one in Phase 1, so this path is a real attempt (not faked) but untrodden.
934
+ */
935
+ applyPlugin(spec: string): Promise<void>;
936
+ /**
937
+ * Remove a plugin. OpenClaw-specific npm plugins were never installed into
938
+ * Hermes (applyPlugin skipped them), so removal is a no-op log. A native
939
+ * Hermes plugin would be disabled via `hermes plugins`.
940
+ */
941
+ removePlugin(spec: string): Promise<void>;
942
+ applySkill(name: string): Promise<void>;
943
+ removeSkill(name: string): Promise<void>;
944
+ applyClawHubSkill(slug: string): Promise<void>;
945
+ removeClawHubSkill(slug: string): Promise<void>;
805
946
  isAvailable(): Promise<boolean>;
947
+ /** Convenience: `hermes config set <args>`, serialized + retried. */
948
+ private runConfigSet;
949
+ /**
950
+ * Unset a single config key. The unset verb is isolated HERE so there is one
951
+ * place to change if the spike proves `hermes config unset` is unavailable.
952
+ *
953
+ * FALLBACK (do NOT pre-build): if `hermes config unset` does not exist, the
954
+ * substitute is `hermes config set <key> ""` (clear the value) or a
955
+ * read-merge-write of config.yaml. Not implemented now — `unset` is the
956
+ * documented verb; confirm in the Phase-0 spike before adding a fallback.
957
+ * TODO(phase-0 spike): confirm `hermes config unset <key>` exists.
958
+ */
959
+ private unsetConfigKey;
960
+ /**
961
+ * Run `hermes config <args>` (set/unset), serialized against every other
962
+ * config write and retried with backoff. Throws an Error whose (scrubbed)
963
+ * message includes stderr after retries are exhausted, so the real cause
964
+ * propagates to the integration errorMessage without leaking the value.
965
+ */
966
+ private runConfigCommand;
806
967
  private readTracking;
807
968
  private writeTracking;
808
969
  }
809
970
  //#endregion
971
+ //#region src/appliers/hermes-mcp-sync.d.ts
972
+ /**
973
+ * The slice of the bundler `Manager` this consumer needs. Kept structural (not a
974
+ * concrete-class dependency) so it stays trivially testable and so the consumer
975
+ * can never reach for producer-side mutation methods — it is read-only over the
976
+ * store plus a change subscription.
977
+ */
978
+ interface McpStoreReader {
979
+ onChange(cb: () => void): () => void;
980
+ listServers(): {
981
+ id: string;
982
+ entry: StoredServerEntry;
983
+ }[];
984
+ }
985
+ interface HermesMcpSyncOptions {
986
+ /** The runtime-agnostic MCP store manager (read + onChange). */
987
+ manager: McpStoreReader;
988
+ /** Hermes home (== workspace == ~/.hermes). config.yaml + .env live here. */
989
+ home?: string;
990
+ /**
991
+ * The `ALFE_API_KEY` value to write into `~/.hermes/.env` so the
992
+ * `${ALFE_API_KEY}` refs in mirrored server envs resolve. In managed mode this
993
+ * is the daemon's own api key; self-hosted threads it explicitly. When absent,
994
+ * the `.env` write is skipped and a warning is logged (the servers would then
995
+ * start in zero-accounts degraded mode).
996
+ */
997
+ apiKey?: string;
998
+ /**
999
+ * Trigger a runtime reload after a real change. MUST be the SAME callback the
1000
+ * daemon wires into `setRuntimeRestartNeededHandler` — there is exactly one
1001
+ * restart path. No-op when the runtime isn't running yet (the initial sync
1002
+ * writes config.yaml before Hermes starts, so no restart is needed then).
1003
+ */
1004
+ requestRestart?: () => void;
1005
+ /** Override config.yaml path (defaults to {home}/config.yaml). For tests. */
1006
+ configPath?: string;
1007
+ /** Override .env path (defaults to {home}/.env). For tests. */
1008
+ envPath?: string;
1009
+ /**
1010
+ * Override the sidecar that records which `mcp_servers` ids this sync wrote
1011
+ * (defaults to {home}/.alfe-mcp-synced.json). Used so removals survive daemon
1012
+ * restarts and so we NEVER delete user-authored `mcp_servers` entries.
1013
+ */
1014
+ trackingPath?: string;
1015
+ /**
1016
+ * Trailing debounce (ms) used to coalesce a burst of store mutations (one
1017
+ * integration install fires `addServer` once per server) into a single
1018
+ * config.yaml write + restart, avoiding a restart storm. Default 250; set 0 in
1019
+ * tests. The initial sync in `start()` is always immediate.
1020
+ */
1021
+ debounceMs?: number;
1022
+ }
1023
+ /**
1024
+ * Read-merge-write mirror of the MCP store into `~/.hermes/config.yaml`.
1025
+ *
1026
+ * Ownership / removal model: every entry returned by `manager.listServers()`
1027
+ * comes from the Alfe store (its `owner` is `integration:<id>` / `cli` /
1028
+ * `manual`) and is therefore Alfe-owned — these are the ids we write. User
1029
+ * authored `mcp_servers` entries live ONLY in config.yaml and never appear in
1030
+ * the store, so we never touch them. To know which config.yaml ids to REMOVE
1031
+ * when a server leaves the store, we persist the set of ids we last wrote to a
1032
+ * sidecar (`.alfe-mcp-synced.json`) and only ever delete from that set — exactly
1033
+ * how the bundler store tracks `_ownedOpenclawKeys` for its openclaw.json mirror.
1034
+ */
1035
+ declare class HermesMcpSync {
1036
+ private readonly manager;
1037
+ private readonly home;
1038
+ private readonly apiKey?;
1039
+ private readonly requestRestart?;
1040
+ private readonly configPath;
1041
+ private readonly envPath;
1042
+ private readonly trackingPath;
1043
+ private readonly debounceMs;
1044
+ /** Ids of `mcp_servers` entries this sync last wrote — the removal set. */
1045
+ private syncedIds;
1046
+ /** Serializes syncs so two onChange-driven runs can't interleave file writes. */
1047
+ private queue;
1048
+ private unsubscribe?;
1049
+ private debounceTimer?;
1050
+ private started;
1051
+ constructor(opts: HermesMcpSyncOptions);
1052
+ /**
1053
+ * Begin mirroring: load the prior removal set, run one immediate sync (so
1054
+ * config.yaml reflects the current store before Hermes first starts), then
1055
+ * subscribe to store changes (debounced). Idempotent.
1056
+ */
1057
+ start(): void;
1058
+ /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
1059
+ stop(): void;
1060
+ /**
1061
+ * Mirror the current store into config.yaml + .env exactly once. Public so the
1062
+ * daemon (and tests) can await a deterministic sync. Serialized against any
1063
+ * other in-flight sync.
1064
+ */
1065
+ syncOnce(): Promise<void>;
1066
+ private schedule;
1067
+ private syncNow;
1068
+ private computeDesired;
1069
+ /**
1070
+ * Transform a stored entry into a Hermes `mcp_servers` entry. SPIKE-PENDING
1071
+ * SEAM #3 lives here — the schema shape. stdio entries get `ALFE_API_KEY`
1072
+ * injected; remote entries pass through (Hermes supports `url`-based MCP).
1073
+ */
1074
+ private toHermesEntry;
1075
+ private withAlfeApiKey;
1076
+ /** Read-merge-write `~/.hermes/.env` to set ALFE_API_KEY without clobbering other keys. */
1077
+ private ensureEnvApiKey;
1078
+ private loadSyncedIds;
1079
+ private persistSyncedIds;
1080
+ }
1081
+ /**
1082
+ * Set `KEY=value` in a `.env`-style file, preserving every other line (comments,
1083
+ * blank lines, unrelated keys) and order. Returns whether the file changed. Not
1084
+ * a full dotenv parser — it only matches simple `KEY=` lines, which is all
1085
+ * `~/.hermes/.env` ever holds.
1086
+ */
1087
+ declare function upsertEnvVar(envPath: string, key: string, value: string): boolean;
1088
+ //#endregion
810
1089
  //#region src/lock.d.ts
811
1090
  interface RuntimePluginEntry {
812
1091
  /**
@@ -826,9 +1105,29 @@ interface RuntimeSkillEntry {
826
1105
  sourceIntegration: string;
827
1106
  integrationVersion: string;
828
1107
  }
1108
+ /**
1109
+ * Records that an integration applied runtime *config* (via the applier's
1110
+ * `applyConfig`) to a runtime — even when it contributed no plugins/skills.
1111
+ *
1112
+ * Without this, a config-only integration left no lock entry for its runtime,
1113
+ * so `removeEntries` returned nothing for it and `deactivate` never drove
1114
+ * `applier.removeConfig(...)` — leaking the applied config on removal. The
1115
+ * applier itself owns the actual config payload (e.g. OpenClaw's tracking
1116
+ * file); the lock only needs to know which integration touched which runtime.
1117
+ */
1118
+ interface RuntimeConfigEntry {
1119
+ sourceIntegration: string;
1120
+ integrationVersion: string;
1121
+ }
829
1122
  interface RuntimeDesiredState {
830
1123
  plugins: RuntimePluginEntry[];
831
1124
  skills: RuntimeSkillEntry[];
1125
+ /**
1126
+ * Integrations that applied config to this runtime. Optional for
1127
+ * back-compat with lock files written before this field existed — always
1128
+ * read it as `state.config ?? []`.
1129
+ */
1130
+ config?: RuntimeConfigEntry[];
832
1131
  }
833
1132
  interface RuntimeLockFile {
834
1133
  version: 1;
@@ -848,15 +1147,26 @@ declare class LockManager {
848
1147
  write(lock: RuntimeLockFile): void;
849
1148
  /**
850
1149
  * Add entries for an integration activation in a specific runtime.
1150
+ *
1151
+ * Pass `opts.configApplied` when the integration applied runtime config so
1152
+ * the contribution is recorded even if it ships no plugins/skills — this is
1153
+ * what lets a config-only integration be torn down on deactivate.
851
1154
  */
852
- addEntries(runtime: string, integrationId: string, version: string, plugins: PluginInstall[], skills: SkillInstall[], installPath: string): void;
1155
+ addEntries(runtime: string, integrationId: string, version: string, plugins: PluginInstall[], skills: SkillInstall[], installPath: string, opts?: {
1156
+ configApplied?: boolean;
1157
+ }): void;
853
1158
  /**
854
1159
  * Remove all entries for a given integration across all runtimes.
855
1160
  * Returns what was removed, keyed by runtime.
1161
+ *
1162
+ * A runtime is included in the result when the integration contributed
1163
+ * plugins, skills, OR config there — so `deactivate` drives
1164
+ * `applier.removeConfig` even for a config-only integration.
856
1165
  */
857
1166
  removeEntries(integrationId: string): Record<string, {
858
1167
  plugins: RuntimePluginEntry[];
859
1168
  skills: RuntimeSkillEntry[];
1169
+ config: RuntimeConfigEntry[];
860
1170
  }>;
861
1171
  /**
862
1172
  * Get the full desired state for a specific runtime.
@@ -924,4 +1234,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
924
1234
  resetReinstallAttempts(integrationId: string): void;
925
1235
  }
926
1236
  //#endregion
927
- export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, 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, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
1237
+ 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 };