@alfe.ai/gateway 0.1.4 → 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";
@@ -109,6 +109,8 @@ const ID_PREFIXES = {
109
109
  identityVerification: "ivf",
110
110
  role: "role",
111
111
  directGrant: "dgr",
112
+ revision: "rev",
113
+ fact: "kfc",
112
114
  oauthConnection: "con",
113
115
  channel: "chn",
114
116
  oauthConnectionRequest: "crq",
@@ -1292,6 +1294,7 @@ const string$1 = (params) => {
1292
1294
  };
1293
1295
  const integer = /^-?\d+$/;
1294
1296
  const number$1 = /^-?\d+(?:\.\d+)?$/;
1297
+ const boolean$1 = /^(?:true|false)$/i;
1295
1298
  const lowercase = /^[^A-Z]*$/;
1296
1299
  const uppercase = /^[^a-z]*$/;
1297
1300
  //#endregion
@@ -2066,6 +2069,24 @@ const $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst,
2066
2069
  $ZodCheckNumberFormat.init(inst, def);
2067
2070
  $ZodNumber.init(inst, def);
2068
2071
  });
2072
+ const $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
2073
+ $ZodType.init(inst, def);
2074
+ inst._zod.pattern = boolean$1;
2075
+ inst._zod.parse = (payload, _ctx) => {
2076
+ if (def.coerce) try {
2077
+ payload.value = Boolean(payload.value);
2078
+ } catch (_) {}
2079
+ const input = payload.value;
2080
+ if (typeof input === "boolean") return payload;
2081
+ payload.issues.push({
2082
+ expected: "boolean",
2083
+ code: "invalid_type",
2084
+ input,
2085
+ inst
2086
+ });
2087
+ return payload;
2088
+ };
2089
+ });
2069
2090
  const $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
2070
2091
  $ZodType.init(inst, def);
2071
2092
  inst._zod.parse = (payload) => payload;
@@ -3046,6 +3067,13 @@ function _int(Class, params) {
3046
3067
  });
3047
3068
  }
3048
3069
  /* @__NO_SIDE_EFFECTS__ */
3070
+ function _boolean(Class, params) {
3071
+ return new Class({
3072
+ type: "boolean",
3073
+ ...normalizeParams(params)
3074
+ });
3075
+ }
3076
+ /* @__NO_SIDE_EFFECTS__ */
3049
3077
  function _unknown(Class) {
3050
3078
  return new Class({ type: "unknown" });
3051
3079
  }
@@ -3588,6 +3616,9 @@ const numberProcessor = (schema, ctx, _json, _params) => {
3588
3616
  }
3589
3617
  if (typeof multipleOf === "number") json.multipleOf = multipleOf;
3590
3618
  };
3619
+ const booleanProcessor = (_schema, _ctx, json, _params) => {
3620
+ json.type = "boolean";
3621
+ };
3591
3622
  const neverProcessor = (_schema, _ctx, json, _params) => {
3592
3623
  json.not = {};
3593
3624
  };
@@ -4063,6 +4094,14 @@ const ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, d
4063
4094
  function int(params) {
4064
4095
  return /* @__PURE__ */ _int(ZodNumberFormat, params);
4065
4096
  }
4097
+ const ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
4098
+ $ZodBoolean.init(inst, def);
4099
+ ZodType.init(inst, def);
4100
+ inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
4101
+ });
4102
+ function boolean(params) {
4103
+ return /* @__PURE__ */ _boolean(ZodBoolean, params);
4104
+ }
4066
4105
  const ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
4067
4106
  $ZodUnknown.init(inst, def);
4068
4107
  ZodType.init(inst, def);
@@ -4391,7 +4430,10 @@ enumValues({
4391
4430
  Month: "month",
4392
4431
  Year: "year"
4393
4432
  });
