@alfe.ai/integrations 0.1.6 → 0.2.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 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
 
@@ -308,6 +308,18 @@ interface RuntimeApplier {
308
308
  applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
309
309
  /** Remove config previously applied by an integration */
310
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>;
311
323
  /** Check if this runtime is available (e.g. workspace directory exists) */
312
324
  isAvailable(): Promise<boolean>;
313
325
  }
@@ -784,6 +796,26 @@ declare class OpenClawApplier implements RuntimeApplier {
784
796
  * without leaking the value payload.
785
797
  */
786
798
  private runConfigCommand;
799
+ /**
800
+ * Read back a single config path and compare it to the value we tried to
801
+ * write. Used by `applyConfig`'s verify-after-write: a `config set` can exit
802
+ * non-zero (a hot-reload races the write, OpenClaw's clobber protection
803
+ * fires) even though the value actually landed, exactly like `applyPlugin`'s
804
+ * install-then-check tolerance.
805
+ *
806
+ * OpenClaw REDACTS sensitive values on `config get`, returning the literal
807
+ * `__OPENCLAW_REDACTED__` instead of the real value. Treat that as a match:
808
+ * the key exists and OpenClaw is hiding it, so a deep-equal against the
809
+ * intended value would otherwise be a false negative and force a re-throw.
810
+ */
811
+ private configValueMatches;
812
+ /**
813
+ * Run a read-back `check` with the same retry/backoff cadence as the config
814
+ * writes — the settling hot-reload may still be rewriting openclaw.json when
815
+ * we first read back, so a single check can be a false negative. Returns true
816
+ * on the first success, false once all attempts are exhausted.
817
+ */
818
+ private verifyApplied;
787
819
  applyPlugin(spec: string, _installPath?: string, opts?: {
788
820
  force?: boolean;
789
821
  }): Promise<void>;
@@ -842,11 +874,238 @@ declare class OpenClawApplier implements RuntimeApplier {
842
874
  * then removes them via `openclaw config unset`.
843
875
  */
844
876
  removeConfig(integrationId: string): Promise<void>;
877
+ /**
878
+ * Raw single-key config write — `openclaw config set <key> <value>`.
879
+ *
880
+ * Deliberately bypasses the `_integrations` tracking that `applyConfig`
881
+ * maintains: this is a fire-and-forget write for the `alfe.config_set`
882
+ * cloud-command, not an integration contribution, so it must NOT be
883
+ * recorded for later `removeConfig` teardown. Reuses the serialized +
884
+ * retried queue so it can't interleave with the hot-reload that integration
885
+ * config writes trigger.
886
+ */
887
+ setConfigRaw(key: string, value: string): Promise<void>;
888
+ isAvailable(): Promise<boolean>;
889
+ private readTracking;
890
+ private writeTracking;
891
+ }
892
+ //#endregion
893
+ //#region src/appliers/hermes-applier.d.ts
894
+ interface HermesApplierOptions {
895
+ /**
896
+ * Path to the Hermes home directory (e.g. ~/.hermes) — where config.yaml,
897
+ * .env, and plugins/ live. For Hermes, home == workspace.
898
+ */
899
+ home?: string;
900
+ /**
901
+ * @deprecated Use `home`. Kept for parity with OpenClawApplier's option shape —
902
+ * when set, used as `home`.
903
+ */
904
+ workspace?: string;
905
+ /** Path to the per-integration tracking file (defaults to {home}/.alfe-integrations.json) */
906
+ configPath?: string;
907
+ /** Max retries for a transient `hermes config set/unset` failure (default 3) */
908
+ configSetRetries?: number;
909
+ /** Backoff between `hermes config` retries, ms (default 750; set 0 in tests) */
910
+ configSetRetryDelayMs?: number;
911
+ }
912
+ declare class HermesApplier implements RuntimeApplier {
913
+ readonly runtime = "hermes";
914
+ private home;
915
+ private trackingPath;
916
+ private configSetRetries;
917
+ private configSetRetryDelayMs;
918
+ /** Serializes all `hermes config` writes so they never interleave. */
919
+ private configSetQueue;
920
+ constructor(options?: HermesApplierOptions);
921
+ /**
922
+ * Apply integration config to the Hermes runtime via `hermes config set`.
923
+ *
924
+ * Each leaf is applied as `hermes config set <dotted.key> <value>`. Each
925
+ * integration's config contribution is tracked in the tracking file so it can
926
+ * be cleanly removed later (mirrors OpenClawApplier's `_integrations`).
927
+ */
928
+ applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
929
+ /**
930
+ * Remove config previously applied by an integration.
931
+ *
932
+ * Reads the tracking file to find which keys this integration set, then
933
+ * removes each via `hermes config unset`. Clears the tracking entry.
934
+ */
935
+ removeConfig(integrationId: string): Promise<void>;
936
+ /**
937
+ * Raw single-key config write — `hermes config set <key> <value>`.
938
+ *
939
+ * Deliberately bypasses the `_integrations` tracking that `applyConfig`
940
+ * maintains: this is a fire-and-forget write for the `alfe.config_set`
941
+ * cloud-command, NOT an integration contribution, so it must not be recorded
942
+ * for later `removeConfig` teardown (doing so would corrupt the
943
+ * per-integration removal accounting). Reuses the serialized queue.
944
+ */
945
+ setConfigRaw(key: string, value: string): Promise<void>;
946
+ /**
947
+ * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
948
+ * (they carry the `openclaw` peer dep) and do NOT apply to Hermes — log + skip
949
+ * them. The manager catches per-plugin, so a skip here is a correct no-op.
950
+ *
951
+ * A genuine Hermes-native plugin spec (git URL / pip / local dir) would
952
+ * `hermes plugins install` then `hermes plugins enable`; no manifest declares
953
+ * one in Phase 1, so this path is a real attempt (not faked) but untrodden.
954
+ */
955
+ applyPlugin(spec: string): Promise<void>;
956
+ /**
957
+ * Remove a plugin. OpenClaw-specific npm plugins were never installed into
958
+ * Hermes (applyPlugin skipped them), so removal is a no-op log. A native
959
+ * Hermes plugin would be disabled via `hermes plugins`.
960
+ */
961
+ removePlugin(spec: string): Promise<void>;
962
+ applySkill(name: string): Promise<void>;
963
+ removeSkill(name: string): Promise<void>;
964
+ applyClawHubSkill(slug: string): Promise<void>;
965
+ removeClawHubSkill(slug: string): Promise<void>;
845
966
  isAvailable(): Promise<boolean>;
967
+ /** Convenience: `hermes config set <args>`, serialized + retried. */
968
+ private runConfigSet;
969
+ /**
970
+ * Unset a single config key. The unset verb is isolated HERE so there is one
971
+ * place to change if the spike proves `hermes config unset` is unavailable.
972
+ *
973
+ * FALLBACK (do NOT pre-build): if `hermes config unset` does not exist, the
974
+ * substitute is `hermes config set <key> ""` (clear the value) or a
975
+ * read-merge-write of config.yaml. Not implemented now — `unset` is the
976
+ * documented verb; confirm in the Phase-0 spike before adding a fallback.
977
+ * TODO(phase-0 spike): confirm `hermes config unset <key>` exists.
978
+ */
979
+ private unsetConfigKey;
980
+ /**
981
+ * Run `hermes config <args>` (set/unset), serialized against every other
982
+ * config write and retried with backoff. Throws an Error whose (scrubbed)
983
+ * message includes stderr after retries are exhausted, so the real cause
984
+ * propagates to the integration errorMessage without leaking the value.
985
+ */
986
+ private runConfigCommand;
846
987
  private readTracking;
847
988
  private writeTracking;
848
989
  }
849
990
  //#endregion
991
+ //#region src/appliers/hermes-mcp-sync.d.ts
992
+ /**
993
+ * The slice of the bundler `Manager` this consumer needs. Kept structural (not a
994
+ * concrete-class dependency) so it stays trivially testable and so the consumer
995
+ * can never reach for producer-side mutation methods — it is read-only over the
996
+ * store plus a change subscription.
997
+ */
998
+ interface McpStoreReader {
999
+ onChange(cb: () => void): () => void;
1000
+ listServers(): {
1001
+ id: string;
1002
+ entry: StoredServerEntry;
1003
+ }[];
1004
+ }
1005
+ interface HermesMcpSyncOptions {
1006
+ /** The runtime-agnostic MCP store manager (read + onChange). */
1007
+ manager: McpStoreReader;
1008
+ /** Hermes home (== workspace == ~/.hermes). config.yaml + .env live here. */
1009
+ home?: string;
1010
+ /**
1011
+ * The `ALFE_API_KEY` value to write into `~/.hermes/.env` so the
1012
+ * `${ALFE_API_KEY}` refs in mirrored server envs resolve. In managed mode this
1013
+ * is the daemon's own api key; self-hosted threads it explicitly. When absent,
1014
+ * the `.env` write is skipped and a warning is logged (the servers would then
1015
+ * start in zero-accounts degraded mode).
1016
+ */
1017
+ apiKey?: string;
1018
+ /**
1019
+ * Trigger a runtime reload after a real change. MUST be the SAME callback the
1020
+ * daemon wires into `setRuntimeRestartNeededHandler` — there is exactly one
1021
+ * restart path. No-op when the runtime isn't running yet (the initial sync
1022
+ * writes config.yaml before Hermes starts, so no restart is needed then).
1023
+ */
1024
+ requestRestart?: () => void;
1025
+ /** Override config.yaml path (defaults to {home}/config.yaml). For tests. */
1026
+ configPath?: string;
1027
+ /** Override .env path (defaults to {home}/.env). For tests. */
1028
+ envPath?: string;
1029
+ /**
1030
+ * Override the sidecar that records which `mcp_servers` ids this sync wrote
1031
+ * (defaults to {home}/.alfe-mcp-synced.json). Used so removals survive daemon
1032
+ * restarts and so we NEVER delete user-authored `mcp_servers` entries.
1033
+ */
1034
+ trackingPath?: string;
1035
+ /**
1036
+ * Trailing debounce (ms) used to coalesce a burst of store mutations (one
1037
+ * integration install fires `addServer` once per server) into a single
1038
+ * config.yaml write + restart, avoiding a restart storm. Default 250; set 0 in
1039
+ * tests. The initial sync in `start()` is always immediate.
1040
+ */
1041
+ debounceMs?: number;
1042
+ }
1043
+ /**
1044
+ * Read-merge-write mirror of the MCP store into `~/.hermes/config.yaml`.
1045
+ *
1046
+ * Ownership / removal model: every entry returned by `manager.listServers()`
1047
+ * comes from the Alfe store (its `owner` is `integration:<id>` / `cli` /
1048
+ * `manual`) and is therefore Alfe-owned — these are the ids we write. User
1049
+ * authored `mcp_servers` entries live ONLY in config.yaml and never appear in
1050
+ * the store, so we never touch them. To know which config.yaml ids to REMOVE
1051
+ * when a server leaves the store, we persist the set of ids we last wrote to a
1052
+ * sidecar (`.alfe-mcp-synced.json`) and only ever delete from that set — exactly
1053
+ * how the bundler store tracks `_ownedOpenclawKeys` for its openclaw.json mirror.
1054
+ */
1055
+ declare class HermesMcpSync {
1056
+ private readonly manager;
1057
+ private readonly home;
1058
+ private readonly apiKey?;
1059
+ private readonly requestRestart?;
1060
+ private readonly configPath;
1061
+ private readonly envPath;
1062
+ private readonly trackingPath;
1063
+ private readonly debounceMs;
1064
+ /** Ids of `mcp_servers` entries this sync last wrote — the removal set. */
1065
+ private syncedIds;
1066
+ /** Serializes syncs so two onChange-driven runs can't interleave file writes. */
1067
+ private queue;
1068
+ private unsubscribe?;
1069
+ private debounceTimer?;
1070
+ private started;
1071
+ constructor(opts: HermesMcpSyncOptions);
1072
+ /**
1073
+ * Begin mirroring: load the prior removal set, run one immediate sync (so
1074
+ * config.yaml reflects the current store before Hermes first starts), then
1075
+ * subscribe to store changes (debounced). Idempotent.
1076
+ */
1077
+ start(): void;
1078
+ /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
1079
+ stop(): void;
1080
+ /**
1081
+ * Mirror the current store into config.yaml + .env exactly once. Public so the
1082
+ * daemon (and tests) can await a deterministic sync. Serialized against any
1083
+ * other in-flight sync.
1084
+ */
1085
+ syncOnce(): Promise<void>;
1086
+ private schedule;
1087
+ private syncNow;
1088
+ private computeDesired;
1089
+ /**
1090
+ * Transform a stored entry into a Hermes `mcp_servers` entry. SPIKE-PENDING
1091
+ * SEAM #3 lives here — the schema shape. stdio entries get `ALFE_API_KEY`
1092
+ * injected; remote entries pass through (Hermes supports `url`-based MCP).
1093
+ */
1094
+ private toHermesEntry;
1095
+ private withAlfeApiKey;
1096
+ /** Read-merge-write `~/.hermes/.env` to set ALFE_API_KEY without clobbering other keys. */
1097
+ private ensureEnvApiKey;
1098
+ private loadSyncedIds;
1099
+ private persistSyncedIds;
1100
+ }
1101
+ /**
1102
+ * Set `KEY=value` in a `.env`-style file, preserving every other line (comments,
1103
+ * blank lines, unrelated keys) and order. Returns whether the file changed. Not
1104
+ * a full dotenv parser — it only matches simple `KEY=` lines, which is all
1105
+ * `~/.hermes/.env` ever holds.
1106
+ */
1107
+ declare function upsertEnvVar(envPath: string, key: string, value: string): boolean;
1108
+ //#endregion
850
1109
  //#region src/lock.d.ts
851
1110
  interface RuntimePluginEntry {
852
1111
  /**
@@ -866,9 +1125,29 @@ interface RuntimeSkillEntry {
866
1125
  sourceIntegration: string;
867
1126
  integrationVersion: string;
868
1127
  }
1128
+ /**
1129
+ * Records that an integration applied runtime *config* (via the applier's
1130
+ * `applyConfig`) to a runtime — even when it contributed no plugins/skills.
1131
+ *
1132
+ * Without this, a config-only integration left no lock entry for its runtime,
1133
+ * so `removeEntries` returned nothing for it and `deactivate` never drove
1134
+ * `applier.removeConfig(...)` — leaking the applied config on removal. The
1135
+ * applier itself owns the actual config payload (e.g. OpenClaw's tracking
1136
+ * file); the lock only needs to know which integration touched which runtime.
1137
+ */
1138
+ interface RuntimeConfigEntry {
1139
+ sourceIntegration: string;
1140
+ integrationVersion: string;
1141
+ }
869
1142
  interface RuntimeDesiredState {
870
1143
  plugins: RuntimePluginEntry[];
871
1144
  skills: RuntimeSkillEntry[];
1145
+ /**
1146
+ * Integrations that applied config to this runtime. Optional for
1147
+ * back-compat with lock files written before this field existed — always
1148
+ * read it as `state.config ?? []`.
1149
+ */
1150
+ config?: RuntimeConfigEntry[];
872
1151
  }
873
1152
  interface RuntimeLockFile {
874
1153
  version: 1;
@@ -888,15 +1167,26 @@ declare class LockManager {
888
1167
  write(lock: RuntimeLockFile): void;
889
1168
  /**
890
1169
  * Add entries for an integration activation in a specific runtime.
1170
+ *
1171
+ * Pass `opts.configApplied` when the integration applied runtime config so
1172
+ * the contribution is recorded even if it ships no plugins/skills — this is
1173
+ * what lets a config-only integration be torn down on deactivate.
891
1174
  */
892
- addEntries(runtime: string, integrationId: string, version: string, plugins: PluginInstall[], skills: SkillInstall[], installPath: string): void;
1175
+ addEntries(runtime: string, integrationId: string, version: string, plugins: PluginInstall[], skills: SkillInstall[], installPath: string, opts?: {
1176
+ configApplied?: boolean;
1177
+ }): void;
893
1178
  /**
894
1179
  * Remove all entries for a given integration across all runtimes.
895
1180
  * Returns what was removed, keyed by runtime.
1181
+ *
1182
+ * A runtime is included in the result when the integration contributed
1183
+ * plugins, skills, OR config there — so `deactivate` drives
1184
+ * `applier.removeConfig` even for a config-only integration.
896
1185
  */
897
1186
  removeEntries(integrationId: string): Record<string, {
898
1187
  plugins: RuntimePluginEntry[];
899
1188
  skills: RuntimeSkillEntry[];
1189
+ config: RuntimeConfigEntry[];
900
1190
  }>;
901
1191
  /**
902
1192
  * Get the full desired state for a specific runtime.
@@ -964,4 +1254,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
964
1254
  resetReinstallAttempts(integrationId: string): void;
965
1255
  }
966
1256
  //#endregion
967
- 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 };
1257
+ 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 };