@alfe.ai/gateway 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.
package/dist/health.js CHANGED
@@ -12,7 +12,7 @@ import crypto from "crypto";
12
12
  import { parse } from "smol-toml";
13
13
  import WebSocket from "ws";
14
14
  import { createConnection, createServer } from "node:net";
15
- import { IntegrationManager, IntegrationManagerAdapter, McpApplier, OpenClawApplier } from "@alfe.ai/integrations";
15
+ import { HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, OpenClawApplier } from "@alfe.ai/integrations";
16
16
  import { AgentApiClient } from "@alfe.ai/agent-api-client";
17
17
  import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
18
18
  import stream, { Readable } from "stream";
@@ -4480,7 +4480,8 @@ enumValues({
4480
4480
  });
4481
4481
  enumValues({
4482
4482
  OpenClaw: "openclaw",
4483
- NanoClaw: "nanoclaw"
4483
+ NanoClaw: "nanoclaw",
4484
+ Hermes: "hermes"
4484
4485
  });
4485
4486
  enumValues({
4486
4487
  Org: "org",
@@ -4965,17 +4966,33 @@ async function loadManagedConfig() {
4965
4966
  agentId: identity.agentId,
4966
4967
  orgId: identity.orgId,
4967
4968
  runtime: identity.runtime,
4968
- runtimes: identity.runtime === "openclaw" ? (() => {
4969
- const home = join(homedir(), ".openclaw");
4970
- return { openclaw: {
4971
- workspace: home,
4972
- agentWorkspace: deriveAgentWorkspace("openclaw", home)
4973
- } };
4974
- })() : {},
4969
+ runtimes: buildManagedRuntimes(identity.runtime),
4975
4970
  autoStartRuntime: true
4976
4971
  };
4977
4972
  }
4978
4973
  /**
4974
+ * Build the managed-mode `runtimes` map for a single resolved runtime.
4975
+ * Returns an empty map for unknown runtimes (the daemon then skips runtime
4976
+ * start rather than crashing).
4977
+ */
4978
+ function buildManagedRuntimes(runtime) {
4979
+ if (runtime === "openclaw") {
4980
+ const home = join(homedir(), ".openclaw");
4981
+ return { openclaw: {
4982
+ workspace: home,
4983
+ agentWorkspace: deriveAgentWorkspace("openclaw", home)
4984
+ } };
4985
+ }
4986
+ if (runtime === "hermes") {
4987
+ const home = join(homedir(), ".hermes");
4988
+ return { hermes: {
4989
+ workspace: home,
4990
+ agentWorkspace: deriveAgentWorkspace("hermes", home)
4991
+ } };
4992
+ }
4993
+ return {};
4994
+ }
4995
+ /**
4979
4996
  * Load full daemon configuration.
4980
4997
  * Reads config.toml, validates the API key, resolves agent identity,
4981
4998
  * and parses runtime configurations.
@@ -4994,6 +5011,7 @@ async function loadDaemonConfig() {
4994
5011
  const identity = await resolveAgentIdentity(alfeConfig.api_key, apiEndpoint);
4995
5012
  const gatewayWsUrl = alfeConfig.gateway_url ?? deriveGatewayWsUrl(apiEndpoint);
4996
5013
  const runtimes = await loadRuntimeConfigs();
5014
+ const runtime = alfeConfig.runtime ?? identity.runtime;
4997
5015
  return {
4998
5016
  apiKey: alfeConfig.api_key,
4999
5017
  apiEndpoint,
@@ -5002,7 +5020,7 @@ async function loadDaemonConfig() {
5002
5020
  pidPath: PID_PATH,
5003
5021
  agentId: identity.agentId,
5004
5022
  orgId: identity.orgId,
5005
- runtime: identity.runtime,
5023
+ runtime,
5006
5024
  runtimes,
5007
5025
  autoStartRuntime: alfeConfig.auto_start_runtime ?? true
5008
5026
  };
@@ -21617,6 +21635,10 @@ var RuntimeProcess = class {
21617
21635
  "--allow-unconfigured"
21618
21636
  ]
21619
21637
  };
21638
+ case "hermes": return {
21639
+ command: "hermes",
21640
+ args: ["gateway", "run"]
21641
+ };
21620
21642
  default: throw new Error(`Unsupported runtime: ${this.options.runtime}`);
21621
21643
  }
21622
21644
  }
@@ -22195,6 +22217,12 @@ let startedAt;
22195
22217
  let integrationManager;
22196
22218
  let mcpBundler = null;
22197
22219
  let mcpManagerRef = null;
22220
+ /**
22221
+ * Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
22222
+ * store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
22223
+ * and for managed/self-hosted runtimes other than hermes.
22224
+ */
22225
+ let hermesMcpSync = null;
22198
22226
  let aiProxyServer = null;
22199
22227
  let runtimeProcess = null;
22200
22228
  let aiProxyUrl = null;
@@ -22207,6 +22235,12 @@ let resolvedRuntimeVersion;
22207
22235
  let upgradingRuntime = false;
22208
22236
  let stopPairingApprovalPoller = null;
22209
22237
  /**
22238
+ * Module-level handle on the runtime appliers built during start(), so the
22239
+ * module-scoped command handler can route `alfe.config_set` to the active
22240
+ * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
22241
+ */
22242
+ let runtimeAppliersRef = null;
22243
+ /**
22210
22244
  * Resolve the installed @alfe.ai/cli version.
22211
22245
  *
22212
22246
  * Strategy (in order):
@@ -22281,15 +22315,44 @@ async function getCliVersion() {
22281
22315
  logger$1.debug("Could not resolve @alfe.ai/cli version");
22282
22316
  }
22283
22317
  /**
22284
- * Resolve the installed runtime (OpenClaw) version.
22285
- * Runs `openclaw --version` and returns the trimmed output.
22318
+ * Per-runtime "print installed version" commands. Local to the gateway — we do
22319
+ * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
22320
+ * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
22321
+ * connection-status report degrades gracefully instead of crashing.
22322
+ */
22323
+ const RUNTIME_VERSION_COMMANDS = {
22324
+ openclaw: {
22325
+ command: "openclaw",
22326
+ args: ["--version"]
22327
+ },
22328
+ hermes: {
22329
+ command: "hermes",
22330
+ args: ["version"]
22331
+ }
22332
+ };
22333
+ /**
22334
+ * Pure resolver (exported for tests) — the per-runtime version command, or
22335
+ * `undefined` for an unknown runtime.
22336
+ */
22337
+ function resolveRuntimeVersionCommand(runtime) {
22338
+ return RUNTIME_VERSION_COMMANDS[runtime];
22339
+ }
22340
+ /**
22341
+ * Resolve the installed runtime version by running the per-runtime version
22342
+ * command and returning the trimmed stdout. An unknown runtime (no command in
22343
+ * the map) returns `undefined` without spawning — it must never throw.
22286
22344
  */
22287
- async function getRuntimeVersion() {
22345
+ async function getRuntimeVersion(runtime) {
22346
+ const cmd = resolveRuntimeVersionCommand(runtime);
22347
+ if (!cmd) {
22348
+ logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
22349
+ return;
22350
+ }
22288
22351
  try {
22289
- const { stdout } = await execFileAsync("openclaw", ["--version"]);
22352
+ const { stdout } = await execFileAsync(cmd.command, cmd.args);
22290
22353
  return stdout.trim() || void 0;
22291
22354
  } catch {
22292
- logger$1.debug("Could not resolve openclaw runtime version");
22355
+ logger$1.debug({ runtime }, "Could not resolve runtime version");
22293
22356
  return;
22294
22357
  }
22295
22358
  }
@@ -22437,7 +22500,7 @@ async function startDaemon() {
22437
22500
  await flushAndExit(1);
22438
22501
  }
22439
22502
  resolvedCliVersion = await getCliVersion();
22440
- resolvedRuntimeVersion = await getRuntimeVersion();
22503
+ resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
22441
22504
  logger$1.info({
22442
22505
  cliVersion: resolvedCliVersion,
22443
22506
  runtimeVersion: resolvedRuntimeVersion
@@ -22470,7 +22533,14 @@ async function startDaemon() {
22470
22533
  home: runtimeCfg.workspace,
22471
22534
  agentWorkspace: runtimeCfg.agentWorkspace
22472
22535
  }, "Registered OpenClaw runtime applier");
22536
+ } else if (name === "hermes") {
22537
+ runtimeAppliers.set(name, new HermesApplier({ home: runtimeCfg.workspace }));
22538
+ logger$1.info({
22539
+ runtime: name,
22540
+ home: runtimeCfg.workspace
22541
+ }, "Registered Hermes runtime applier");
22473
22542
  } else logger$1.warn({ runtime: name }, "Unknown runtime type — skipping");
22543
+ runtimeAppliersRef = runtimeAppliers;
22474
22544
  const integrationsService = new IntegrationsService(new AlfeApiClient({
22475
22545
  apiBaseUrl: config.apiEndpoint,
22476
22546
  getToken: () => Promise.resolve(config.apiKey)
@@ -22485,22 +22555,25 @@ async function startDaemon() {
22485
22555
  logger: logger$1,
22486
22556
  idleTtlMs: 0
22487
22557
  }, { connect: defaultConnect });
22488
- await mcpManager.loadIntoBundler(mcpBundler);
22489
- const warmBundler = (reason) => {
22490
- if (!mcpBundler) return;
22491
- mcpBundler.warmup().catch((err) => {
22492
- logger$1.warn({
22493
- err: err instanceof Error ? err.message : String(err),
22494
- reason
22495
- }, "MCP bundler warmup failed");
22558
+ if (config.runtime === "hermes") logger$1.info("Hermes runtime — daemon MCP bundler left idle (store mirrored to config.yaml by HermesMcpSync)");
22559
+ else {
22560
+ await mcpManager.loadIntoBundler(mcpBundler);
22561
+ const warmBundler = (reason) => {
22562
+ if (!mcpBundler) return;
22563
+ mcpBundler.warmup().catch((err) => {
22564
+ logger$1.warn({
22565
+ err: err instanceof Error ? err.message : String(err),
22566
+ reason
22567
+ }, "MCP bundler warmup failed");
22568
+ });
22569
+ };
22570
+ warmBundler("startup");
22571
+ mcpManager.onChange(() => {
22572
+ warmBundler("store change");
22496
22573
  });
22497
- };
22498
- warmBundler("startup");
22499
- mcpManager.onChange(() => {
22500
- warmBundler("store change");
22501
- });
22502
- logger$1.info("MCP bundler attached to manager — warming children");
22503
- await runOpenclawMcpCleanup({
22574
+ logger$1.info("MCP bundler attached to manager — warming children");
22575
+ }
22576
+ if (config.runtime === "openclaw") await runOpenclawMcpCleanup({
22504
22577
  manager: mcpManager,
22505
22578
  logger: logger$1
22506
22579
  }).catch((err) => {
@@ -22521,14 +22594,27 @@ async function startDaemon() {
22521
22594
  });
22522
22595
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
22523
22596
  cloudClient.setIntegrationManager(integrationAdapter);
22524
- cloudClient.setRuntimeRestartNeededHandler(() => {
22597
+ const requestRuntimeRestart = () => {
22525
22598
  if (runtimeProcess) {
22526
22599
  logger$1.info("Runtime state changed — restarting runtime");
22527
22600
  runtimeProcess.restart().catch((err) => {
22528
22601
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
22529
22602
  });
22530
22603
  }
22531
- });
22604
+ };
22605
+ cloudClient.setRuntimeRestartNeededHandler(requestRuntimeRestart);
22606
+ if (config.runtime === "hermes") {
22607
+ const hermesCfg = config.runtimes[config.runtime];
22608
+ if (hermesCfg) {
22609
+ hermesMcpSync = new HermesMcpSync({
22610
+ manager: mcpManager,
22611
+ home: hermesCfg.workspace,
22612
+ apiKey: config.apiKey,
22613
+ requestRestart: requestRuntimeRestart
22614
+ });
22615
+ hermesMcpSync.start();
22616
+ }
22617
+ }
22532
22618
  cloudClient.setOnReconciliationComplete(() => {
22533
22619
  try {
22534
22620
  const activeCommands = integrationManager.getActiveCommands();
@@ -22583,6 +22669,10 @@ async function startDaemon() {
22583
22669
  await runtimeProcess.stop();
22584
22670
  logger$1.debug("Runtime process stopped");
22585
22671
  }
22672
+ if (hermesMcpSync) {
22673
+ hermesMcpSync.stop();
22674
+ hermesMcpSync = null;
22675
+ }
22586
22676
  if (mcpBundler) {
22587
22677
  logger$1.debug("Stopping MCP bundler...");
22588
22678
  await mcpBundler.dispose();
@@ -22671,17 +22761,28 @@ async function handleCloudCommand(command) {
22671
22761
  message: "Runtime upgrade already in progress"
22672
22762
  }
22673
22763
  };
22674
- const version = command.payload?.version ?? "2026.6.8";
22764
+ const payload = command.payload;
22765
+ const runtime = config.runtime;
22766
+ const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.8" : void 0);
22675
22767
  upgradingRuntime = true;
22676
22768
  setTimeout(() => {
22677
22769
  (async () => {
22678
22770
  try {
22679
22771
  const { upgradeRuntime } = await import("./runtime-upgrade.js");
22680
- if ((await upgradeRuntime(version, runtimeProcess)).success) {
22681
- resolvedRuntimeVersion = await getRuntimeVersion();
22682
- if (!resolvedRuntimeVersion) logger$1.warn({ requestedVersion: version }, "Could not detect runtime version after upgrade — using requested version");
22683
- logger$1.info({ runtimeVersion: resolvedRuntimeVersion ?? version }, "Runtime version updated after upgrade");
22684
- cloudClient.updateRuntimeVersionAndReRegister(resolvedRuntimeVersion ?? version);
22772
+ if ((await upgradeRuntime(runtime, version, runtimeProcess)).success) {
22773
+ resolvedRuntimeVersion = await getRuntimeVersion(runtime);
22774
+ const reportedVersion = resolvedRuntimeVersion ?? version;
22775
+ if (!resolvedRuntimeVersion) logger$1.warn({
22776
+ runtime,
22777
+ requestedVersion: version
22778
+ }, "Could not detect runtime version after upgrade");
22779
+ if (reportedVersion) {
22780
+ logger$1.info({
22781
+ runtime,
22782
+ runtimeVersion: reportedVersion
22783
+ }, "Runtime version updated after upgrade");
22784
+ cloudClient.updateRuntimeVersionAndReRegister(reportedVersion);
22785
+ } else logger$1.warn({ runtime }, "No runtime version to report after upgrade — skipping re-register");
22685
22786
  }
22686
22787
  } catch (err) {
22687
22788
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Runtime upgrade failed");
@@ -22696,6 +22797,7 @@ async function handleCloudCommand(command) {
22696
22797
  status: "ok",
22697
22798
  result: {
22698
22799
  upgrading: true,
22800
+ runtime,
22699
22801
  version
22700
22802
  }
22701
22803
  };
@@ -22776,17 +22878,30 @@ async function handleCloudCommand(command) {
22776
22878
  message: "alfe.config_set requires key and value"
22777
22879
  }
22778
22880
  };
22881
+ const runtime = config.runtime;
22882
+ const applier = runtimeAppliersRef?.get(runtime);
22883
+ if (!applier || typeof applier.setConfigRaw !== "function") {
22884
+ const message = `No runtime applier supports config_set for runtime "${runtime}"`;
22885
+ logger$1.warn({
22886
+ runtime,
22887
+ key
22888
+ }, message);
22889
+ return {
22890
+ type: "COMMAND_ACK",
22891
+ commandId: command.commandId,
22892
+ status: "error",
22893
+ result: {
22894
+ code: "CONFIG_SET_UNSUPPORTED",
22895
+ message
22896
+ }
22897
+ };
22898
+ }
22779
22899
  try {
22780
- await execFileAsync("openclaw", [
22781
- "config",
22782
- "set",
22783
- key,
22784
- value
22785
- ], { timeout: 1e4 });
22900
+ await applier.setConfigRaw(key, value);
22786
22901
  logger$1.info({
22787
- key,
22788
- value
22789
- }, "Applied config via openclaw config set");
22902
+ runtime,
22903
+ key
22904
+ }, "Applied config via runtime applier setConfigRaw");
22790
22905
  return {
22791
22906
  type: "COMMAND_ACK",
22792
22907
  commandId: command.commandId,
@@ -22800,9 +22915,9 @@ async function handleCloudCommand(command) {
22800
22915
  const message = err instanceof Error ? err.message : String(err);
22801
22916
  logger$1.error({
22802
22917
  err: message,
22803
- key,
22804
- value
22805
- }, "Failed to apply config via openclaw");
22918
+ runtime,
22919
+ key
22920
+ }, "Failed to apply config via runtime applier");
22806
22921
  return {
22807
22922
  type: "COMMAND_ACK",
22808
22923
  commandId: command.commandId,
@@ -3,40 +3,87 @@ import { execFile } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  //#region src/runtime-upgrade.ts
5
5
  /**
6
- * Runtime upgrade — npm install then cycle the RuntimeProcess.
6
+ * Runtime upgrade — install/update the runtime binary then cycle the RuntimeProcess.
7
7
  *
8
8
  * Unlike CLI upgrade (which exits the daemon for systemd restart),
9
9
  * runtime upgrade keeps the daemon running and just restarts the child process.
10
10
  *
11
- * Flow:
11
+ * Flow (generic across runtimes):
12
12
  * 1. Stop the RuntimeProcess child (SIGTERM + grace period)
13
- * 2. npm install -g openclaw@{version}
13
+ * 2. Run the per-runtime upgrade command (see `resolveUpgradeCommand`)
14
14
  * 3. Restart the RuntimeProcess child
15
15
  *
16
- * On npm failure, the runtime is restarted on the old version.
16
+ * On install failure, the runtime is restarted on the old version.
17
17
  */
18
18
  const execFileAsync = promisify(execFile);
19
- async function upgradeRuntime(version, runtimeProcess) {
20
- logger.info({ version }, "Upgrading OpenClaw runtime...");
19
+ /**
20
+ * Resolve the per-runtime upgrade command.
21
+ *
22
+ * - `openclaw`: `npm install -g openclaw@<version>` — version-pinned (unchanged).
23
+ * - `hermes`: `hermes update --yes` — Hermes self-updates from its own channel.
24
+ * A pinned version does not map to a Hermes CLI flag, so the
25
+ * `version` arg is intentionally ignored on this branch.
26
+ *
27
+ * Returns `undefined` for an unknown runtime (or openclaw with no version),
28
+ * which the caller treats as a failed upgrade (restart on the old version).
29
+ */
30
+ function resolveUpgradeCommand(runtime, version) {
31
+ switch (runtime) {
32
+ case "openclaw":
33
+ if (!version) return void 0;
34
+ return {
35
+ command: "npm",
36
+ args: [
37
+ "install",
38
+ "-g",
39
+ `openclaw@${version}`
40
+ ]
41
+ };
42
+ case "hermes": return {
43
+ command: "hermes",
44
+ args: ["update", "--yes"]
45
+ };
46
+ default: return;
47
+ }
48
+ }
49
+ async function upgradeRuntime(runtime, version, runtimeProcess) {
50
+ const cmd = resolveUpgradeCommand(runtime, version);
51
+ if (!cmd) {
52
+ const message = `No upgrade command for runtime '${runtime}'`;
53
+ logger.error({
54
+ runtime,
55
+ version
56
+ }, message);
57
+ return {
58
+ success: false,
59
+ runtime,
60
+ version,
61
+ error: message
62
+ };
63
+ }
64
+ logger.info({
65
+ runtime,
66
+ version
67
+ }, "Upgrading runtime...");
21
68
  if (runtimeProcess) {
22
69
  logger.info("Stopping runtime for upgrade...");
23
70
  await runtimeProcess.stop();
24
71
  }
25
72
  try {
26
- const { stdout, stderr } = await execFileAsync("npm", [
27
- "install",
28
- "-g",
29
- `openclaw@${version}`
30
- ], { timeout: 12e4 });
31
- if (stdout) logger.debug({ stdout: stdout.trim() }, "npm install stdout");
32
- if (stderr) logger.debug({ stderr: stderr.trim() }, "npm install stderr");
33
- logger.info({ version }, "OpenClaw runtime upgraded successfully");
73
+ const { stdout, stderr } = await execFileAsync(cmd.command, cmd.args, { timeout: 12e4 });
74
+ if (stdout) logger.debug({ stdout: stdout.trim() }, "runtime upgrade stdout");
75
+ if (stderr) logger.debug({ stderr: stderr.trim() }, "runtime upgrade stderr");
76
+ logger.info({
77
+ runtime,
78
+ version
79
+ }, "Runtime upgraded successfully");
34
80
  } catch (err) {
35
81
  const message = err instanceof Error ? err.message : String(err);
36
82
  logger.error({
37
83
  err: message,
84
+ runtime,
38
85
  version
39
- }, "Runtime upgrade npm install failed");
86
+ }, "Runtime upgrade install command failed");
40
87
  if (runtimeProcess) try {
41
88
  logger.info("Restarting runtime on previous version after failed upgrade...");
42
89
  await runtimeProcess.restart();
@@ -46,6 +93,7 @@ async function upgradeRuntime(version, runtimeProcess) {
46
93
  }
47
94
  return {
48
95
  success: false,
96
+ runtime,
49
97
  version,
50
98
  error: message
51
99
  };
@@ -56,6 +104,7 @@ async function upgradeRuntime(version, runtimeProcess) {
56
104
  }
57
105
  return {
58
106
  success: true,
107
+ runtime,
59
108
  version
60
109
  };
61
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,11 +22,11 @@
22
22
  "pino-roll": "^1.2.0",
23
23
  "smol-toml": ">=1.6.1",
24
24
  "ws": "^8.18.0",
25
- "@alfe.ai/agent-api-client": "^0.2.3",
26
- "@alfe.ai/ai-proxy-local": "^0.0.10",
27
- "@alfe.ai/config": "^0.0.9",
28
- "@alfe.ai/integration-manifest": "^0.2.1",
29
- "@alfe.ai/integrations": "^0.1.6",
25
+ "@alfe.ai/agent-api-client": "^0.3.0",
26
+ "@alfe.ai/ai-proxy-local": "^0.0.11",
27
+ "@alfe.ai/config": "^0.1.0",
28
+ "@alfe.ai/integration-manifest": "^0.3.0",
29
+ "@alfe.ai/integrations": "^0.2.0",
30
30
  "@alfe.ai/mcp-bundler": "^0.2.1"
31
31
  },
32
32
  "license": "UNLICENSED",