@alfe.ai/gateway 0.1.5 → 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/README.md CHANGED
@@ -161,3 +161,8 @@ See `src/protocol.ts` for full type definitions.
161
161
 
162
162
  - Dashboard integration management (backend wired, UI in progress)
163
163
  - Voice migration to integration model (`packages/openclaw-voice/` exists, not yet full integration)
164
+
165
+ ## Links
166
+
167
+ - 🌐 Website: <https://alfe.ai>
168
+ - 📚 Docs: <https://docs.alfe.ai>
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";
@@ -495,6 +495,15 @@ var AuthService = class {
495
495
  getSubscriptionStatus() {
496
496
  return this.client.request(`${this.prefix}/subscription`);
497
497
  }
498
+ /**
499
+ * Mint a short-lived, single-use Clerk sign-in token for the calling user so
500
+ * a native client can hand its logged-in session to the web dashboard
501
+ * (`${dashboard}/handoff?__clerk_ticket=<token>`). Server mints only for the
502
+ * authenticated user — never a caller-supplied id.
503
+ */
504
+ createHandoffToken() {
505
+ return this.client.request(`${this.prefix}/handoff-token`, { method: "POST" });
506
+ }
498
507
  applyForStartup(input) {
499
508
  return this.client.request(`${this.prefix}/subscription/apply-startup`, {
500
509
  method: "POST",
@@ -566,10 +575,8 @@ var AuthService = class {
566
575
  //#region ../../packages-internal/api-client/dist/services/integrations.js
567
576
  var IntegrationsService = class {
568
577
  client;
569
- voiceClient;
570
- constructor(client, voiceClient) {
578
+ constructor(client) {
571
579
  this.client = client;
572
- this.voiceClient = voiceClient;
573
580
  }
574
581
  listScopedInstalls(scope, scopeId) {
575
582
  const params = new URLSearchParams({
@@ -618,8 +625,11 @@ var IntegrationsService = class {
618
625
  });
619
626
  }
620
627
  listIntegrations(agentId, options) {
621
- const qs = options?.includeInherited ? "?includeInherited=true" : "";
622
- return this.client.request(`/integrations/agents/${agentId}${qs}`);
628
+ const params = new URLSearchParams();
629
+ if (options?.includeInherited) params.set("includeInherited", "true");
630
+ if (options?.effective) params.set("effective", "true");
631
+ const qs = params.toString();
632
+ return this.client.request(`/integrations/agents/${agentId}${qs ? `?${qs}` : ""}`);
623
633
  }
624
634
  installIntegration(agentId, data) {
625
635
  return this.client.request(`/integrations/agents/${agentId}`, {
@@ -651,10 +661,6 @@ var IntegrationsService = class {
651
661
  body: JSON.stringify({ agentId })
652
662
  });
653
663
  }
654
- listVoices() {
655
- if (this.voiceClient) return this.voiceClient.request("/api/voices");
656
- return this.client.request("/integrations/voices");
657
- }
658
664
  getDiscordGuildChannels(guildId) {
659
665
  return this.client.request(`/discord/guilds/${encodeURIComponent(guildId)}/channels`);
660
666
  }
@@ -4480,7 +4486,17 @@ enumValues({
4480
4486
  });
4481
4487
  enumValues({
4482
4488
  OpenClaw: "openclaw",
4483
- NanoClaw: "nanoclaw"
4489
+ NanoClaw: "nanoclaw",
4490
+ Hermes: "hermes"
4491
+ });
4492
+ const TTS_MODELS = enumValues({
4493
+ ElevenLabsTurbo: "elevenlabs-turbo",
4494
+ ElevenLabsStandard: "elevenlabs-standard"
4495
+ });
4496
+ object({
4497
+ voiceId: string().regex(/^[A-Za-z0-9]{8,64}$/).optional(),
4498
+ ttsModel: _enum(TTS_MODELS).optional(),
4499
+ enabled: boolean().optional()
4484
4500
  });
4485
4501
  enumValues({
4486
4502
  Org: "org",
@@ -4768,10 +4784,12 @@ const NotificationType = {
4768
4784
  SubscriptionCancelled: "subscription.cancelled",
4769
4785
  SubscriptionPastDue: "subscription.past_due",
4770
4786
  BalanceThresholdWarning: "balance.threshold.warning",
4787
+ PlatformTierPriceIncrease: "platform.tier_price_increase",
4771
4788
  AgentCreated: "agent.created",
4772
4789
  AgentProvisionFailed: "agent.provision.failed",
4773
4790
  AgentBillingSuspended: "agent.billing.suspended",
4774
4791
  AgentDisconnectedExtended: "agent.disconnected.extended",
4792
+ BrowserTakeoverRequested: "browser_takeover_requested",
4775
4793
  InviteCreated: "onboarding.invite.created",
4776
4794
  OrgClaimed: "onboarding.org.claimed",
4777
4795
  TeamMemberAdded: "team.member.added",
@@ -4788,7 +4806,7 @@ const RecipientStrategy = {
4788
4806
  TenantAdmins: "tenant_admins",
4789
4807
  SpecificUser: "specific_user"
4790
4808
  };
4791
- NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
4809
+ NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
4792
4810
  //#endregion
4793
4811
  //#region src/config.ts
4794
4812
  /**
@@ -4965,17 +4983,33 @@ async function loadManagedConfig() {
4965
4983
  agentId: identity.agentId,
4966
4984
  orgId: identity.orgId,
4967
4985
  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
- })() : {},
4986
+ runtimes: buildManagedRuntimes(identity.runtime),
4975
4987
  autoStartRuntime: true
4976
4988
  };
4977
4989
  }
4978
4990
  /**
4991
+ * Build the managed-mode `runtimes` map for a single resolved runtime.
4992
+ * Returns an empty map for unknown runtimes (the daemon then skips runtime
4993
+ * start rather than crashing).
4994
+ */
4995
+ function buildManagedRuntimes(runtime) {
4996
+ if (runtime === "openclaw") {
4997
+ const home = join(homedir(), ".openclaw");
4998
+ return { openclaw: {
4999
+ workspace: home,
5000
+ agentWorkspace: deriveAgentWorkspace("openclaw", home)
5001
+ } };
5002
+ }
5003
+ if (runtime === "hermes") {
5004
+ const home = join(homedir(), ".hermes");
5005
+ return { hermes: {
5006
+ workspace: home,
5007
+ agentWorkspace: deriveAgentWorkspace("hermes", home)
5008
+ } };
5009
+ }
5010
+ return {};
5011
+ }
5012
+ /**
4979
5013
  * Load full daemon configuration.
4980
5014
  * Reads config.toml, validates the API key, resolves agent identity,
4981
5015
  * and parses runtime configurations.
@@ -4994,6 +5028,7 @@ async function loadDaemonConfig() {
4994
5028
  const identity = await resolveAgentIdentity(alfeConfig.api_key, apiEndpoint);
4995
5029
  const gatewayWsUrl = alfeConfig.gateway_url ?? deriveGatewayWsUrl(apiEndpoint);
4996
5030
  const runtimes = await loadRuntimeConfigs();
5031
+ const runtime = alfeConfig.runtime ?? identity.runtime;
4997
5032
  return {
4998
5033
  apiKey: alfeConfig.api_key,
4999
5034
  apiEndpoint,
@@ -5002,7 +5037,7 @@ async function loadDaemonConfig() {
5002
5037
  pidPath: PID_PATH,
5003
5038
  agentId: identity.agentId,
5004
5039
  orgId: identity.orgId,
5005
- runtime: identity.runtime,
5040
+ runtime,
5006
5041
  runtimes,
5007
5042
  autoStartRuntime: alfeConfig.auto_start_runtime ?? true
5008
5043
  };
@@ -5029,6 +5064,7 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
5029
5064
  const wsResult = await wsResponse.json();
5030
5065
  const templateKey = wsResult.data?.templateKey;
5031
5066
  const defaultModel = wsResult.data?.defaultModel ?? void 0;
5067
+ const installedFrom = wsResult.data?.installedFrom;
5032
5068
  if (!templateKey) {
5033
5069
  logger$1.debug("No templateKey in workspace response");
5034
5070
  return defaultModel ? {
@@ -5036,8 +5072,14 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
5036
5072
  files: {}
5037
5073
  } : null;
5038
5074
  }
5039
- logger$1.debug({ templateKey }, "Fetching template files...");
5040
- const filesResponse = await fetch(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`, {
5075
+ const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
5076
+ const filesUrl = new URL(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`);
5077
+ if (pinnedVersion !== void 0) filesUrl.searchParams.set("version", String(pinnedVersion));
5078
+ logger$1.debug({
5079
+ templateKey,
5080
+ version: pinnedVersion
5081
+ }, "Fetching template files...");
5082
+ const filesResponse = await fetch(filesUrl.toString(), {
5041
5083
  method: "GET",
5042
5084
  headers: { "Authorization": `Bearer ${apiKey}` }
5043
5085
  });
@@ -5049,16 +5091,18 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
5049
5091
  files: {}
5050
5092
  };
5051
5093
  }
5052
- const filesResult = await filesResponse.json();
5053
- const fileCount = Object.keys(filesResult.data?.files ?? {}).length;
5094
+ const files = (await filesResponse.json()).data?.files ?? {};
5095
+ const fileCount = Object.keys(files).length;
5054
5096
  logger$1.debug({
5055
5097
  templateKey,
5098
+ version: pinnedVersion,
5056
5099
  fileCount
5057
5100
  }, "Template files fetched");
5058
5101
  return {
5059
5102
  templateKey,
5060
5103
  defaultModel,
5061
- files: filesResult.data?.files ?? {}
5104
+ files,
5105
+ personaFiles: Object.keys(files)
5062
5106
  };
5063
5107
  } catch (err) {
5064
5108
  logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
@@ -21617,6 +21661,10 @@ var RuntimeProcess = class {
21617
21661
  "--allow-unconfigured"
21618
21662
  ]
21619
21663
  };
21664
+ case "hermes": return {
21665
+ command: "hermes",
21666
+ args: ["gateway", "run"]
21667
+ };
21620
21668
  default: throw new Error(`Unsupported runtime: ${this.options.runtime}`);
21621
21669
  }
21622
21670
  }
@@ -22195,6 +22243,12 @@ let startedAt;
22195
22243
  let integrationManager;
22196
22244
  let mcpBundler = null;
22197
22245
  let mcpManagerRef = null;
22246
+ /**
22247
+ * Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
22248
+ * store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
22249
+ * and for managed/self-hosted runtimes other than hermes.
22250
+ */
22251
+ let hermesMcpSync = null;
22198
22252
  let aiProxyServer = null;
22199
22253
  let runtimeProcess = null;
22200
22254
  let aiProxyUrl = null;
@@ -22207,6 +22261,12 @@ let resolvedRuntimeVersion;
22207
22261
  let upgradingRuntime = false;
22208
22262
  let stopPairingApprovalPoller = null;
22209
22263
  /**
22264
+ * Module-level handle on the runtime appliers built during start(), so the
22265
+ * module-scoped command handler can route `alfe.config_set` to the active
22266
+ * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
22267
+ */
22268
+ let runtimeAppliersRef = null;
22269
+ /**
22210
22270
  * Resolve the installed @alfe.ai/cli version.
22211
22271
  *
22212
22272
  * Strategy (in order):
@@ -22281,15 +22341,44 @@ async function getCliVersion() {
22281
22341
  logger$1.debug("Could not resolve @alfe.ai/cli version");
22282
22342
  }
22283
22343
  /**
22284
- * Resolve the installed runtime (OpenClaw) version.
22285
- * Runs `openclaw --version` and returns the trimmed output.
22344
+ * Per-runtime "print installed version" commands. Local to the gateway — we do
22345
+ * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
22346
+ * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
22347
+ * connection-status report degrades gracefully instead of crashing.
22348
+ */
22349
+ const RUNTIME_VERSION_COMMANDS = {
22350
+ openclaw: {
22351
+ command: "openclaw",
22352
+ args: ["--version"]
22353
+ },
22354
+ hermes: {
22355
+ command: "hermes",
22356
+ args: ["version"]
22357
+ }
22358
+ };
22359
+ /**
22360
+ * Pure resolver (exported for tests) — the per-runtime version command, or
22361
+ * `undefined` for an unknown runtime.
22362
+ */
22363
+ function resolveRuntimeVersionCommand(runtime) {
22364
+ return RUNTIME_VERSION_COMMANDS[runtime];
22365
+ }
22366
+ /**
22367
+ * Resolve the installed runtime version by running the per-runtime version
22368
+ * command and returning the trimmed stdout. An unknown runtime (no command in
22369
+ * the map) returns `undefined` without spawning — it must never throw.
22286
22370
  */
22287
- async function getRuntimeVersion() {
22371
+ async function getRuntimeVersion(runtime) {
22372
+ const cmd = resolveRuntimeVersionCommand(runtime);
22373
+ if (!cmd) {
22374
+ logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
22375
+ return;
22376
+ }
22288
22377
  try {
22289
- const { stdout } = await execFileAsync("openclaw", ["--version"]);
22378
+ const { stdout } = await execFileAsync(cmd.command, cmd.args);
22290
22379
  return stdout.trim() || void 0;
22291
22380
  } catch {
22292
- logger$1.debug("Could not resolve openclaw runtime version");
22381
+ logger$1.debug({ runtime }, "Could not resolve runtime version");
22293
22382
  return;
22294
22383
  }
22295
22384
  }
@@ -22437,7 +22526,7 @@ async function startDaemon() {
22437
22526
  await flushAndExit(1);
22438
22527
  }
22439
22528
  resolvedCliVersion = await getCliVersion();
22440
- resolvedRuntimeVersion = await getRuntimeVersion();
22529
+ resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
22441
22530
  logger$1.info({
22442
22531
  cliVersion: resolvedCliVersion,
22443
22532
  runtimeVersion: resolvedRuntimeVersion
@@ -22470,7 +22559,14 @@ async function startDaemon() {
22470
22559
  home: runtimeCfg.workspace,
22471
22560
  agentWorkspace: runtimeCfg.agentWorkspace
22472
22561
  }, "Registered OpenClaw runtime applier");
22562
+ } else if (name === "hermes") {
22563
+ runtimeAppliers.set(name, new HermesApplier({ home: runtimeCfg.workspace }));
22564
+ logger$1.info({
22565
+ runtime: name,
22566
+ home: runtimeCfg.workspace
22567
+ }, "Registered Hermes runtime applier");
22473
22568
  } else logger$1.warn({ runtime: name }, "Unknown runtime type — skipping");
22569
+ runtimeAppliersRef = runtimeAppliers;
22474
22570
  const integrationsService = new IntegrationsService(new AlfeApiClient({
22475
22571
  apiBaseUrl: config.apiEndpoint,
22476
22572
  getToken: () => Promise.resolve(config.apiKey)
@@ -22485,22 +22581,25 @@ async function startDaemon() {
22485
22581
  logger: logger$1,
22486
22582
  idleTtlMs: 0
22487
22583
  }, { 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");
22584
+ if (config.runtime === "hermes") logger$1.info("Hermes runtime — daemon MCP bundler left idle (store mirrored to config.yaml by HermesMcpSync)");
22585
+ else {
22586
+ await mcpManager.loadIntoBundler(mcpBundler);
22587
+ const warmBundler = (reason) => {
22588
+ if (!mcpBundler) return;
22589
+ mcpBundler.warmup().catch((err) => {
22590
+ logger$1.warn({
22591
+ err: err instanceof Error ? err.message : String(err),
22592
+ reason
22593
+ }, "MCP bundler warmup failed");
22594
+ });
22595
+ };
22596
+ warmBundler("startup");
22597
+ mcpManager.onChange(() => {
22598
+ warmBundler("store change");
22496
22599
  });
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({
22600
+ logger$1.info("MCP bundler attached to manager — warming children");
22601
+ }
22602
+ if (config.runtime === "openclaw") await runOpenclawMcpCleanup({
22504
22603
  manager: mcpManager,
22505
22604
  logger: logger$1
22506
22605
  }).catch((err) => {
@@ -22521,14 +22620,27 @@ async function startDaemon() {
22521
22620
  });
22522
22621
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
22523
22622
  cloudClient.setIntegrationManager(integrationAdapter);
22524
- cloudClient.setRuntimeRestartNeededHandler(() => {
22623
+ const requestRuntimeRestart = () => {
22525
22624
  if (runtimeProcess) {
22526
22625
  logger$1.info("Runtime state changed — restarting runtime");
22527
22626
  runtimeProcess.restart().catch((err) => {
22528
22627
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
22529
22628
  });
22530
22629
  }
22531
- });
22630
+ };
22631
+ cloudClient.setRuntimeRestartNeededHandler(requestRuntimeRestart);
22632
+ if (config.runtime === "hermes") {
22633
+ const hermesCfg = config.runtimes[config.runtime];
22634
+ if (hermesCfg) {
22635
+ hermesMcpSync = new HermesMcpSync({
22636
+ manager: mcpManager,
22637
+ home: hermesCfg.workspace,
22638
+ apiKey: config.apiKey,
22639
+ requestRestart: requestRuntimeRestart
22640
+ });
22641
+ hermesMcpSync.start();
22642
+ }
22643
+ }
22532
22644
  cloudClient.setOnReconciliationComplete(() => {
22533
22645
  try {
22534
22646
  const activeCommands = integrationManager.getActiveCommands();
@@ -22583,6 +22695,10 @@ async function startDaemon() {
22583
22695
  await runtimeProcess.stop();
22584
22696
  logger$1.debug("Runtime process stopped");
22585
22697
  }
22698
+ if (hermesMcpSync) {
22699
+ hermesMcpSync.stop();
22700
+ hermesMcpSync = null;
22701
+ }
22586
22702
  if (mcpBundler) {
22587
22703
  logger$1.debug("Stopping MCP bundler...");
22588
22704
  await mcpBundler.dispose();
@@ -22671,17 +22787,28 @@ async function handleCloudCommand(command) {
22671
22787
  message: "Runtime upgrade already in progress"
22672
22788
  }
22673
22789
  };
22674
- const version = command.payload?.version ?? "2026.6.8";
22790
+ const payload = command.payload;
22791
+ const runtime = config.runtime;
22792
+ const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.8" : void 0);
22675
22793
  upgradingRuntime = true;
22676
22794
  setTimeout(() => {
22677
22795
  (async () => {
22678
22796
  try {
22679
22797
  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);
22798
+ if ((await upgradeRuntime(runtime, version, runtimeProcess)).success) {
22799
+ resolvedRuntimeVersion = await getRuntimeVersion(runtime);
22800
+ const reportedVersion = resolvedRuntimeVersion ?? version;
22801
+ if (!resolvedRuntimeVersion) logger$1.warn({
22802
+ runtime,
22803
+ requestedVersion: version
22804
+ }, "Could not detect runtime version after upgrade");
22805
+ if (reportedVersion) {
22806
+ logger$1.info({
22807
+ runtime,
22808
+ runtimeVersion: reportedVersion
22809
+ }, "Runtime version updated after upgrade");
22810
+ cloudClient.updateRuntimeVersionAndReRegister(reportedVersion);
22811
+ } else logger$1.warn({ runtime }, "No runtime version to report after upgrade — skipping re-register");
22685
22812
  }
22686
22813
  } catch (err) {
22687
22814
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Runtime upgrade failed");
@@ -22696,6 +22823,7 @@ async function handleCloudCommand(command) {
22696
22823
  status: "ok",
22697
22824
  result: {
22698
22825
  upgrading: true,
22826
+ runtime,
22699
22827
  version
22700
22828
  }
22701
22829
  };
@@ -22776,17 +22904,30 @@ async function handleCloudCommand(command) {
22776
22904
  message: "alfe.config_set requires key and value"
22777
22905
  }
22778
22906
  };
22907
+ const runtime = config.runtime;
22908
+ const applier = runtimeAppliersRef?.get(runtime);
22909
+ if (!applier || typeof applier.setConfigRaw !== "function") {
22910
+ const message = `No runtime applier supports config_set for runtime "${runtime}"`;
22911
+ logger$1.warn({
22912
+ runtime,
22913
+ key
22914
+ }, message);
22915
+ return {
22916
+ type: "COMMAND_ACK",
22917
+ commandId: command.commandId,
22918
+ status: "error",
22919
+ result: {
22920
+ code: "CONFIG_SET_UNSUPPORTED",
22921
+ message
22922
+ }
22923
+ };
22924
+ }
22779
22925
  try {
22780
- await execFileAsync("openclaw", [
22781
- "config",
22782
- "set",
22783
- key,
22784
- value
22785
- ], { timeout: 1e4 });
22926
+ await applier.setConfigRaw(key, value);
22786
22927
  logger$1.info({
22787
- key,
22788
- value
22789
- }, "Applied config via openclaw config set");
22928
+ runtime,
22929
+ key
22930
+ }, "Applied config via runtime applier setConfigRaw");
22790
22931
  return {
22791
22932
  type: "COMMAND_ACK",
22792
22933
  commandId: command.commandId,
@@ -22800,9 +22941,9 @@ async function handleCloudCommand(command) {
22800
22941
  const message = err instanceof Error ? err.message : String(err);
22801
22942
  logger$1.error({
22802
22943
  err: message,
22803
- key,
22804
- value
22805
- }, "Failed to apply config via openclaw");
22944
+ runtime,
22945
+ key
22946
+ }, "Failed to apply config via runtime applier");
22806
22947
  return {
22807
22948
  type: "COMMAND_ACK",
22808
22949
  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
  }
@@ -60,6 +60,18 @@ interface AgentWorkspaceConfig {
60
60
  templateKey?: string;
61
61
  defaultModel?: string;
62
62
  files: Record<string, string>;
63
+ /**
64
+ * Filenames within `files` that are agent-owned persona files (SOUL.md,
65
+ * IDENTITY.md, AGENTS.md, USER.md, BOOTSTRAP.md, …). The consumer
66
+ * (`writeWorkspaceFiles`) seeds these ONLY IF ABSENT so an agent's own edits
67
+ * — restored by the openclaw-sync backup on a fresh VM — are never clobbered
68
+ * by a setup-time re-seed. Filenames NOT in this set keep overwrite-on-write
69
+ * semantics. The templates `/files` endpoint returns persona files
70
+ * exclusively today, so `fetchAgentConfig` populates this with every key; the
71
+ * field is threaded explicitly rather than assumed so a future workspace-file
72
+ * source can mix persona (seed-if-missing) and non-persona (overwrite) files.
73
+ */
74
+ personaFiles?: string[];
63
75
  }
64
76
  declare const ALFE_DIR: string;
65
77
  declare const SOCKET_PATH: string;
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.1",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,14 +22,22 @@
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",
30
- "@alfe.ai/mcp-bundler": "^0.2.1"
25
+ "@alfe.ai/agent-api-client": "^0.4.0",
26
+ "@alfe.ai/ai-proxy-local": "^0.0.12",
27
+ "@alfe.ai/config": "^0.2.0",
28
+ "@alfe.ai/integration-manifest": "^0.3.1",
29
+ "@alfe.ai/integrations": "^0.2.4",
30
+ "@alfe.ai/mcp-bundler": "^0.2.2"
31
31
  },
32
32
  "license": "UNLICENSED",
33
+ "homepage": "https://alfe.ai",
34
+ "author": "Alfe (https://alfe.ai)",
35
+ "keywords": [
36
+ "alfe",
37
+ "ai-agents",
38
+ "agent",
39
+ "llm"
40
+ ],
33
41
  "scripts": {
34
42
  "build": "tsdown",
35
43
  "dev": "tsdown --watch",