4394
- object({ gracePeriodDays: number().int().positive().optional() });
4433
+ object({
4434
+ gracePeriodDays: number().int().positive().optional(),
4435
+ launchDiscountEnabled: boolean().optional()
4436
+ });
4395
4437
  enumValues({
4396
4438
  ManagedHosting: "managed-hosting",
4397
4439
  PlatformTier: "platform-tier",
@@ -4399,12 +4441,13 @@ enumValues({
4399
4441
  Other: "other"
4400
4442
  });
4401
4443
  enumValues({
4402
- Free: "free",
4403
- Personal: "personal",
4404
- PersonalPlus: "personal_plus",
4405
- PersonalPro: "personal_pro",
4406
- Tier1: "tier_1",
4407
- Tier2: "tier_2"
4444
+ IndividualLite: "individual_lite",
4445
+ IndividualNormal: "individual_normal",
4446
+ IndividualPro: "individual_pro",
4447
+ IndividualMax: "individual_max",
4448
+ OrgFree: "org_free",
4449
+ OrgProfessional: "org_professional",
4450
+ OrgEnterprise: "org_enterprise"
4408
4451
  });
4409
4452
  enumValues({
4410
4453
  Pending: "pending",
@@ -4437,7 +4480,8 @@ enumValues({
4437
4480
  });
4438
4481
  enumValues({
4439
4482
  OpenClaw: "openclaw",
4440
- NanoClaw: "nanoclaw"
4483
+ NanoClaw: "nanoclaw",
4484
+ Hermes: "hermes"
4441
4485
  });
4442
4486
  enumValues({
4443
4487
  Org: "org",
@@ -4922,17 +4966,33 @@ async function loadManagedConfig() {
4922
4966
  agentId: identity.agentId,
4923
4967
  orgId: identity.orgId,
4924
4968
  runtime: identity.runtime,
4925
- runtimes: identity.runtime === "openclaw" ? (() => {
4926
- const home = join(homedir(), ".openclaw");
4927
- return { openclaw: {
4928
- workspace: home,
4929
- agentWorkspace: deriveAgentWorkspace("openclaw", home)
4930
- } };
4931
- })() : {},
4969
+ runtimes: buildManagedRuntimes(identity.runtime),
4932
4970
  autoStartRuntime: true
4933
4971
  };
4934
4972
  }
4935
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
+ /**
4936
4996
  * Load full daemon configuration.
4937
4997
  * Reads config.toml, validates the API key, resolves agent identity,
4938
4998
  * and parses runtime configurations.
@@ -4951,6 +5011,7 @@ async function loadDaemonConfig() {
4951
5011
  const identity = await resolveAgentIdentity(alfeConfig.api_key, apiEndpoint);
4952
5012
  const gatewayWsUrl = alfeConfig.gateway_url ?? deriveGatewayWsUrl(apiEndpoint);
4953
5013
  const runtimes = await loadRuntimeConfigs();
5014
+ const runtime = alfeConfig.runtime ?? identity.runtime;
4954
5015
  return {
4955
5016
  apiKey: alfeConfig.api_key,
4956
5017
  apiEndpoint,
@@ -4959,7 +5020,7 @@ async function loadDaemonConfig() {
4959
5020
  pidPath: PID_PATH,
4960
5021
  agentId: identity.agentId,
4961
5022
  orgId: identity.orgId,
4962
- runtime: identity.runtime,
5023
+ runtime,
4963
5024
  runtimes,
4964
5025
  autoStartRuntime: alfeConfig.auto_start_runtime ?? true
4965
5026
  };
@@ -5172,7 +5233,18 @@ const PROTOCOL_VERSION = 1;
5172
5233
  //#endregion
5173
5234
  //#region src/reconciliation.ts
5174
5235
  const log$2 = logger$1.child({ component: "Reconciliation" });
5236
+ /**
5237
+ * How many times reconcile will re-attempt activation of an intact integration
5238
+ * stuck in `error` (with no reinstall requested) before giving up and waiting
5239
+ * for a manual reinstall. Each reconcile pass = one cloud DESIRED_STATE push,
5240
+ * so these attempts are spread across pushes, not a tight loop. This lets a
5241
+ * transient activation failure (e.g. an `openclaw config set` racing a runtime
5242
+ * hot-reload) self-heal without the user clicking Reinstall.
5243
+ */
5244
+ const MAX_ERROR_ACTIVATE_ATTEMPTS = 3;
5175
5245
  var ReconciliationEngine = class {
5246
+ /** Consecutive re-activation attempts for an intact error-state integration, keyed by id. */
5247
+ activateAttempts = /* @__PURE__ */ new Map();
5176
5248
  constructor(manager) {
5177
5249
  this.manager = manager;
5178
5250
  }
@@ -5236,6 +5308,7 @@ var ReconciliationEngine = class {
5236
5308
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5237
5309
  log$2.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5238
5310
  this.manager.resetReinstallAttempts(id);
5311
+ this.activateAttempts.delete(id);
5239
5312
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5240
5313
  report.installed.push(id);
5241
5314
  report.activated.push(id);
@@ -5267,6 +5340,7 @@ var ReconciliationEngine = class {
5267
5340
  try {
5268
5341
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5269
5342
  this.manager.resetReinstallAttempts(id);
5343
+ this.activateAttempts.delete(id);
5270
5344
  report.installed.push(id);
5271
5345
  report.activated.push(id);
5272
5346
  report.results.push({
@@ -5290,7 +5364,37 @@ var ReconciliationEngine = class {
5290
5364
  }
5291
5365
  return;
5292
5366
  }
5293
- log$2.warn(`Integration ${id} is in error state — waiting for reinstall request`);
5367
+ const activateAttempts = this.activateAttempts.get(id) ?? 0;
5368
+ if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5369
+ this.activateAttempts.set(id, activateAttempts + 1);
5370
+ log$2.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5371
+ try {
5372
+ if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5373
+ this.activateAttempts.delete(id);
5374
+ report.activated.push(id);
5375
+ report.results.push({
5376
+ integrationId: id,
5377
+ action: "activated",
5378
+ actualStatus: "active"
5379
+ });
5380
+ return;
5381
+ } catch (reactivateErr) {
5382
+ const reactivateMsg = reactivateErr instanceof Error ? reactivateErr.message : String(reactivateErr);
5383
+ log$2.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5384
+ report.errors.push({
5385
+ integrationId: id,
5386
+ error: reactivateMsg
5387
+ });
5388
+ report.results.push({
5389
+ integrationId: id,
5390
+ action: "error",
5391
+ actualStatus: "error",
5392
+ errorMessage: reactivateMsg
5393
+ });
5394
+ return;
5395
+ }
5396
+ }
5397
+ log$2.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5294
5398
  report.errors.push({
5295
5399
  integrationId: id,
5296
5400
  error: "Integration is in error state"
@@ -5317,6 +5421,7 @@ var ReconciliationEngine = class {
5317
5421
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5318
5422
  log$2.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5319
5423
  this.manager.resetReinstallAttempts(id);
5424
+ this.activateAttempts.delete(id);
5320
5425
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5321
5426
  report.installed.push(id);
5322
5427
  report.activated.push(id);
@@ -5327,6 +5432,7 @@ var ReconciliationEngine = class {
5327
5432
  });
5328
5433
  return;
5329
5434
  }
5435
+ this.activateAttempts.delete(id);
5330
5436
  report.results.push({
5331
5437
  integrationId: id,
5332
5438
  action: "up_to_date",
@@ -21529,6 +21635,10 @@ var RuntimeProcess = class {
21529
21635
  "--allow-unconfigured"
21530
21636
  ]
21531
21637
  };
21638
+ case "hermes": return {
21639
+ command: "hermes",
21640
+ args: ["gateway", "run"]
21641
+ };
21532
21642
  default: throw new Error(`Unsupported runtime: ${this.options.runtime}`);
21533
21643
  }
21534
21644
  }
@@ -22107,6 +22217,12 @@ let startedAt;
22107
22217
  let integrationManager;
22108
22218
  let mcpBundler = null;
22109
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;
22110
22226
  let aiProxyServer = null;
22111
22227
  let runtimeProcess = null;
22112
22228
  let aiProxyUrl = null;
@@ -22119,6 +22235,12 @@ let resolvedRuntimeVersion;
22119
22235
  let upgradingRuntime = false;
22120
22236
  let stopPairingApprovalPoller = null;
22121
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
+ /**
22122
22244
  * Resolve the installed @alfe.ai/cli version.
22123
22245
  *
22124
22246
  * Strategy (in order):
@@ -22193,15 +22315,44 @@ async function getCliVersion() {
22193
22315
  logger$1.debug("Could not resolve @alfe.ai/cli version");
22194
22316
  }
22195
22317
  /**
22196
- * Resolve the installed runtime (OpenClaw) version.
22197
- * 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.
22198
22344
  */
22199
- 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
+ }
22200
22351
  try {
22201
- const { stdout } = await execFileAsync("openclaw", ["--version"]);
22352
+ const { stdout } = await execFileAsync(cmd.command, cmd.args);
22202
22353
  return stdout.trim() || void 0;
22203
22354
  } catch {
22204
- logger$1.debug("Could not resolve openclaw runtime version");
22355
+ logger$1.debug({ runtime }, "Could not resolve runtime version");
22205
22356
  return;
22206
22357
  }
22207
22358
  }
@@ -22349,7 +22500,7 @@ async function startDaemon() {
22349
22500
  await flushAndExit(1);
22350
22501
  }
22351
22502
  resolvedCliVersion = await getCliVersion();
22352
- resolvedRuntimeVersion = await getRuntimeVersion();
22503
+ resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
22353
22504
  logger$1.info({
22354
22505
  cliVersion: resolvedCliVersion,
22355
22506
  runtimeVersion: resolvedRuntimeVersion
@@ -22382,7 +22533,14 @@ async function startDaemon() {
22382
22533
  home: runtimeCfg.workspace,
22383
22534
  agentWorkspace: runtimeCfg.agentWorkspace
22384
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");
22385
22542
  } else logger$1.warn({ runtime: name }, "Unknown runtime type — skipping");
22543
+ runtimeAppliersRef = runtimeAppliers;
22386
22544
  const integrationsService = new IntegrationsService(new AlfeApiClient({
22387
22545
  apiBaseUrl: config.apiEndpoint,
22388
22546
  getToken: () => Promise.resolve(config.apiKey)
@@ -22397,22 +22555,25 @@ async function startDaemon() {
22397
22555
  logger: logger$1,
22398
22556
  idleTtlMs: 0
22399
22557
  }, { connect: defaultConnect });
22400
- await mcpManager.loadIntoBundler(mcpBundler);
22401
- const warmBundler = (reason) => {
22402
- if (!mcpBundler) return;
22403
- mcpBundler.warmup().catch((err) => {
22404
- logger$1.warn({
22405
- err: err instanceof Error ? err.message : String(err),
22406
- reason
22407
- }, "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");
22408
22573
  });
22409
- };
22410
- warmBundler("startup");
22411
- mcpManager.onChange(() => {
22412
- warmBundler("store change");
22413
- });
22414
- logger$1.info("MCP bundler attached to manager — warming children");
22415
- await runOpenclawMcpCleanup({
22574
+ logger$1.info("MCP bundler attached to manager — warming children");
22575
+ }
22576
+ if (config.runtime === "openclaw") await runOpenclawMcpCleanup({
22416
22577
  manager: mcpManager,
22417
22578
  logger: logger$1
22418
22579
  }).catch((err) => {
@@ -22433,14 +22594,27 @@ async function startDaemon() {
22433
22594
  });
22434
22595
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
22435
22596
  cloudClient.setIntegrationManager(integrationAdapter);
22436
- cloudClient.setRuntimeRestartNeededHandler(() => {
22597
+ const requestRuntimeRestart = () => {
22437
22598
  if (runtimeProcess) {
22438
22599
  logger$1.info("Runtime state changed — restarting runtime");
22439
22600
  runtimeProcess.restart().catch((err) => {
22440
22601
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
22441
22602
  });
22442
22603
  }
22443
- });
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
+ }
22444
22618
  cloudClient.setOnReconciliationComplete(() => {
22445
22619
  try {
22446
22620
  const activeCommands = integrationManager.getActiveCommands();
@@ -22495,6 +22669,10 @@ async function startDaemon() {
22495
22669
  await runtimeProcess.stop();
22496
22670
  logger$1.debug("Runtime process stopped");
22497
22671
  }
22672
+ if (hermesMcpSync) {
22673
+ hermesMcpSync.stop();
22674
+ hermesMcpSync = null;
22675
+ }
22498
22676
  if (mcpBundler) {
22499
22677
  logger$1.debug("Stopping MCP bundler...");
22500
22678
  await mcpBundler.dispose();
@@ -22583,17 +22761,28 @@ async function handleCloudCommand(command) {
22583
22761
  message: "Runtime upgrade already in progress"
22584
22762
  }
22585
22763
  };
22586
- 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);
22587
22767
  upgradingRuntime = true;
22588
22768
  setTimeout(() => {
22589
22769
  (async () => {
22590
22770
  try {
22591
22771
  const { upgradeRuntime } = await import("./runtime-upgrade.js");
22592
- if ((await upgradeRuntime(version, runtimeProcess)).success) {
22593
- resolvedRuntimeVersion = await getRuntimeVersion();
22594
- if (!resolvedRuntimeVersion) logger$1.warn({ requestedVersion: version }, "Could not detect runtime version after upgrade — using requested version");
22595
- logger$1.info({ runtimeVersion: resolvedRuntimeVersion ?? version }, "Runtime version updated after upgrade");
22596
- 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");
22597
22786
  }
22598
22787
  } catch (err) {
22599
22788
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Runtime upgrade failed");
@@ -22608,6 +22797,7 @@ async function handleCloudCommand(command) {
22608
22797
  status: "ok",
22609
22798
  result: {
22610
22799
  upgrading: true,
22800
+ runtime,
22611
22801
  version
22612
22802
  }
22613
22803
  };
@@ -22688,17 +22878,30 @@ async function handleCloudCommand(command) {
22688
22878
  message: "alfe.config_set requires key and value"
22689
22879
  }
22690
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
+ }
22691
22899
  try {
22692
- await execFileAsync("openclaw", [
22693
- "config",
22694
- "set",
22695
- key,
22696
- value
22697
- ], { timeout: 1e4 });
22900
+ await applier.setConfigRaw(key, value);
22698
22901
  logger$1.info({
22699
- key,
22700
- value
22701
- }, "Applied config via openclaw config set");
22902
+ runtime,
22903
+ key
22904
+ }, "Applied config via runtime applier setConfigRaw");
22702
22905
  return {
22703
22906
  type: "COMMAND_ACK",
22704
22907
  commandId: command.commandId,
@@ -22712,9 +22915,9 @@ async function handleCloudCommand(command) {
22712
22915
  const message = err instanceof Error ? err.message : String(err);
22713
22916
  logger$1.error({
22714
22917
  err: message,
22715
- key,
22716
- value
22717
- }, "Failed to apply config via openclaw");
22918
+ runtime,
22919
+ key
22920
+ }, "Failed to apply config via runtime applier");
22718
22921
  return {
22719
22922
  type: "COMMAND_ACK",
22720
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.4",
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.2",
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.5",
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",