@alfe.ai/gateway 0.9.4 → 0.9.6

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
@@ -1,21 +1,21 @@
1
1
  import { n as logger$1 } from "./logger.js";
2
2
  import { a as captureFatal, c as captureRuntimeCrash, d as initAgentSentry, f as setAgentContext, l as captureRuntimeErrorOutput, o as captureIntegrationFailure, s as captureMcpFailure, u as flushSentry } from "./sentry.js";
3
3
  import { createRequire } from "node:module";
4
- import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
5
- import { execFile, execSync, spawn } from "node:child_process";
6
- import { promisify } from "node:util";
4
+ import { chmod, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
7
5
  import { dirname, join } from "node:path";
8
6
  import { homedir } from "node:os";
9
7
  import pino from "pino";
10
8
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
9
  import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
12
10
  import crypto from "crypto";
11
+ import { AgentApiClient } from "@alfe.ai/agent-api-client";
13
12
  import { parse } from "smol-toml";
14
13
  import WebSocket from "ws";
15
- import { createConnection, createServer } from "node:net";
14
+ import { execFile, execSync, spawn } from "node:child_process";
15
+ import { promisify } from "node:util";
16
16
  import { ClaudeCodeApplier, ClaudeCodeMcpSync, HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
17
- import { AgentApiClient } from "@alfe.ai/agent-api-client";
18
- import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
17
+ import { Manager, McpBundler, defaultConnect, serverLaunchFingerprint } from "@alfe.ai/mcp-bundler";
18
+ import { createConnection, createServer } from "node:net";
19
19
  import stream, { Readable } from "stream";
20
20
  import util, { format } from "util";
21
21
  import http from "http";
@@ -94,7 +94,7 @@ const PINNED_OPENCLAW_VERSION = "2026.6.11";
94
94
  * `PINNED_OPENCLAW_VERSION` — there is no `services/compute/Dockerfile` mirror to
95
95
  * keep in sync.
96
96
  */
97
- const PINNED_CLAUDE_CODE_HOST_VERSION = "0.1.3";
97
+ const PINNED_CLAUDE_CODE_HOST_VERSION = "0.1.6";
98
98
  //#endregion
99
99
  //#region ../../packages-internal/ids/dist/prefixes.js
100
100
  const ID_PREFIXES = {
@@ -110,6 +110,7 @@ const ID_PREFIXES = {
110
110
  promoCode: "prc",
111
111
  promoRedemption: "prr",
112
112
  pendingPromo: "pdp",
113
+ remoteSession: "rms",
113
114
  webhook: "whk",
114
115
  webhookDelivery: "wdl",
115
116
  onboardingSession: "obs",
@@ -298,7 +299,7 @@ var AlfeApiClient = class {
298
299
  getToken;
299
300
  onAuthFailure;
300
301
  constructor(options) {
301
- this.apiBaseUrl = options.apiBaseUrl;
302
+ this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
302
303
  this.getToken = options.getToken;
303
304
  this.onAuthFailure = options.onAuthFailure;
304
305
  }
@@ -315,7 +316,7 @@ var AlfeApiClient = class {
315
316
  try {
316
317
  const url = `${this.apiBaseUrl}${path}`;
317
318
  const headers = new Headers(options?.headers);
318
- headers.set("Content-Type", "application/json");
319
+ if (typeof options?.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
319
320
  headers.set("x-correlation-id", correlationId());
320
321
  if (!skipAuth) {
321
322
  const token = await this.getToken();
@@ -347,15 +348,24 @@ var AlfeApiClient = class {
347
348
  }
348
349
  };
349
350
  }
350
- const body = await res.json();
351
- if (!res.ok) return {
352
- ok: false,
353
- result: {
351
+ const text = res.status === 204 ? "" : await res.text();
352
+ let body;
353
+ if (text) try {
354
+ body = JSON.parse(text);
355
+ } catch {
356
+ body = text;
357
+ }
358
+ if (!res.ok) {
359
+ const errBody = body;
360
+ return {
354
361
  ok: false,
355
- error: body.message || `API error: ${String(res.status)}`,
356
- status: res.status
357
- }
358
- };
362
+ result: {
363
+ ok: false,
364
+ error: (typeof errBody === "object" ? errBody.message : void 0) ?? (typeof body === "string" ? body : `API error: ${String(res.status)}`),
365
+ status: res.status
366
+ }
367
+ };
368
+ }
359
369
  return {
360
370
  ok: true,
361
371
  res,
@@ -378,6 +388,10 @@ var AlfeApiClient = class {
378
388
  async request(path, options) {
379
389
  const result = await this._fetch(path, options);
380
390
  if (!result.ok) return result.result;
391
+ if (result.body === void 0) return {
392
+ ok: true,
393
+ data: void 0
394
+ };
381
395
  return {
382
396
  ok: true,
383
397
  data: result.body.data
@@ -392,6 +406,10 @@ var AlfeApiClient = class {
392
406
  async publicRequest(path, options) {
393
407
  const result = await this._fetch(path, options, true);
394
408
  if (!result.ok) return result.result;
409
+ if (result.body === void 0) return {
410
+ ok: true,
411
+ data: void 0
412
+ };
395
413
  return {
396
414
  ok: true,
397
415
  data: result.body.data
@@ -422,7 +440,9 @@ var AlfeApiClient = class {
422
440
  * (both need `readonly [string, ...string[]]`).
423
441
  */
424
442
  function enumValues(obj) {
425
- return Object.values(obj);
443
+ const values = Object.values(obj);
444
+ if (values.length === 0) throw new TypeError("enumValues requires at least one value");
445
+ return values;
426
446
  }
427
447
  Object.freeze({ status: "aborted" });
428
448
  function $constructor(name, initializer, params) {
@@ -4105,6 +4125,12 @@ function refine(fn, _params = {}) {
4105
4125
  function superRefine(fn) {
4106
4126
  return /* @__PURE__ */ _superRefine(fn);
4107
4127
  }
4128
+ //#endregion
4129
+ //#region ../../packages-internal/types/dist/lib/usd.js
4130
+ function isWholeUsdCentAmount(amountUsd) {
4131
+ const roundedCents = Math.round(amountUsd * 100);
4132
+ return Math.abs(amountUsd - roundedCents / 100) < 1e-9;
4133
+ }
4108
4134
  enumValues({
4109
4135
  Active: "active",
4110
4136
  PastDue: "past_due",
@@ -4133,6 +4159,7 @@ enumValues({
4133
4159
  Month: "month",
4134
4160
  Year: "year"
4135
4161
  });
4162
+ number().min(0).max(1e6).refine(isWholeUsdCentAmount, "Stripe price amounts cannot contain fractional cents");
4136
4163
  object({
4137
4164
  gracePeriodDays: number().int().positive().optional(),
4138
4165
  launchDiscountEnabled: boolean().optional()
@@ -4268,6 +4295,7 @@ enumValues({
4268
4295
  Completed: "completed",
4269
4296
  Failed: "failed"
4270
4297
  });
4298
+ number().min(.01).max(1e6).refine(isWholeUsdCentAmount, "discountAmountUsd cannot contain fractional cents");
4271
4299
  enumValues({
4272
4300
  BalanceCredit: "balance_credit",
4273
4301
  SubscriptionDiscount: "subscription_discount",
@@ -4448,7 +4476,7 @@ _enum(XAI_MODELS);
4448
4476
  _enum(ZHIPU_MODELS);
4449
4477
  _enum(MOONSHOT_MODELS);
4450
4478
  _enum(QWEN_MODELS);
4451
- string().min(1);
4479
+ string().trim().min(1);
4452
4480
  AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4453
4481
  AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4454
4482
  enumValues({
@@ -4553,7 +4581,7 @@ const RecipientStrategy = {
4553
4581
  TenantAdmins: "tenant_admins",
4554
4582
  SpecificUser: "specific_user"
4555
4583
  };
4556
- 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.StartupGrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.GrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.EnterpriseGrantEnded, 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;
4584
+ 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.StartupGrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.GrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.EnterpriseGrantEnded, 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.TenantAdmins, 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;
4557
4585
  //#endregion
4558
4586
  //#region ../../packages-internal/api-client/dist/services/auth.js
4559
4587
  var AuthService = class {
@@ -4578,7 +4606,7 @@ var AuthService = class {
4578
4606
  });
4579
4607
  }
4580
4608
  deleteToken(tokenId) {
4581
- return this.client.request(`${this.prefix}/tokens/${tokenId}`, { method: "DELETE" });
4609
+ return this.client.request(`${this.prefix}/tokens/${encodeURIComponent(tokenId)}`, { method: "DELETE" });
4582
4610
  }
4583
4611
  /**
4584
4612
  * Start a device-code flow. Called by the CLI when the user runs
@@ -4798,6 +4826,12 @@ var IntegrationsService = class {
4798
4826
  constructor(client) {
4799
4827
  this.client = client;
4800
4828
  }
4829
+ agentIntegrationsPath(agentId) {
4830
+ return `/integrations/agents/${encodeURIComponent(agentId)}`;
4831
+ }
4832
+ agentIntegrationPath(agentId, integrationId) {
4833
+ return `${this.agentIntegrationsPath(agentId)}/${encodeURIComponent(integrationId)}`;
4834
+ }
4801
4835
  listScopedInstalls(scope, scopeId) {
4802
4836
  const params = new URLSearchParams({
4803
4837
  scope,
@@ -4846,31 +4880,31 @@ var IntegrationsService = class {
4846
4880
  }
4847
4881
  listIntegrations(agentId, options) {
4848
4882
  const params = new URLSearchParams();
4849
- if (options?.includeInherited) params.set("includeInherited", "true");
4850
- if (options?.effective) params.set("effective", "true");
4883
+ if (options?.includeInherited !== void 0) params.set("includeInherited", String(options.includeInherited));
4884
+ if (options?.effective !== void 0) params.set("effective", String(options.effective));
4851
4885
  const qs = params.toString();
4852
- return this.client.request(`/integrations/agents/${agentId}${qs ? `?${qs}` : ""}`);
4886
+ return this.client.request(`${this.agentIntegrationsPath(agentId)}${qs ? `?${qs}` : ""}`);
4853
4887
  }
4854
4888
  installIntegration(agentId, data) {
4855
- return this.client.request(`/integrations/agents/${agentId}`, {
4889
+ return this.client.request(this.agentIntegrationsPath(agentId), {
4856
4890
  method: "POST",
4857
4891
  body: JSON.stringify(data)
4858
4892
  });
4859
4893
  }
4860
4894
  getIntegrationConfig(agentId, integrationId) {
4861
- return this.client.request(`/integrations/agents/${agentId}/${integrationId}/config`);
4895
+ return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/config`);
4862
4896
  }
4863
4897
  updateIntegration(agentId, integrationId, data) {
4864
- return this.client.request(`/integrations/agents/${agentId}/${integrationId}`, {
4898
+ return this.client.request(this.agentIntegrationPath(agentId, integrationId), {
4865
4899
  method: "PATCH",
4866
4900
  body: JSON.stringify(data)
4867
4901
  });
4868
4902
  }
4869
4903
  removeIntegration(agentId, integrationId) {
4870
- return this.client.request(`/integrations/agents/${agentId}/${integrationId}`, { method: "DELETE" });
4904
+ return this.client.request(this.agentIntegrationPath(agentId, integrationId), { method: "DELETE" });
4871
4905
  }
4872
4906
  reinstallIntegration(agentId, integrationId) {
4873
- return this.client.request(`/integrations/agents/${agentId}/${integrationId}/reinstall`, { method: "POST" });
4907
+ return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/reinstall`, { method: "POST" });
4874
4908
  }
4875
4909
  /**
4876
4910
  * Upgrade an integration to the registry's latest version via the fast,
@@ -4879,7 +4913,7 @@ var IntegrationsService = class {
4879
4913
  * {@link reinstallIntegration}, which is the destructive repair path.
4880
4914
  */
4881
4915
  upgradeIntegration(agentId, integrationId) {
4882
- return this.client.request(`/integrations/agents/${agentId}/${integrationId}/upgrade`, { method: "POST" });
4916
+ return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/upgrade`, { method: "POST" });
4883
4917
  }
4884
4918
  getRegistry() {
4885
4919
  return this.client.request("/integrations/registry");
@@ -4890,17 +4924,16 @@ var IntegrationsService = class {
4890
4924
  body: JSON.stringify({ agentId })
4891
4925
  });
4892
4926
  }
4893
- getDiscordGuildChannels(guildId) {
4894
- return this.client.request(`/discord/guilds/${encodeURIComponent(guildId)}/channels`);
4895
- }
4896
- listMobileNumbers() {
4897
- return this.client.request("/mobile/numbers");
4927
+ getDiscordGuildChannels(agentId, guildId) {
4928
+ const query = new URLSearchParams({ agentId });
4929
+ return this.client.request(`/discord/guilds/${encodeURIComponent(guildId)}/channels?${query}`);
4898
4930
  }
4899
4931
  searchMobileNumbers(country, query) {
4900
4932
  const params = new URLSearchParams();
4901
- if (country) params.set("country", country);
4902
- if (query) params.set("query", query);
4903
- return this.client.request(`/mobile/numbers/search?${params}`);
4933
+ if (country !== void 0) params.set("country", country);
4934
+ if (query !== void 0) params.set("query", query);
4935
+ const qs = params.toString();
4936
+ return this.client.request(`/mobile/numbers/search${qs ? `?${qs}` : ""}`);
4904
4937
  }
4905
4938
  assignMobileNumber(agentId, phoneNumber, countryCode) {
4906
4939
  return this.client.request("/mobile/numbers/assign", {
@@ -4921,10 +4954,6 @@ var IntegrationsService = class {
4921
4954
  getMobileNumber(agentId) {
4922
4955
  return this.client.request(`/mobile/numbers?agentId=${encodeURIComponent(agentId)}`);
4923
4956
  }
4924
- disconnectGoogle(agentId, email) {
4925
- const query = email ? `?email=${encodeURIComponent(email)}` : "";
4926
- return this.client.request(`/google/agents/${encodeURIComponent(agentId)}/account${query}`, { method: "DELETE" });
4927
- }
4928
4957
  getAtlassianSites(agentId) {
4929
4958
  return this.client.request(`/atlassian/agents/${encodeURIComponent(agentId)}/sites`);
4930
4959
  }
@@ -4966,10 +4995,7 @@ const PID_PATH = join(ALFE_DIR, "gateway.pid");
4966
4995
  * Returns agentId (derived from tokenId) and orgId (tenantId).
4967
4996
  */
4968
4997
  async function resolveAgentIdentity(apiKey, apiEndpoint) {
4969
- logger$1.debug({
4970
- apiEndpoint,
4971
- keyPrefix: apiKey.slice(0, 8) + "..."
4972
- }, "Resolving agent identity...");
4998
+ logger$1.debug({ apiEndpoint }, "Resolving agent identity...");
4973
4999
  const auth = new AuthService(new AlfeApiClient({
4974
5000
  apiBaseUrl: apiEndpoint,
4975
5001
  getToken: () => Promise.resolve(apiKey)
@@ -5065,21 +5091,32 @@ function deriveAgentWorkspace(runtime, home) {
5065
5091
  if (runtime === "claude-code") return join(home, "workspace");
5066
5092
  return home;
5067
5093
  }
5094
+ const UNSAFE_RECORD_KEYS = new Set([
5095
+ "__proto__",
5096
+ "constructor",
5097
+ "prototype"
5098
+ ]);
5099
+ function isRecord$1(value) {
5100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5101
+ }
5102
+ function parseRuntimeConfigs(parsed) {
5103
+ if (!isRecord$1(parsed) || !isRecord$1(parsed.runtimes)) return {};
5104
+ const result = {};
5105
+ for (const [name, candidate] of Object.entries(parsed.runtimes)) {
5106
+ if (name.length === 0 || name.length > 128 || UNSAFE_RECORD_KEYS.has(name) || !isRecord$1(candidate) || typeof candidate.workspace !== "string" || candidate.workspace.length === 0 || candidate.workspace.length > 4096) continue;
5107
+ const workspace = candidate.workspace.startsWith("~/") ? join(homedir(), candidate.workspace.slice(2)) : candidate.workspace;
5108
+ result[name] = {
5109
+ workspace,
5110
+ agentWorkspace: deriveAgentWorkspace(name, workspace)
5111
+ };
5112
+ }
5113
+ return result;
5114
+ }
5068
5115
  async function loadRuntimeConfigs() {
5069
5116
  const configPath = join(ALFE_DIR, "config.toml");
5070
5117
  if (!existsSync(configPath)) return {};
5071
5118
  try {
5072
- const runtimes = parse(await readFile(configPath, "utf-8")).runtimes;
5073
- if (!runtimes) return {};
5074
- const result = {};
5075
- for (const [name, cfg] of Object.entries(runtimes)) if (typeof cfg.workspace === "string") {
5076
- const workspace = cfg.workspace.startsWith("~/") ? join(homedir(), cfg.workspace.slice(2)) : cfg.workspace;
5077
- result[name] = {
5078
- workspace,
5079
- agentWorkspace: deriveAgentWorkspace(name, workspace)
5080
- };
5081
- }
5082
- return result;
5119
+ return parseRuntimeConfigs(parse(await readFile(configPath, "utf-8")));
5083
5120
  } catch {
5084
5121
  return {};
5085
5122
  }
@@ -5204,66 +5241,59 @@ async function loadDaemonConfig() {
5204
5241
  * Returns null if the agent has no template assigned or the fetch fails.
5205
5242
  */
5206
5243
  async function fetchAgentConfig(apiKey, apiEndpoint) {
5244
+ const client = new AgentApiClient({
5245
+ apiKey,
5246
+ apiUrl: apiEndpoint
5247
+ });
5248
+ logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
5249
+ let workspace;
5207
5250
  try {
5208
- logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
5209
- const wsResponse = await fetch(`${apiEndpoint}/agents/me/workspace`, {
5210
- method: "GET",
5211
- headers: { "Authorization": `Bearer ${apiKey}` }
5212
- });
5213
- if (!wsResponse.ok) {
5214
- logger$1.debug({ status: wsResponse.status }, "Workspace config fetch failed");
5215
- return null;
5216
- }
5217
- const wsResult = await wsResponse.json();
5218
- const templateKey = wsResult.data?.templateKey;
5219
- const defaultModel = wsResult.data?.defaultModel ?? void 0;
5220
- const installedFrom = wsResult.data?.installedFrom;
5221
- if (!templateKey) {
5222
- logger$1.debug("No templateKey in workspace response");
5223
- return defaultModel ? {
5224
- defaultModel,
5225
- files: {}
5226
- } : null;
5227
- }
5228
- const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
5229
- const filesUrl = new URL(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`);
5230
- if (pinnedVersion !== void 0) filesUrl.searchParams.set("version", String(pinnedVersion));
5231
- logger$1.debug({
5232
- templateKey,
5233
- version: pinnedVersion
5234
- }, "Fetching template files...");
5235
- const filesResponse = await fetch(filesUrl.toString(), {
5236
- method: "GET",
5237
- headers: { "Authorization": `Bearer ${apiKey}` }
5238
- });
5239
- if (!filesResponse.ok) {
5240
- logger$1.debug({ status: filesResponse.status }, "Template files fetch failed");
5241
- return {
5242
- templateKey,
5243
- defaultModel,
5244
- files: {}
5245
- };
5246
- }
5247
- const files = (await filesResponse.json()).data?.files ?? {};
5248
- const fileCount = Object.keys(files).length;
5249
- logger$1.debug({
5250
- templateKey,
5251
- version: pinnedVersion,
5252
- fileCount
5253
- }, "Template files fetched");
5251
+ workspace = await client.getWorkspace();
5252
+ } catch (err) {
5253
+ logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Workspace config fetch failed");
5254
+ return null;
5255
+ }
5256
+ const { templateKey, installedFrom } = workspace;
5257
+ const defaultModel = workspace.defaultModel ?? void 0;
5258
+ if (!templateKey) {
5259
+ logger$1.debug("No templateKey in workspace response");
5260
+ return defaultModel ? {
5261
+ defaultModel,
5262
+ files: {}
5263
+ } : null;
5264
+ }
5265
+ const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
5266
+ logger$1.debug({
5267
+ templateKey,
5268
+ version: pinnedVersion
5269
+ }, "Fetching template files...");
5270
+ let files;
5271
+ try {
5272
+ files = (await client.getTemplateFiles(templateKey, { version: pinnedVersion })).files;
5273
+ } catch (err) {
5274
+ logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Template files fetch failed");
5254
5275
  return {
5255
5276
  templateKey,
5256
5277
  defaultModel,
5257
- files,
5258
- personaFiles: Object.keys(files)
5278
+ files: {}
5259
5279
  };
5260
- } catch (err) {
5261
- logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
5262
- return null;
5263
5280
  }
5281
+ logger$1.debug({
5282
+ templateKey,
5283
+ version: pinnedVersion,
5284
+ fileCount: Object.keys(files).length
5285
+ }, "Template files fetched");
5286
+ return {
5287
+ templateKey,
5288
+ defaultModel,
5289
+ files,
5290
+ personaFiles: Object.keys(files)
5291
+ };
5264
5292
  }
5265
5293
  //#endregion
5266
5294
  //#region src/protocol.ts
5295
+ /** Maximum size of one newline-delimited local IPC message. */
5296
+ const MAX_IPC_MESSAGE_BYTES = 1024 * 1024;
5267
5297
  /**
5268
5298
  * Map a cloud command name to an IPC method name.
5269
5299
  * Cloud commands use dot-notation matching IPC methods.
@@ -5362,23 +5392,56 @@ function parseMessage(raw) {
5362
5392
  return null;
5363
5393
  }
5364
5394
  }
5395
+ const MAX_DESIRED_INTEGRATIONS = 4096;
5396
+ const MAX_CONFIG_VALUES = 512;
5397
+ function isRecord(value) {
5398
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5399
+ }
5400
+ function isNonEmptyString(value) {
5401
+ return typeof value === "string" && value.trim().length > 0;
5402
+ }
5403
+ function isDesiredCustomSource(value) {
5404
+ if (!isRecord(value)) return false;
5405
+ return isNonEmptyString(value.connectionId) && isNonEmptyString(value.manifestRepo) && isNonEmptyString(value.manifestPath) && isNonEmptyString(value.manifestRef) && isNonEmptyString(value.manifestSha) && isNonEmptyString(value.manifestCommit) && isRecord(value.manifest);
5406
+ }
5407
+ function isDesiredConfig(value) {
5408
+ if (!isRecord(value)) return false;
5409
+ if (!Number.isSafeInteger(value.version) || value.version < 0) return false;
5410
+ if (!isRecord(value.values)) return false;
5411
+ const entries = Object.entries(value.values);
5412
+ return entries.length <= MAX_CONFIG_VALUES && entries.every(([key, configValue]) => key.trim().length > 0 && (typeof configValue === "string" || configValue === null));
5413
+ }
5414
+ function isDesiredIntegration(value) {
5415
+ if (!isRecord(value)) return false;
5416
+ return isNonEmptyString(value.integrationId) && typeof value.version === "string" && (value.desiredStatus === "active" || value.desiredStatus === "removed") && (value.reinstallRequestedAt === void 0 || typeof value.reinstallRequestedAt === "string") && (value.customSource === void 0 || isDesiredCustomSource(value.customSource));
5417
+ }
5365
5418
  /**
5366
5419
  * Type guard: is this a cloud COMMAND message?
5367
5420
  */
5368
5421
  function isCloudCommand(msg) {
5369
- return typeof msg === "object" && msg !== null && msg.type === "COMMAND" && typeof msg.commandId === "string";
5422
+ if (!isRecord(msg)) return false;
5423
+ return msg.type === "COMMAND" && isNonEmptyString(msg.commandId) && isNonEmptyString(msg.agentId) && isNonEmptyString(msg.command);
5370
5424
  }
5371
5425
  /**
5372
5426
  * Type guard: is this a cloud SERVICE_ACK message?
5373
5427
  */
5374
5428
  function isCloudServiceAck(msg) {
5375
- return typeof msg === "object" && msg !== null && msg.type === "SERVICE_ACK";
5429
+ return isRecord(msg) && msg.type === "SERVICE_ACK" && (msg.status === "ok" || msg.status === "error");
5376
5430
  }
5377
5431
  /**
5378
5432
  * Type guard: is this a cloud DESIRED_STATE message?
5379
5433
  */
5380
5434
  function isCloudDesiredState(msg) {
5381
- return typeof msg === "object" && msg !== null && msg.type === "DESIRED_STATE" && Array.isArray(msg.integrations);
5435
+ if (!isRecord(msg) || msg.type !== "DESIRED_STATE" || !Array.isArray(msg.integrations)) return false;
5436
+ if (msg.integrations.length > MAX_DESIRED_INTEGRATIONS) return false;
5437
+ if (msg.config !== void 0 && !isDesiredConfig(msg.config)) return false;
5438
+ const integrationIds = /* @__PURE__ */ new Set();
5439
+ for (const integration of msg.integrations) {
5440
+ if (!isDesiredIntegration(integration)) return false;
5441
+ if (integrationIds.has(integration.integrationId)) return false;
5442
+ integrationIds.add(integration.integrationId);
5443
+ }
5444
+ return true;
5382
5445
  }
5383
5446
  /**
5384
5447
  * Create a RECONCILIATION_REPORT message.
@@ -5411,7 +5474,7 @@ function isCloudPing(msg) {
5411
5474
  * Type guard: is this an IPC request?
5412
5475
  */
5413
5476
  function isIPCRequest(msg) {
5414
- return typeof msg === "object" && msg !== null && msg.type === "req" && typeof msg.id === "string" && typeof msg.method === "string";
5477
+ return isRecord(msg) && msg.type === "req" && isNonEmptyString(msg.id) && isNonEmptyString(msg.method) && isRecord(msg.params);
5415
5478
  }
5416
5479
  /**
5417
5480
  * Type guard: is this an IPC response?
@@ -5567,7 +5630,20 @@ var ReconciliationEngine = class {
5567
5630
  localIntegrations = await this.manager.getInstalledIntegrations();
5568
5631
  } catch (err) {
5569
5632
  log$5.error({ err }, "Failed to get local integrations");
5570
- localIntegrations = [];
5633
+ captureIntegrationFailure("_reconciliation", "read_local_state", err);
5634
+ const errorMessage = "Failed to read local integration state; reconciliation skipped";
5635
+ const affectedIds = desiredIntegrations.length > 0 ? desiredIntegrations.map((desired) => desired.integrationId) : ["*"];
5636
+ report.errors.push(...affectedIds.map((integrationId) => ({
5637
+ integrationId,
5638
+ error: errorMessage
5639
+ })));
5640
+ report.results.push(...desiredIntegrations.map((desired) => ({
5641
+ integrationId: desired.integrationId,
5642
+ action: "error",
5643
+ actualStatus: "unknown",
5644
+ errorMessage
5645
+ })));
5646
+ return report;
5571
5647
  }
5572
5648
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
5573
5649
  const desiredMap = new Map(desiredIntegrations.map((d) => [d.integrationId, d]));
@@ -5922,6 +5998,7 @@ var CloudClient = class {
5922
5998
  closed = false;
5923
5999
  registered = false;
5924
6000
  pingTimer = null;
6001
+ reconnectTimer = null;
5925
6002
  lastPong = 0;
5926
6003
  config;
5927
6004
  onCommand = null;
@@ -5994,6 +6071,7 @@ var CloudClient = class {
5994
6071
  agentId: this.config.agentId
5995
6072
  }, "Cloud client starting...");
5996
6073
  this.closed = false;
6074
+ if (this.ws || this.reconnectTimer) return;
5997
6075
  this.doConnect();
5998
6076
  }
5999
6077
  /**
@@ -6003,12 +6081,14 @@ var CloudClient = class {
6003
6081
  logger$1.debug("Cloud client stopping...");
6004
6082
  this.closed = true;
6005
6083
  this.stopPingTimer();
6084
+ this.stopReconnectTimer();
6006
6085
  if (this.ws) {
6007
- logger$1.debug({ readyState: this.ws.readyState }, "Cloud: closing WebSocket");
6086
+ const ws = this.ws;
6087
+ this.ws = null;
6088
+ logger$1.debug({ readyState: ws.readyState }, "Cloud: closing WebSocket");
6008
6089
  try {
6009
- this.ws.close(1e3, "Daemon shutting down");
6090
+ ws.close(1e3, "Daemon shutting down");
6010
6091
  } catch {}
6011
- this.ws = null;
6012
6092
  }
6013
6093
  this.registered = false;
6014
6094
  this.reconciling = false;
@@ -6043,35 +6123,45 @@ var CloudClient = class {
6043
6123
  logger$1.debug("Cloud: doConnect skipped — client is closed");
6044
6124
  return;
6045
6125
  }
6126
+ if (this.ws) {
6127
+ logger$1.debug("Cloud: doConnect skipped — connection already exists");
6128
+ return;
6129
+ }
6046
6130
  logger$1.info({
6047
6131
  url: this.config.wsUrl,
6048
6132
  backoffMs: this.backoffMs
6049
6133
  }, "Connecting to cloud gateway...");
6050
- logger$1.debug({
6051
- agentId: this.config.agentId,
6052
- keyPrefix: this.config.apiKey.slice(0, 12) + "..."
6053
- }, "Cloud: connection details");
6054
- this.ws = new WebSocket(this.config.wsUrl, {
6134
+ logger$1.debug({ agentId: this.config.agentId }, "Cloud: connection details");
6135
+ const ws = new WebSocket(this.config.wsUrl, {
6055
6136
  headers: { authorization: `Bearer ${this.config.apiKey}` },
6056
6137
  maxPayload: 10 * 1024 * 1024,
6057
6138
  handshakeTimeout: 1e4
6058
6139
  });
6059
- this.ws.on("open", () => {
6140
+ this.ws = ws;
6141
+ ws.on("open", () => {
6142
+ if (this.ws !== ws) return;
6060
6143
  logger$1.info("Cloud WebSocket connected");
6061
- logger$1.debug({ readyState: this.ws?.readyState }, "Cloud: WebSocket open, sending registration...");
6144
+ logger$1.debug({ readyState: ws.readyState }, "Cloud: WebSocket open, sending registration...");
6062
6145
  this.backoffMs = 1e3;
6063
6146
  this.sendRegister();
6064
6147
  });
6065
- this.ws.on("message", (data) => {
6148
+ ws.on("message", (data) => {
6149
+ if (this.ws !== ws) return;
6066
6150
  const text = Buffer.isBuffer(data) ? data.toString("utf-8") : Buffer.from(data).toString("utf-8");
6067
6151
  logger$1.debug({ size: text.length }, "Cloud: received message");
6068
6152
  this.handleMessage(text);
6069
6153
  });
6070
- this.ws.on("ping", () => {
6154
+ ws.on("ping", () => {
6155
+ if (this.ws !== ws) return;
6071
6156
  logger$1.debug("Cloud: received ping, sending pong");
6072
- this.ws?.pong();
6157
+ ws.pong();
6073
6158
  });
6074
- this.ws.on("close", (code, reason) => {
6159
+ ws.on("close", (code, reason) => {
6160
+ if (this.ws !== ws) {
6161
+ logger$1.debug("Cloud: ignoring close from superseded WebSocket");
6162
+ return;
6163
+ }
6164
+ this.ws = null;
6075
6165
  logger$1.warn({
6076
6166
  code,
6077
6167
  reason: reason.toString()
@@ -6081,7 +6171,8 @@ var CloudClient = class {
6081
6171
  this.onConnectionChange?.(false);
6082
6172
  this.scheduleReconnect();
6083
6173
  });
6084
- this.ws.on("error", (err) => {
6174
+ ws.on("error", (err) => {
6175
+ if (this.ws !== ws) return;
6085
6176
  logger$1.error({
6086
6177
  err: err.message,
6087
6178
  url: this.config.wsUrl
@@ -6128,10 +6219,16 @@ var CloudClient = class {
6128
6219
  if (ack.status === "ok") {
6129
6220
  logger$1.info("Cloud: registered successfully ✅");
6130
6221
  this.registered = true;
6222
+ this.lastPong = Date.now();
6131
6223
  this.startPingTimer();
6132
6224
  this.onConnectionChange?.(true);
6225
+ } else if (ack.retryable === false) {
6226
+ logger$1.error({ message: ack.message }, "Cloud: registration rejected — not retrying");
6227
+ this.closed = true;
6228
+ this.stopReconnectTimer();
6229
+ this.ws?.close(1008, "Registration rejected");
6133
6230
  } else {
6134
- logger$1.error({ message: ack.message }, "Cloud: registration failed");
6231
+ logger$1.error({ message: ack.message }, "Cloud: registration failed — will retry");
6135
6232
  this.ws?.close(1008, "Registration rejected");
6136
6233
  }
6137
6234
  }
@@ -6261,16 +6358,27 @@ var CloudClient = class {
6261
6358
  logger$1.debug("Cloud: reconnect skipped — client is closed");
6262
6359
  return;
6263
6360
  }
6361
+ if (this.reconnectTimer) {
6362
+ logger$1.debug("Cloud: reconnect already scheduled");
6363
+ return;
6364
+ }
6264
6365
  const delay = this.backoffMs;
6265
6366
  this.backoffMs = Math.min(this.backoffMs * 2, 3e4);
6266
6367
  logger$1.info({
6267
6368
  delayMs: delay,
6268
6369
  nextBackoffMs: this.backoffMs
6269
6370
  }, "Cloud: scheduling reconnect...");
6270
- setTimeout(() => {
6371
+ this.reconnectTimer = setTimeout(() => {
6372
+ this.reconnectTimer = null;
6271
6373
  this.doConnect();
6272
6374
  }, delay);
6273
6375
  }
6376
+ stopReconnectTimer() {
6377
+ if (this.reconnectTimer) {
6378
+ clearTimeout(this.reconnectTimer);
6379
+ this.reconnectTimer = null;
6380
+ }
6381
+ }
6274
6382
  };
6275
6383
  //#endregion
6276
6384
  //#region src/config-reconciler.ts
@@ -6360,406 +6468,70 @@ var ConfigReconciler = class {
6360
6468
  }
6361
6469
  };
6362
6470
  //#endregion
6363
- //#region src/ipc-server.ts
6364
- /**
6365
- * IPC Server — Unix socket server for local plugin connections.
6366
- *
6367
- * Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
6368
- * Request: { type: 'req', id, method, params }
6369
- * Response: { id, ok, payload?, error? }
6370
- * Event: { type: 'event', event, payload }
6371
- *
6372
- * Each connected plugin registers with its name/version and receives
6373
- * commands from the daemon (forwarded from cloud).
6374
- */
6375
- var IPCServer = class {
6376
- server = null;
6377
- connections = /* @__PURE__ */ new Map();
6378
- socketPath;
6379
- requestHandler = null;
6380
- constructor(socketPath) {
6381
- this.socketPath = socketPath;
6471
+ //#region src/command-queue.ts
6472
+ const DEFAULT_TTL_MS$1 = 300 * 1e3;
6473
+ const DEFAULT_MAX_PER_SERVICE = 100;
6474
+ const DEFAULT_MAX_TOTAL = 1e3;
6475
+ var CommandQueue = class {
6476
+ queues = /* @__PURE__ */ new Map();
6477
+ ttlMs;
6478
+ gcTimer = null;
6479
+ constructor(ttlMs = DEFAULT_TTL_MS$1, maxPerService = DEFAULT_MAX_PER_SERVICE, maxTotal = DEFAULT_MAX_TOTAL) {
6480
+ this.maxPerService = maxPerService;
6481
+ this.maxTotal = maxTotal;
6482
+ this.ttlMs = ttlMs;
6382
6483
  }
6383
6484
  /**
6384
- * Set the handler for incoming IPC requests from plugins.
6485
+ * Start periodic garbage collection of expired commands.
6385
6486
  */
6386
- setRequestHandler(handler) {
6387
- this.requestHandler = handler;
6487
+ startGC(intervalMs = 3e4) {
6488
+ this.stopGC();
6489
+ this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
6388
6490
  }
6389
6491
  /**
6390
- * Start listening on the Unix socket.
6492
+ * Stop periodic garbage collection.
6391
6493
  */
6392
- async start() {
6393
- try {
6394
- unlinkSync(this.socketPath);
6395
- } catch {}
6396
- return new Promise((resolve, reject) => {
6397
- this.server = createServer((socket) => {
6398
- this.handleConnection(socket);
6399
- });
6400
- this.server.on("error", (err) => {
6401
- logger$1.error({ err: err.message }, "IPC server error");
6402
- reject(err);
6403
- });
6404
- this.server.listen(this.socketPath, () => {
6405
- try {
6406
- chmodSync(this.socketPath, 384);
6407
- } catch (err) {
6408
- logger$1.warn({ err }, "Failed to chmod socket");
6409
- }
6410
- logger$1.info({ path: this.socketPath }, "IPC server listening");
6411
- resolve();
6412
- });
6413
- });
6494
+ stopGC() {
6495
+ if (this.gcTimer) {
6496
+ clearInterval(this.gcTimer);
6497
+ this.gcTimer = null;
6498
+ }
6414
6499
  }
6415
6500
  /**
6416
- * Stop the IPC server and disconnect all plugins.
6501
+ * Enqueue a command for a specific service.
6417
6502
  */
6418
- async stop() {
6419
- for (const conn of this.connections.values()) try {
6420
- this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
6421
- conn.socket.end();
6422
- } catch {}
6423
- return new Promise((resolve) => {
6424
- if (!this.server) {
6425
- resolve();
6426
- return;
6427
- }
6428
- this.server.close(() => {
6429
- try {
6430
- unlinkSync(this.socketPath);
6431
- } catch {}
6432
- logger$1.info("IPC server stopped");
6433
- resolve();
6434
- });
6435
- for (const conn of this.connections.values()) conn.socket.destroy();
6436
- this.connections.clear();
6503
+ enqueue(serviceId, request, commandId) {
6504
+ this.purgeExpired();
6505
+ let queue = this.queues.get(serviceId);
6506
+ if (!queue) {
6507
+ queue = [];
6508
+ this.queues.set(serviceId, queue);
6509
+ }
6510
+ if (queue.length >= this.maxPerService || this.totalPending() >= this.maxTotal) {
6511
+ if (queue.length === 0) this.queues.delete(serviceId);
6512
+ return false;
6513
+ }
6514
+ queue.push({
6515
+ request,
6516
+ queuedAt: Date.now(),
6517
+ commandId
6437
6518
  });
6519
+ return true;
6438
6520
  }
6439
6521
  /**
6440
- * Send an IPC request to a specific plugin and wait for response.
6522
+ * Drain all pending (non-expired) commands for a service.
6523
+ * Returns them in order and removes them from the queue.
6441
6524
  */
6442
- async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
6443
- const conn = this.connections.get(pluginId);
6444
- if (!conn?.info) return {
6445
- id: "",
6446
- ok: false,
6447
- error: {
6448
- code: "PLUGIN_NOT_CONNECTED",
6449
- message: `Plugin ${pluginId} not connected`
6450
- }
6451
- };
6452
- const id = pluginConnectionId();
6453
- const request = {
6454
- type: "req",
6455
- id,
6456
- method,
6457
- params
6458
- };
6459
- return new Promise((resolve, reject) => {
6460
- const timer = setTimeout(() => {
6461
- conn.pending.delete(id);
6462
- resolve({
6463
- id,
6464
- ok: false,
6465
- error: {
6466
- code: "TIMEOUT",
6467
- message: `Request ${method} timed out after ${String(timeoutMs)}ms`
6468
- }
6469
- });
6470
- }, timeoutMs);
6471
- conn.pending.set(id, {
6472
- resolve,
6473
- reject,
6474
- timer
6475
- });
6476
- try {
6477
- conn.socket.write(JSON.stringify(request) + "\n");
6478
- } catch (err) {
6479
- clearTimeout(timer);
6480
- conn.pending.delete(id);
6481
- resolve({
6482
- id,
6483
- ok: false,
6484
- error: {
6485
- code: "SEND_FAILED",
6486
- message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
6487
- }
6488
- });
6489
- }
6490
- });
6525
+ drain(serviceId) {
6526
+ const queue = this.queues.get(serviceId);
6527
+ if (!queue || queue.length === 0) return [];
6528
+ const now = Date.now();
6529
+ const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6530
+ this.queues.delete(serviceId);
6531
+ return valid;
6491
6532
  }
6492
6533
  /**
6493
- * Send an IPC request to ALL registered plugins.
6494
- * Returns responses keyed by plugin ID.
6495
- */
6496
- async broadcastRequest(method, params, timeoutMs = 3e4) {
6497
- const results = /* @__PURE__ */ new Map();
6498
- const registered = this.getRegisteredPlugins();
6499
- await Promise.all(registered.map(async ([pluginId]) => {
6500
- const response = await this.sendRequest(pluginId, method, params, timeoutMs);
6501
- results.set(pluginId, response);
6502
- }));
6503
- return results;
6504
- }
6505
- /**
6506
- * Send an event to a specific plugin (fire-and-forget).
6507
- */
6508
- sendEventToPlugin(pluginId, event, payload) {
6509
- const conn = this.connections.get(pluginId);
6510
- if (!conn) return false;
6511
- return this.sendEvent(conn, event, payload);
6512
- }
6513
- /**
6514
- * Broadcast an event to all connected plugins.
6515
- */
6516
- broadcastEvent(event, payload) {
6517
- for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
6518
- }
6519
- /**
6520
- * Get all registered plugins and their info.
6521
- */
6522
- getRegisteredPlugins() {
6523
- const plugins = [];
6524
- for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
6525
- return plugins;
6526
- }
6527
- /**
6528
- * Get number of connected plugins (including unregistered).
6529
- */
6530
- get connectionCount() {
6531
- return this.connections.size;
6532
- }
6533
- /**
6534
- * Get number of registered plugins.
6535
- */
6536
- get registeredCount() {
6537
- let count = 0;
6538
- for (const conn of this.connections.values()) if (conn.info) count++;
6539
- return count;
6540
- }
6541
- handleConnection(socket) {
6542
- const connId = pluginConnectionId();
6543
- const conn = {
6544
- id: connId,
6545
- socket,
6546
- info: null,
6547
- buffer: "",
6548
- pending: /* @__PURE__ */ new Map()
6549
- };
6550
- this.connections.set(connId, conn);
6551
- logger$1.info({ connId }, "IPC: new connection");
6552
- socket.on("data", (data) => {
6553
- conn.buffer += data.toString();
6554
- this.processBuffer(conn);
6555
- });
6556
- socket.on("close", () => {
6557
- logger$1.info({
6558
- connId,
6559
- plugin: conn.info?.name
6560
- }, "IPC: connection closed");
6561
- for (const [, pending] of conn.pending) {
6562
- clearTimeout(pending.timer);
6563
- pending.resolve({
6564
- id: "",
6565
- ok: false,
6566
- error: {
6567
- code: "DISCONNECTED",
6568
- message: "Plugin disconnected"
6569
- }
6570
- });
6571
- }
6572
- this.connections.delete(connId);
6573
- });
6574
- socket.on("error", (err) => {
6575
- logger$1.error({
6576
- connId,
6577
- err: err.message
6578
- }, "IPC: socket error");
6579
- });
6580
- }
6581
- processBuffer(conn) {
6582
- let newlineIdx;
6583
- while ((newlineIdx = conn.buffer.indexOf("\n")) !== -1) {
6584
- const line = conn.buffer.slice(0, newlineIdx).trim();
6585
- conn.buffer = conn.buffer.slice(newlineIdx + 1);
6586
- if (!line) continue;
6587
- let parsed;
6588
- try {
6589
- parsed = JSON.parse(line);
6590
- } catch {
6591
- logger$1.warn({ connId: conn.id }, "IPC: invalid JSON from plugin");
6592
- continue;
6593
- }
6594
- if (isIPCResponse(parsed)) {
6595
- this.handlePluginResponse(conn, parsed);
6596
- continue;
6597
- }
6598
- if (isIPCRequest(parsed)) {
6599
- this.handlePluginRequest(conn, parsed);
6600
- continue;
6601
- }
6602
- logger$1.debug({
6603
- connId: conn.id,
6604
- msg: parsed
6605
- }, "IPC: unhandled message");
6606
- }
6607
- }
6608
- handlePluginResponse(conn, response) {
6609
- const pending = conn.pending.get(response.id);
6610
- if (!pending) return;
6611
- clearTimeout(pending.timer);
6612
- conn.pending.delete(response.id);
6613
- pending.resolve(response);
6614
- }
6615
- async handlePluginRequest(conn, request) {
6616
- if (conn.info) conn.info.lastSeen = Date.now();
6617
- if (request.method === "register") {
6618
- this.handleRegister(conn, request);
6619
- return;
6620
- }
6621
- if (this.requestHandler) try {
6622
- const result = await this.requestHandler(request.method, request.params, conn.id);
6623
- const response = result.ok ? createIPCResponse(request.id, result.payload) : createIPCError(request.id, result.error?.code ?? "UNKNOWN", result.error?.message ?? "Unknown error");
6624
- this.sendResponse(conn, response);
6625
- } catch (err) {
6626
- const message = err instanceof Error ? err.message : String(err);
6627
- this.sendResponse(conn, createIPCError(request.id, "INTERNAL", message));
6628
- }
6629
- else this.sendResponse(conn, createIPCError(request.id, "NO_HANDLER", `No handler for method: ${request.method}`));
6630
- }
6631
- handleRegister(conn, request) {
6632
- const { name, version, protocolVersion, capabilities, pid } = request.params;
6633
- if (!name || !version) {
6634
- this.sendResponse(conn, createIPCError(request.id, "INVALID_REGISTER", "name and version are required"));
6635
- return;
6636
- }
6637
- if (protocolVersion !== void 0 && protocolVersion !== 1) {
6638
- this.sendResponse(conn, createIPCError(request.id, "PROTOCOL_MISMATCH", `Unsupported protocol version ${String(protocolVersion)}. Server supports v${String(1)}.`));
6639
- return;
6640
- }
6641
- const staleIds = [];
6642
- for (const [existingId, existing] of this.connections) if (existingId !== conn.id && existing.info?.name === name) if (pid != null && existing.info.pid === pid) {
6643
- logger$1.info({
6644
- staleConnId: existingId,
6645
- newConnId: conn.id,
6646
- plugin: name,
6647
- pid
6648
- }, "IPC: closing stale duplicate connection (same PID)");
6649
- existing.socket.destroy();
6650
- staleIds.push(existingId);
6651
- } else {
6652
- logger$1.info({
6653
- staleConnId: existingId,
6654
- newConnId: conn.id,
6655
- plugin: name,
6656
- oldPid: existing.info.pid,
6657
- newPid: pid
6658
- }, "IPC: replacing connection from different process");
6659
- this.sendEvent(existing, "plugin.replaced", {
6660
- reason: "Another process registered with the same plugin name",
6661
- replacedBy: conn.id
6662
- });
6663
- existing.socket.end();
6664
- staleIds.push(existingId);
6665
- }
6666
- for (const id of staleIds) this.connections.delete(id);
6667
- conn.info = {
6668
- name,
6669
- version,
6670
- protocolVersion: protocolVersion ?? 1,
6671
- capabilities: capabilities ?? [],
6672
- pid: pid ?? null,
6673
- connectedAt: Date.now(),
6674
- lastSeen: Date.now()
6675
- };
6676
- logger$1.info({
6677
- connId: conn.id,
6678
- plugin: name,
6679
- version,
6680
- capabilities
6681
- }, "IPC: plugin registered");
6682
- this.sendResponse(conn, createIPCResponse(request.id, {
6683
- status: "registered",
6684
- daemonVersion: "0.1.0",
6685
- protocolVersion: 1
6686
- }));
6687
- }
6688
- sendResponse(conn, response) {
6689
- try {
6690
- conn.socket.write(JSON.stringify(response) + "\n");
6691
- } catch (err) {
6692
- logger$1.error({
6693
- connId: conn.id,
6694
- err
6695
- }, "IPC: failed to send response");
6696
- }
6697
- }
6698
- sendEvent(conn, event, payload) {
6699
- try {
6700
- const msg = createIPCEvent(event, payload);
6701
- conn.socket.write(JSON.stringify(msg) + "\n");
6702
- return true;
6703
- } catch {
6704
- return false;
6705
- }
6706
- }
6707
- };
6708
- //#endregion
6709
- //#region src/command-queue.ts
6710
- const DEFAULT_TTL_MS$1 = 300 * 1e3;
6711
- var CommandQueue = class {
6712
- queues = /* @__PURE__ */ new Map();
6713
- ttlMs;
6714
- gcTimer = null;
6715
- constructor(ttlMs = DEFAULT_TTL_MS$1) {
6716
- this.ttlMs = ttlMs;
6717
- }
6718
- /**
6719
- * Start periodic garbage collection of expired commands.
6720
- */
6721
- startGC(intervalMs = 3e4) {
6722
- this.stopGC();
6723
- this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
6724
- }
6725
- /**
6726
- * Stop periodic garbage collection.
6727
- */
6728
- stopGC() {
6729
- if (this.gcTimer) {
6730
- clearInterval(this.gcTimer);
6731
- this.gcTimer = null;
6732
- }
6733
- }
6734
- /**
6735
- * Enqueue a command for a specific service.
6736
- */
6737
- enqueue(serviceId, request, commandId) {
6738
- let queue = this.queues.get(serviceId);
6739
- if (!queue) {
6740
- queue = [];
6741
- this.queues.set(serviceId, queue);
6742
- }
6743
- queue.push({
6744
- request,
6745
- queuedAt: Date.now(),
6746
- commandId
6747
- });
6748
- }
6749
- /**
6750
- * Drain all pending (non-expired) commands for a service.
6751
- * Returns them in order and removes them from the queue.
6752
- */
6753
- drain(serviceId) {
6754
- const queue = this.queues.get(serviceId);
6755
- if (!queue || queue.length === 0) return [];
6756
- const now = Date.now();
6757
- const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6758
- this.queues.delete(serviceId);
6759
- return valid;
6760
- }
6761
- /**
6762
- * Get the number of pending commands for a service.
6534
+ * Get the number of pending commands for a service.
6763
6535
  */
6764
6536
  pendingCount(serviceId) {
6765
6537
  const queue = this.queues.get(serviceId);
@@ -6813,6 +6585,13 @@ var CommandQueue = class {
6813
6585
  */
6814
6586
  const LAUNCHD_LABEL = "ai.alfe.gateway";
6815
6587
  const SYSTEMD_SERVICE = "alfe-gateway";
6588
+ const MANAGED_ENV_VARS = [
6589
+ "ALFE_MANAGED",
6590
+ "ALFE_API_KEY",
6591
+ "LOG_LEVEL",
6592
+ "ALFE_CLI_VERSION"
6593
+ ];
6594
+ const execFileAsync$3 = promisify(execFile);
6816
6595
  /**
6817
6596
  * On-disk path for the boot-time self-heal guard script (Linux only).
6818
6597
  * Written next to the systemd unit at setup time and invoked via
@@ -6827,6 +6606,21 @@ function isRootUser() {
6827
6606
  function getSystemdSystemServicePath() {
6828
6607
  return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
6829
6608
  }
6609
+ function getManagedEnvironmentPath() {
6610
+ return isRootUser() ? "/etc/alfe/gateway.env" : join(homedir(), ".alfe", "gateway.env");
6611
+ }
6612
+ function escapeEnvironmentFileValue(value) {
6613
+ if (/[\0\r\n]/.test(value)) throw new Error("Managed environment values cannot contain NUL or newlines");
6614
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
6615
+ }
6616
+ /** @internal exported for unit tests; not re-exported from the barrel. */
6617
+ function generateManagedEnvironmentFile() {
6618
+ const lines = MANAGED_ENV_VARS.flatMap((key) => {
6619
+ const value = process.env[key];
6620
+ return value === void 0 ? [] : [`${key}="${escapeEnvironmentFileValue(value)}"`];
6621
+ });
6622
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
6623
+ }
6830
6624
  function getLaunchdPlistPath() {
6831
6625
  return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6832
6626
  }
@@ -6921,7 +6715,7 @@ function generateLaunchdPlist() {
6921
6715
  */
6922
6716
  /** @internal exported for unit tests; not re-exported from the barrel. */
6923
6717
  function generateGuardScript() {
6924
- const version = process.env.ALFE_CLI_VERSION;
6718
+ const exactVersion = process.env.ALFE_CLI_VERSION?.match(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/)?.[0];
6925
6719
  return `#!/bin/sh
6926
6720
  # Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
6927
6721
  # Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
@@ -6929,7 +6723,7 @@ function generateGuardScript() {
6929
6723
  # Must never wedge boot: every failure path logs and exits 0.
6930
6724
  set -u
6931
6725
 
6932
- TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
6726
+ TARGET='${exactVersion ? `@alfe.ai/cli@${exactVersion}` : "@alfe.ai/cli@latest"}'
6933
6727
  log() { echo "[alfe-cli-guard] $*" >&2; }
6934
6728
 
6935
6729
  # Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
@@ -6993,12 +6787,7 @@ exit 0
6993
6787
  function generateSystemdUnit() {
6994
6788
  const alfeBin = getAlfeBinPath();
6995
6789
  const root = isRootUser();
6996
- const envLines = [
6997
- "ALFE_MANAGED",
6998
- "ALFE_API_KEY",
6999
- "LOG_LEVEL",
7000
- "ALFE_CLI_VERSION"
7001
- ].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
6790
+ const managedEnvironmentPath = getManagedEnvironmentPath();
7002
6791
  return `[Unit]
7003
6792
  Description=Alfe Gateway Daemon
7004
6793
  After=network-online.target
@@ -7017,7 +6806,7 @@ RestartSec=10
7017
6806
  # SIGKILL when the stop timeout expires.
7018
6807
  KillMode=mixed
7019
6808
  Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
7020
- ${envLines}
6809
+ EnvironmentFile=-${managedEnvironmentPath}
7021
6810
 
7022
6811
  [Install]
7023
6812
  WantedBy=${root ? "multi-user.target" : "default.target"}`;
@@ -7035,6 +6824,19 @@ async function writeGuardScript() {
7035
6824
  });
7036
6825
  logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
7037
6826
  }
6827
+ async function writeManagedEnvironmentFile() {
6828
+ const environmentPath = getManagedEnvironmentPath();
6829
+ await mkdir(dirname(environmentPath), {
6830
+ recursive: true,
6831
+ mode: 448
6832
+ });
6833
+ await writeFile(environmentPath, generateManagedEnvironmentFile(), {
6834
+ encoding: "utf-8",
6835
+ mode: 384
6836
+ });
6837
+ await chmod(environmentPath, 384);
6838
+ logger$1.info({ path: environmentPath }, "Wrote managed environment file");
6839
+ }
7038
6840
  /**
7039
6841
  * Install the service unit for the current platform.
7040
6842
  */
@@ -7098,6 +6900,7 @@ async function installSystemd() {
7098
6900
  const ctl = root ? "systemctl" : "systemctl --user";
7099
6901
  if (!root) await mkdir(dir, { recursive: true });
7100
6902
  await writeGuardScript();
6903
+ await writeManagedEnvironmentFile();
7101
6904
  await writeFile(unitPath, generateSystemdUnit(), "utf-8");
7102
6905
  logger$1.info({
7103
6906
  path: unitPath,
@@ -7123,6 +6926,9 @@ async function uninstallSystemd() {
7123
6926
  try {
7124
6927
  await unlink(getGuardScriptPath());
7125
6928
  } catch {}
6929
+ try {
6930
+ await unlink(getManagedEnvironmentPath());
6931
+ } catch {}
7126
6932
  try {
7127
6933
  execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
7128
6934
  } catch {}
@@ -7217,6 +7023,26 @@ function isServiceInstalled() {
7217
7023
  async function writePidFile() {
7218
7024
  await writeFile(PID_PATH, String(process.pid), "utf-8");
7219
7025
  }
7026
+ /** @internal exported for unit tests; not re-exported from the barrel. */
7027
+ function isGatewayDaemonCommand(command) {
7028
+ const normalized = command.replaceAll("\0", " ").replace(/\s+/g, " ").trim();
7029
+ const hasGatewayEntrypoint = /(?:^|[\s/])(?:alfe-gateway|gateway(?:\.js)?)(?:\s|$)/.test(normalized) || /(?:^|\s)alfe(?:\s|$)/.test(normalized) && /(?:^|\s)gateway(?:\s|$)/.test(normalized);
7030
+ const hasDaemonVerb = /(?:^|\s)(?:daemon|start|restart)(?:\s|$)/.test(normalized);
7031
+ return hasGatewayEntrypoint && hasDaemonVerb;
7032
+ }
7033
+ async function isGatewayDaemonProcess(pid) {
7034
+ try {
7035
+ const { stdout } = await execFileAsync$3("ps", [
7036
+ "-p",
7037
+ String(pid),
7038
+ "-o",
7039
+ "command="
7040
+ ], { timeout: 2e3 });
7041
+ return isGatewayDaemonCommand(stdout);
7042
+ } catch {
7043
+ return false;
7044
+ }
7045
+ }
7220
7046
  /**
7221
7047
  * Remove the PID file.
7222
7048
  */
@@ -7232,15 +7058,18 @@ async function removePidFile() {
7232
7058
  async function checkExistingDaemon() {
7233
7059
  if (!existsSync(PID_PATH)) return null;
7234
7060
  try {
7235
- const pidStr = await readFile(PID_PATH, "utf-8");
7236
- const pid = parseInt(pidStr.trim(), 10);
7237
- if (isNaN(pid)) {
7061
+ const trimmedPid = (await readFile(PID_PATH, "utf-8")).trim();
7062
+ const pid = /^\d+$/.test(trimmedPid) ? Number(trimmedPid) : NaN;
7063
+ if (!Number.isSafeInteger(pid) || pid <= 1) {
7238
7064
  await removePidFile();
7239
7065
  return null;
7240
7066
  }
7241
7067
  try {
7242
7068
  process.kill(pid, 0);
7243
- return pid;
7069
+ if (await isGatewayDaemonProcess(pid)) return pid;
7070
+ logger$1.warn({ pid }, "Ignoring stale daemon PID owned by another process");
7071
+ await removePidFile();
7072
+ return null;
7244
7073
  } catch {
7245
7074
  await removePidFile();
7246
7075
  return null;
@@ -7266,7 +7095,8 @@ async function stopExistingDaemon() {
7266
7095
  return true;
7267
7096
  }
7268
7097
  }
7269
- process.kill(pid, "SIGKILL");
7098
+ if (await isGatewayDaemonProcess(pid)) process.kill(pid, "SIGKILL");
7099
+ else logger$1.warn({ pid }, "Skipping SIGKILL because daemon PID ownership changed");
7270
7100
  await removePidFile();
7271
7101
  return true;
7272
7102
  } catch {
@@ -7275,176 +7105,1104 @@ async function stopExistingDaemon() {
7275
7105
  }
7276
7106
  }
7277
7107
  //#endregion
7278
- //#region ../../node_modules/.pnpm/dotenv@17.2.2/node_modules/dotenv/package.json
7279
- var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
7280
- module.exports = {
7281
- "name": "dotenv",
7282
- "version": "17.2.2",
7283
- "description": "Loads environment variables from .env file",
7284
- "main": "lib/main.js",
7285
- "types": "lib/main.d.ts",
7286
- "exports": {
7287
- ".": {
7288
- "types": "./lib/main.d.ts",
7289
- "require": "./lib/main.js",
7290
- "default": "./lib/main.js"
7291
- },
7292
- "./config": "./config.js",
7293
- "./config.js": "./config.js",
7294
- "./lib/env-options": "./lib/env-options.js",
7295
- "./lib/env-options.js": "./lib/env-options.js",
7296
- "./lib/cli-options": "./lib/cli-options.js",
7297
- "./lib/cli-options.js": "./lib/cli-options.js",
7298
- "./package.json": "./package.json"
7299
- },
7300
- "scripts": {
7301
- "dts-check": "tsc --project tests/types/tsconfig.json",
7302
- "lint": "standard",
7303
- "pretest": "npm run lint && npm run dts-check",
7304
- "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
7305
- "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
7306
- "prerelease": "npm test",
7307
- "release": "standard-version"
7308
- },
7309
- "repository": {
7310
- "type": "git",
7311
- "url": "git://github.com/motdotla/dotenv.git"
7312
- },
7313
- "homepage": "https://github.com/motdotla/dotenv#readme",
7314
- "funding": "https://dotenvx.com",
7315
- "keywords": [
7316
- "dotenv",
7317
- "env",
7318
- ".env",
7319
- "environment",
7320
- "variables",
7321
- "config",
7322
- "settings"
7323
- ],
7324
- "readmeFilename": "README.md",
7325
- "license": "BSD-2-Clause",
7326
- "devDependencies": {
7327
- "@types/node": "^18.11.3",
7328
- "decache": "^4.6.2",
7329
- "sinon": "^14.0.1",
7330
- "standard": "^17.0.0",
7331
- "standard-version": "^9.5.0",
7332
- "tap": "^19.2.0",
7333
- "typescript": "^4.8.4"
7334
- },
7335
- "engines": { "node": ">=12" },
7336
- "browser": { "fs": false }
7337
- };
7338
- }));
7339
- (/* @__PURE__ */ __commonJSMin(((exports, module) => {
7340
- const fs$1 = __require("fs");
7341
- const path$1 = __require("path");
7342
- const os$1 = __require("os");
7343
- const crypto$2 = __require("crypto");
7344
- const version = require_package().version;
7345
- const TIPS = [
7346
- "🔐 encrypt with Dotenvx: https://dotenvx.com",
7347
- "🔐 prevent committing .env to code: https://dotenvx.com/precommit",
7348
- "🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
7349
- "📡 observe env with Radar: https://dotenvx.com/radar",
7350
- "📡 auto-backup env with Radar: https://dotenvx.com/radar",
7351
- "📡 version env with Radar: https://dotenvx.com/radar",
7352
- "🛠️ run anywhere with `dotenvx run -- yourcommand`",
7353
- "⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
7354
- "⚙️ enable debug logging with { debug: true }",
7355
- "⚙️ override existing env vars with { override: true }",
7356
- "⚙️ suppress all logs with { quiet: true }",
7357
- "⚙️ write to custom object with { processEnv: myObject }",
7358
- "⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
7359
- ];
7360
- function _getRandomTip() {
7361
- return TIPS[Math.floor(Math.random() * TIPS.length)];
7362
- }
7363
- function parseBoolean(value) {
7364
- if (typeof value === "string") return ![
7365
- "false",
7366
- "0",
7367
- "no",
7368
- "off",
7369
- ""
7370
- ].includes(value.toLowerCase());
7371
- return Boolean(value);
7108
+ //#region src/ipc-server.ts
7109
+ /**
7110
+ * IPC Server — Unix socket server for local plugin connections.
7111
+ *
7112
+ * Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
7113
+ * Request: { type: 'req', id, method, params }
7114
+ * Response: { id, ok, payload?, error? }
7115
+ * Event: { type: 'event', event, payload }
7116
+ *
7117
+ * Each connected plugin registers with its name/version and receives
7118
+ * commands from the daemon (forwarded from cloud).
7119
+ */
7120
+ var IPCServer = class {
7121
+ server = null;
7122
+ connections = /* @__PURE__ */ new Map();
7123
+ socketPath;
7124
+ requestHandler = null;
7125
+ constructor(socketPath, daemonVersion = "unknown") {
7126
+ this.daemonVersion = daemonVersion;
7127
+ this.socketPath = socketPath;
7372
7128
  }
7373
- function supportsAnsi() {
7374
- return process.stdout.isTTY;
7129
+ /**
7130
+ * Set the handler for incoming IPC requests from plugins.
7131
+ */
7132
+ setRequestHandler(handler) {
7133
+ this.requestHandler = handler;
7375
7134
  }
7376
- function dim(text) {
7377
- return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text;
7135
+ /**
7136
+ * Start listening on the Unix socket.
7137
+ */
7138
+ async start() {
7139
+ try {
7140
+ unlinkSync(this.socketPath);
7141
+ } catch {}
7142
+ return new Promise((resolve, reject) => {
7143
+ this.server = createServer((socket) => {
7144
+ this.handleConnection(socket);
7145
+ });
7146
+ this.server.on("error", (err) => {
7147
+ logger$1.error({ err: err.message }, "IPC server error");
7148
+ reject(err);
7149
+ });
7150
+ this.server.listen(this.socketPath, () => {
7151
+ try {
7152
+ chmodSync(this.socketPath, 384);
7153
+ } catch (err) {
7154
+ logger$1.warn({ err }, "Failed to chmod socket");
7155
+ }
7156
+ logger$1.info({ path: this.socketPath }, "IPC server listening");
7157
+ resolve();
7158
+ });
7159
+ });
7378
7160
  }
7379
- const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
7380
- function parse(src) {
7381
- const obj = {};
7382
- let lines = src.toString();
7383
- lines = lines.replace(/\r\n?/gm, "\n");
7384
- let match;
7385
- while ((match = LINE.exec(lines)) != null) {
7386
- const key = match[1];
7387
- let value = match[2] || "";
7388
- value = value.trim();
7389
- const maybeQuote = value[0];
7390
- value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
7391
- if (maybeQuote === "\"") {
7392
- value = value.replace(/\\n/g, "\n");
7393
- value = value.replace(/\\r/g, "\r");
7161
+ /**
7162
+ * Stop the IPC server and disconnect all plugins.
7163
+ */
7164
+ async stop() {
7165
+ for (const conn of this.connections.values()) try {
7166
+ this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
7167
+ conn.socket.end();
7168
+ } catch {}
7169
+ return new Promise((resolve) => {
7170
+ if (!this.server) {
7171
+ resolve();
7172
+ return;
7394
7173
  }
7395
- obj[key] = value;
7396
- }
7397
- return obj;
7398
- }
7399
- function _parseVault(options) {
7400
- options = options || {};
7401
- const vaultPath = _vaultPath(options);
7402
- options.path = vaultPath;
7403
- const result = DotenvModule.configDotenv(options);
7404
- if (!result.parsed) {
7405
- const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
7406
- err.code = "MISSING_DATA";
7407
- throw err;
7408
- }
7409
- const keys = _dotenvKey(options).split(",");
7410
- const length = keys.length;
7411
- let decrypted;
7412
- for (let i = 0; i < length; i++) try {
7413
- const attrs = _instructions(result, keys[i].trim());
7414
- decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
7415
- break;
7416
- } catch (error) {
7417
- if (i + 1 >= length) throw error;
7418
- }
7419
- return DotenvModule.parse(decrypted);
7420
- }
7421
- function _warn(message) {
7422
- console.error(`[dotenv@${version}][WARN] ${message}`);
7423
- }
7424
- function _debug(message) {
7425
- console.log(`[dotenv@${version}][DEBUG] ${message}`);
7426
- }
7427
- function _log(message) {
7428
- console.log(`[dotenv@${version}] ${message}`);
7429
- }
7430
- function _dotenvKey(options) {
7431
- if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
7432
- if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
7433
- return "";
7174
+ this.server.close(() => {
7175
+ try {
7176
+ unlinkSync(this.socketPath);
7177
+ } catch {}
7178
+ logger$1.info("IPC server stopped");
7179
+ resolve();
7180
+ });
7181
+ for (const conn of this.connections.values()) conn.socket.destroy();
7182
+ this.connections.clear();
7183
+ });
7434
7184
  }
7435
- function _instructions(result, dotenvKey) {
7436
- let uri;
7437
- try {
7438
- uri = new URL(dotenvKey);
7439
- } catch (error) {
7440
- if (error.code === "ERR_INVALID_URL") {
7441
- const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
7442
- err.code = "INVALID_DOTENV_KEY";
7443
- throw err;
7444
- }
7445
- throw error;
7446
- }
7447
- const key = uri.password;
7185
+ /**
7186
+ * Send an IPC request to a specific plugin and wait for response.
7187
+ */
7188
+ async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
7189
+ const conn = this.connections.get(pluginId);
7190
+ if (!conn?.info) return {
7191
+ id: "",
7192
+ ok: false,
7193
+ error: {
7194
+ code: "PLUGIN_NOT_CONNECTED",
7195
+ message: `Plugin ${pluginId} not connected`
7196
+ }
7197
+ };
7198
+ const id = pluginConnectionId();
7199
+ const request = {
7200
+ type: "req",
7201
+ id,
7202
+ method,
7203
+ params
7204
+ };
7205
+ return new Promise((resolve, reject) => {
7206
+ const timer = setTimeout(() => {
7207
+ conn.pending.delete(id);
7208
+ resolve({
7209
+ id,
7210
+ ok: false,
7211
+ error: {
7212
+ code: "TIMEOUT",
7213
+ message: `Request ${method} timed out after ${String(timeoutMs)}ms`
7214
+ }
7215
+ });
7216
+ }, timeoutMs);
7217
+ conn.pending.set(id, {
7218
+ resolve,
7219
+ reject,
7220
+ timer
7221
+ });
7222
+ try {
7223
+ conn.socket.write(JSON.stringify(request) + "\n");
7224
+ } catch (err) {
7225
+ clearTimeout(timer);
7226
+ conn.pending.delete(id);
7227
+ resolve({
7228
+ id,
7229
+ ok: false,
7230
+ error: {
7231
+ code: "SEND_FAILED",
7232
+ message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
7233
+ }
7234
+ });
7235
+ }
7236
+ });
7237
+ }
7238
+ /**
7239
+ * Send an IPC request to ALL registered plugins.
7240
+ * Returns responses keyed by plugin ID.
7241
+ */
7242
+ async broadcastRequest(method, params, timeoutMs = 3e4) {
7243
+ const results = /* @__PURE__ */ new Map();
7244
+ const registered = this.getRegisteredPlugins();
7245
+ await Promise.all(registered.map(async ([pluginId]) => {
7246
+ const response = await this.sendRequest(pluginId, method, params, timeoutMs);
7247
+ results.set(pluginId, response);
7248
+ }));
7249
+ return results;
7250
+ }
7251
+ /**
7252
+ * Send an event to a specific plugin (fire-and-forget).
7253
+ */
7254
+ sendEventToPlugin(pluginId, event, payload) {
7255
+ const conn = this.connections.get(pluginId);
7256
+ if (!conn) return false;
7257
+ return this.sendEvent(conn, event, payload);
7258
+ }
7259
+ /**
7260
+ * Broadcast an event to all connected plugins.
7261
+ */
7262
+ broadcastEvent(event, payload) {
7263
+ for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
7264
+ }
7265
+ /**
7266
+ * Get all registered plugins and their info.
7267
+ */
7268
+ getRegisteredPlugins() {
7269
+ const plugins = [];
7270
+ for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
7271
+ return plugins;
7272
+ }
7273
+ /**
7274
+ * Get number of connected plugins (including unregistered).
7275
+ */
7276
+ get connectionCount() {
7277
+ return this.connections.size;
7278
+ }
7279
+ /**
7280
+ * Get number of registered plugins.
7281
+ */
7282
+ get registeredCount() {
7283
+ let count = 0;
7284
+ for (const conn of this.connections.values()) if (conn.info) count++;
7285
+ return count;
7286
+ }
7287
+ handleConnection(socket) {
7288
+ const connId = pluginConnectionId();
7289
+ const conn = {
7290
+ id: connId,
7291
+ socket,
7292
+ info: null,
7293
+ buffer: "",
7294
+ pending: /* @__PURE__ */ new Map()
7295
+ };
7296
+ this.connections.set(connId, conn);
7297
+ logger$1.info({ connId }, "IPC: new connection");
7298
+ socket.on("data", (data) => {
7299
+ conn.buffer += data.toString();
7300
+ this.processBuffer(conn);
7301
+ if (Buffer.byteLength(conn.buffer, "utf8") > 1048576) this.closeOversizedConnection(conn);
7302
+ });
7303
+ socket.on("close", () => {
7304
+ logger$1.info({
7305
+ connId,
7306
+ plugin: conn.info?.name
7307
+ }, "IPC: connection closed");
7308
+ for (const [, pending] of conn.pending) {
7309
+ clearTimeout(pending.timer);
7310
+ pending.resolve({
7311
+ id: "",
7312
+ ok: false,
7313
+ error: {
7314
+ code: "DISCONNECTED",
7315
+ message: "Plugin disconnected"
7316
+ }
7317
+ });
7318
+ }
7319
+ this.connections.delete(connId);
7320
+ });
7321
+ socket.on("error", (err) => {
7322
+ logger$1.error({
7323
+ connId,
7324
+ err: err.message
7325
+ }, "IPC: socket error");
7326
+ });
7327
+ }
7328
+ processBuffer(conn) {
7329
+ let newlineIdx;
7330
+ while ((newlineIdx = conn.buffer.indexOf("\n")) !== -1) {
7331
+ const rawLine = conn.buffer.slice(0, newlineIdx);
7332
+ conn.buffer = conn.buffer.slice(newlineIdx + 1);
7333
+ if (Buffer.byteLength(rawLine, "utf8") > 1048576) {
7334
+ this.closeOversizedConnection(conn);
7335
+ return;
7336
+ }
7337
+ const line = rawLine.trim();
7338
+ if (!line) continue;
7339
+ let parsed;
7340
+ try {
7341
+ parsed = JSON.parse(line);
7342
+ } catch {
7343
+ logger$1.warn({ connId: conn.id }, "IPC: invalid JSON from plugin");
7344
+ continue;
7345
+ }
7346
+ if (isIPCResponse(parsed)) {
7347
+ this.handlePluginResponse(conn, parsed);
7348
+ continue;
7349
+ }
7350
+ if (isIPCRequest(parsed)) {
7351
+ this.handlePluginRequest(conn, parsed);
7352
+ continue;
7353
+ }
7354
+ logger$1.debug({
7355
+ connId: conn.id,
7356
+ msg: parsed
7357
+ }, "IPC: unhandled message");
7358
+ }
7359
+ }
7360
+ closeOversizedConnection(conn) {
7361
+ logger$1.warn({
7362
+ connId: conn.id,
7363
+ maxBytes: MAX_IPC_MESSAGE_BYTES
7364
+ }, "IPC: message exceeded size limit — closing connection");
7365
+ conn.buffer = "";
7366
+ conn.socket.destroy();
7367
+ }
7368
+ handlePluginResponse(conn, response) {
7369
+ const pending = conn.pending.get(response.id);
7370
+ if (!pending) return;
7371
+ clearTimeout(pending.timer);
7372
+ conn.pending.delete(response.id);
7373
+ pending.resolve(response);
7374
+ }
7375
+ async handlePluginRequest(conn, request) {
7376
+ if (conn.info) conn.info.lastSeen = Date.now();
7377
+ if (request.method === "register") {
7378
+ this.handleRegister(conn, request);
7379
+ return;
7380
+ }
7381
+ if (this.requestHandler) try {
7382
+ const result = await this.requestHandler(request.method, request.params, conn.id);
7383
+ const response = result.ok ? createIPCResponse(request.id, result.payload) : createIPCError(request.id, result.error?.code ?? "UNKNOWN", result.error?.message ?? "Unknown error");
7384
+ this.sendResponse(conn, response);
7385
+ } catch (err) {
7386
+ const message = err instanceof Error ? err.message : String(err);
7387
+ this.sendResponse(conn, createIPCError(request.id, "INTERNAL", message));
7388
+ }
7389
+ else this.sendResponse(conn, createIPCError(request.id, "NO_HANDLER", `No handler for method: ${request.method}`));
7390
+ }
7391
+ handleRegister(conn, request) {
7392
+ const { name, version, protocolVersion, capabilities, pid } = request.params;
7393
+ const validCapabilities = capabilities === void 0 || Array.isArray(capabilities) && capabilities.length <= 256 && capabilities.every((capability) => typeof capability === "string" && capability.trim().length > 0 && capability.length <= 256);
7394
+ const validPid = pid === void 0 || Number.isSafeInteger(pid) && pid > 0;
7395
+ const validProtocolVersion = protocolVersion === void 0 || Number.isSafeInteger(protocolVersion);
7396
+ if (typeof name !== "string" || name.trim().length === 0 || name.length > 256 || typeof version !== "string" || version.trim().length === 0 || version.length > 256 || !validCapabilities || !validPid || !validProtocolVersion) {
7397
+ this.sendResponse(conn, createIPCError(request.id, "INVALID_REGISTER", "Invalid plugin registration fields"));
7398
+ return;
7399
+ }
7400
+ if (protocolVersion !== void 0 && protocolVersion !== 1) {
7401
+ this.sendResponse(conn, createIPCError(request.id, "PROTOCOL_MISMATCH", `Unsupported protocol version ${String(protocolVersion)}. Server supports v${String(1)}.`));
7402
+ return;
7403
+ }
7404
+ const staleIds = [];
7405
+ for (const [existingId, existing] of this.connections) if (existingId !== conn.id && existing.info?.name === name) if (pid != null && existing.info.pid === pid) {
7406
+ logger$1.info({
7407
+ staleConnId: existingId,
7408
+ newConnId: conn.id,
7409
+ plugin: name,
7410
+ pid
7411
+ }, "IPC: closing stale duplicate connection (same PID)");
7412
+ existing.socket.destroy();
7413
+ staleIds.push(existingId);
7414
+ } else {
7415
+ logger$1.info({
7416
+ staleConnId: existingId,
7417
+ newConnId: conn.id,
7418
+ plugin: name,
7419
+ oldPid: existing.info.pid,
7420
+ newPid: pid
7421
+ }, "IPC: replacing connection from different process");
7422
+ this.sendEvent(existing, "plugin.replaced", {
7423
+ reason: "Another process registered with the same plugin name",
7424
+ replacedBy: conn.id
7425
+ });
7426
+ existing.socket.end();
7427
+ staleIds.push(existingId);
7428
+ }
7429
+ for (const id of staleIds) this.connections.delete(id);
7430
+ conn.info = {
7431
+ name,
7432
+ version,
7433
+ protocolVersion: protocolVersion ?? 1,
7434
+ capabilities: capabilities ? [...capabilities] : [],
7435
+ pid: pid ?? null,
7436
+ connectedAt: Date.now(),
7437
+ lastSeen: Date.now()
7438
+ };
7439
+ logger$1.info({
7440
+ connId: conn.id,
7441
+ plugin: name,
7442
+ version,
7443
+ capabilities
7444
+ }, "IPC: plugin registered");
7445
+ this.sendResponse(conn, createIPCResponse(request.id, {
7446
+ status: "registered",
7447
+ daemonVersion: this.daemonVersion,
7448
+ protocolVersion: 1
7449
+ }));
7450
+ }
7451
+ sendResponse(conn, response) {
7452
+ try {
7453
+ conn.socket.write(JSON.stringify(response) + "\n");
7454
+ } catch (err) {
7455
+ logger$1.error({
7456
+ connId: conn.id,
7457
+ err
7458
+ }, "IPC: failed to send response");
7459
+ }
7460
+ }
7461
+ sendEvent(conn, event, payload) {
7462
+ try {
7463
+ const msg = createIPCEvent(event, payload);
7464
+ conn.socket.write(JSON.stringify(msg) + "\n");
7465
+ return true;
7466
+ } catch {
7467
+ return false;
7468
+ }
7469
+ }
7470
+ };
7471
+ //#endregion
7472
+ //#region src/daemon-bootstrap.ts
7473
+ /**
7474
+ * Daemon bootstrap helpers — startup ordering, config/version resolution,
7475
+ * AI-proxy + IPC server wiring.
7476
+ *
7477
+ * Extracted from daemon.ts (which remains the orchestrator). These are the
7478
+ * standalone pieces of `startDaemon()`: version detection, the one-shot
7479
+ * persona-file migration, the AI proxy listener, and the IPC server start.
7480
+ */
7481
+ const execFileAsync$2 = promisify(execFile);
7482
+ /**
7483
+ * Resolve the installed @alfe.ai/cli version.
7484
+ *
7485
+ * Strategy (in order):
7486
+ * 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
7487
+ * 2. Walk up from this file to find @alfe.ai/cli/package.json
7488
+ * (works when systemd runs the gateway binary directly, since
7489
+ * the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
7490
+ */
7491
+ async function getCliVersion() {
7492
+ if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
7493
+ try {
7494
+ const { fileURLToPath } = await import("node:url");
7495
+ const { dirname } = await import("node:path");
7496
+ let dir = dirname(fileURLToPath(import.meta.url));
7497
+ for (let i = 0; i < 10; i++) {
7498
+ const candidate = join(dir, "package.json");
7499
+ try {
7500
+ const raw = await readFile(candidate, "utf-8");
7501
+ const pkg = JSON.parse(raw);
7502
+ if (pkg.name === "@alfe.ai/cli") return pkg.version;
7503
+ } catch {}
7504
+ const parent = dirname(dir);
7505
+ if (parent === dir) break;
7506
+ dir = parent;
7507
+ }
7508
+ } catch {}
7509
+ logger$1.debug("Could not resolve @alfe.ai/cli version");
7510
+ }
7511
+ /**
7512
+ * Per-runtime "print installed version" commands. Local to the gateway — we do
7513
+ * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
7514
+ * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
7515
+ * connection-status report degrades gracefully instead of crashing.
7516
+ */
7517
+ const RUNTIME_VERSION_COMMANDS = {
7518
+ openclaw: {
7519
+ command: "openclaw",
7520
+ args: ["--version"]
7521
+ },
7522
+ hermes: {
7523
+ command: "hermes",
7524
+ args: ["version"]
7525
+ },
7526
+ "claude-code": {
7527
+ command: "alfe-claude-host",
7528
+ args: ["--version"]
7529
+ }
7530
+ };
7531
+ /**
7532
+ * Pure resolver (exported for tests) — the per-runtime version command, or
7533
+ * `undefined` for an unknown runtime.
7534
+ */
7535
+ function resolveRuntimeVersionCommand(runtime) {
7536
+ return RUNTIME_VERSION_COMMANDS[runtime];
7537
+ }
7538
+ /**
7539
+ * Resolve the installed runtime version by running the per-runtime version
7540
+ * command and returning the trimmed stdout. An unknown runtime (no command in
7541
+ * the map) returns `undefined` without spawning — it must never throw.
7542
+ */
7543
+ async function getRuntimeVersion(runtime) {
7544
+ const cmd = resolveRuntimeVersionCommand(runtime);
7545
+ if (!cmd) {
7546
+ logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
7547
+ return;
7548
+ }
7549
+ try {
7550
+ const { stdout } = await execFileAsync$2(cmd.command, cmd.args);
7551
+ return stdout.trim() || void 0;
7552
+ } catch {
7553
+ logger$1.debug({ runtime }, "Could not resolve runtime version");
7554
+ return;
7555
+ }
7556
+ }
7557
+ /**
7558
+ * Flush pino's async transport and exit.
7559
+ * process.exit() can drop buffered log lines — this ensures they're written first.
7560
+ */
7561
+ async function flushAndExit(code) {
7562
+ await flushSentry();
7563
+ await new Promise((resolve) => {
7564
+ logger$1.flush();
7565
+ setTimeout(resolve, 500);
7566
+ });
7567
+ process.exit(code);
7568
+ }
7569
+ /**
7570
+ * One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
7571
+ * fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
7572
+ * OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
7573
+ * (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
7574
+ * workspace already has the file. Idempotent and safe to run on every daemon start.
7575
+ */
7576
+ const PERSONA_FILES = [
7577
+ "SOUL.md",
7578
+ "IDENTITY.md",
7579
+ "BOOTSTRAP.md",
7580
+ "AGENTS.md"
7581
+ ];
7582
+ async function migrateLegacyPersonaFiles(home, agentWorkspace) {
7583
+ if (home === agentWorkspace) return;
7584
+ let moved = 0;
7585
+ let skipped = 0;
7586
+ for (const filename of PERSONA_FILES) {
7587
+ const src = join(home, filename);
7588
+ const dst = join(agentWorkspace, filename);
7589
+ try {
7590
+ await stat(src);
7591
+ } catch {
7592
+ continue;
7593
+ }
7594
+ try {
7595
+ await stat(dst);
7596
+ skipped++;
7597
+ continue;
7598
+ } catch {}
7599
+ try {
7600
+ await mkdir(agentWorkspace, { recursive: true });
7601
+ await rename(src, dst);
7602
+ moved++;
7603
+ } catch (err) {
7604
+ logger$1.warn({
7605
+ src,
7606
+ dst,
7607
+ err: err instanceof Error ? err.message : String(err)
7608
+ }, "Persona file migration: rename failed");
7609
+ }
7610
+ }
7611
+ if (moved > 0 || skipped > 0) logger$1.info({
7612
+ home,
7613
+ agentWorkspace,
7614
+ moved,
7615
+ skipped
7616
+ }, "Persona file backfill migration complete");
7617
+ }
7618
+ /**
7619
+ * Start the local AI proxy (before the runtime so LLM requests work
7620
+ * immediately). Failure is non-fatal — the daemon starts anyway and LLM
7621
+ * requests fail until the proxy is fixed.
7622
+ */
7623
+ async function startAiProxy(apiKey) {
7624
+ logger$1.debug("Starting AI proxy...");
7625
+ const handle = {
7626
+ server: null,
7627
+ url: null,
7628
+ running: false
7629
+ };
7630
+ try {
7631
+ const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
7632
+ const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
7633
+ const proxyUrl = getAiServiceUrlFromToken(apiKey);
7634
+ const port = DEFAULT_AI_PROXY_PORT ?? 18193;
7635
+ handle.server = createProxyServer({
7636
+ port,
7637
+ apiKey,
7638
+ proxyUrl
7639
+ });
7640
+ const server = handle.server;
7641
+ await new Promise((resolve, reject) => {
7642
+ server.listen(port, "127.0.0.1", () => {
7643
+ handle.running = true;
7644
+ handle.url = `http://127.0.0.1:${String(port)}`;
7645
+ logger$1.info({
7646
+ port,
7647
+ upstream: proxyUrl
7648
+ }, "AI proxy started");
7649
+ resolve();
7650
+ });
7651
+ server.on("error", reject);
7652
+ });
7653
+ } catch (err) {
7654
+ const message = err instanceof Error ? err.message : String(err);
7655
+ const stack = err instanceof Error ? err.stack : void 0;
7656
+ logger$1.error({
7657
+ err: message,
7658
+ stack
7659
+ }, "Failed to start AI proxy — LLM requests will fail");
7660
+ }
7661
+ return handle;
7662
+ }
7663
+ /**
7664
+ * Start the IPC server (runtime plugins connect via this socket). A failure
7665
+ * here is fatal — plugins have no other path to the daemon — so this flushes
7666
+ * logs and exits instead of returning.
7667
+ */
7668
+ async function startIpcServer(socketPath, requestHandler, daemonVersion) {
7669
+ logger$1.debug({ socketPath }, "Starting IPC server...");
7670
+ const ipcServer = new IPCServer(socketPath, daemonVersion);
7671
+ ipcServer.setRequestHandler(requestHandler);
7672
+ try {
7673
+ await ipcServer.start();
7674
+ logger$1.debug("IPC server started");
7675
+ } catch (err) {
7676
+ const message = err instanceof Error ? err.message : String(err);
7677
+ const stack = err instanceof Error ? err.stack : void 0;
7678
+ logger$1.error({
7679
+ err: message,
7680
+ stack
7681
+ }, "Failed to start IPC server");
7682
+ await flushAndExit(1);
7683
+ }
7684
+ return ipcServer;
7685
+ }
7686
+ //#endregion
7687
+ //#region src/mcp-handlers.ts
7688
+ /**
7689
+ * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
7690
+ * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
7691
+ * cheap (in-memory snapshot, no I/O).
7692
+ */
7693
+ function handleMcpListTools(bundler) {
7694
+ if (!bundler) return {
7695
+ ok: false,
7696
+ error: {
7697
+ code: "MCP_BUNDLER_UNAVAILABLE",
7698
+ message: "MCP bundler not initialized"
7699
+ }
7700
+ };
7701
+ return {
7702
+ ok: true,
7703
+ payload: { tools: bundler.listTools() }
7704
+ };
7705
+ }
7706
+ /**
7707
+ * Observability backstop for the INTEGRATION warm path. `applyForIntegration`
7708
+ * registers MCP servers into the store and the daemon warms them silently via
7709
+ * the store-change `onChange` hook — so a server that fails its connect (e.g.
7710
+ * an MCP server whose backing account/credential wasn't resolvable at warm
7711
+ * time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
7712
+ * making the black hole undiagnosable. After a warm, read `bundler.statuses()`
7713
+ * and warn once per still-failed server so the failure is visible. These
7714
+ * self-heal on the bundler's background retry sweep once the dependency
7715
+ * appears. Returns the servers it warned about (for tests / callers).
7716
+ */
7717
+ function warnFailedMcpServers(bundler, log, reason) {
7718
+ if (!bundler) return [];
7719
+ const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
7720
+ for (const status of failed) log.warn({
7721
+ server: status.name,
7722
+ reason,
7723
+ consecutiveFailures: status.consecutiveFailures,
7724
+ lastError: status.lastError
7725
+ }, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
7726
+ return failed;
7727
+ }
7728
+ /**
7729
+ * Route a tool call to the appropriate MCP child via the daemon-hosted
7730
+ * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
7731
+ * the raw JSON object the LLM produced.
7732
+ */
7733
+ /**
7734
+ * List every server entry in the alfe bundler store. Lets agents inspect
7735
+ * what they've already registered before adding a new one.
7736
+ */
7737
+ function handleMcpListServers(manager) {
7738
+ if (!manager) return {
7739
+ ok: false,
7740
+ error: {
7741
+ code: "MCP_MANAGER_UNAVAILABLE",
7742
+ message: "MCP manager not initialized"
7743
+ }
7744
+ };
7745
+ const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
7746
+ const byName = new Map(statuses.map((s) => [s.name, s]));
7747
+ return {
7748
+ ok: true,
7749
+ payload: { servers: manager.listServers().map(({ id, entry }) => ({
7750
+ id,
7751
+ entry: toPublicServerEntry(entry),
7752
+ fingerprint: serverLaunchFingerprint(entry),
7753
+ status: byName.get(id) ?? {
7754
+ name: id,
7755
+ connected: false,
7756
+ toolCount: 0,
7757
+ consecutiveFailures: 0
7758
+ }
7759
+ })) }
7760
+ };
7761
+ }
7762
+ function toPublicServerEntry(entry) {
7763
+ return {
7764
+ owner: entry.owner,
7765
+ addedAt: entry.addedAt,
7766
+ transport: entry.transport,
7767
+ ...entry.version !== void 0 ? { version: entry.version } : {}
7768
+ };
7769
+ }
7770
+ /**
7771
+ * How long the add-confirm probe waits for the freshly-added server to
7772
+ * connect before replying. This is effectively the whole budget for
7773
+ * `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
7774
+ * BEFORE resolving, so the Connection object already exists and
7775
+ * `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
7776
+ * plugin caller's 30s IPC timeout. A slower server still connects in the
7777
+ * background and surfaces on the next `alfe_mcp_list_tools` — the probe just
7778
+ * reports what it saw within the window; a missed connect is re-attempted by
7779
+ * the bundler's `retryNeverConnected` sweep.
7780
+ */
7781
+ const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
7782
+ /**
7783
+ * Register a new MCP server in the alfe bundler store on behalf of the agent,
7784
+ * then CONFIRM the connect before replying so the agent learns whether the
7785
+ * server actually works. Owned as `'manual'` so the agent can later remove it
7786
+ * without an expectedOwner conflict — matches what `alfe mcp add` does from the
7787
+ * CLI. Registration is durable regardless of the probe outcome: a probe
7788
+ * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
7789
+ * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
7790
+ */
7791
+ async function handleMcpAddServer(params, manager) {
7792
+ if (!manager) return {
7793
+ ok: false,
7794
+ error: {
7795
+ code: "MCP_MANAGER_UNAVAILABLE",
7796
+ message: "MCP manager not initialized"
7797
+ }
7798
+ };
7799
+ const p = params;
7800
+ if (typeof p.id !== "string" || p.id.length === 0) return {
7801
+ ok: false,
7802
+ error: {
7803
+ code: "INVALID_PARAMS",
7804
+ message: "id is required (string)"
7805
+ }
7806
+ };
7807
+ const config = buildServerConfig(p);
7808
+ if (!config) return {
7809
+ ok: false,
7810
+ error: {
7811
+ code: "INVALID_PARAMS",
7812
+ message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
7813
+ }
7814
+ };
7815
+ try {
7816
+ await manager.addServer(config, {
7817
+ id: p.id,
7818
+ owner: "manual"
7819
+ });
7820
+ } catch (err) {
7821
+ const message = err instanceof Error ? err.message : String(err);
7822
+ logger$1.warn({
7823
+ id: p.id,
7824
+ err: message
7825
+ }, "mcp.add_server failed");
7826
+ return {
7827
+ ok: false,
7828
+ error: {
7829
+ code: "MCP_ADD_FAILED",
7830
+ message
7831
+ }
7832
+ };
7833
+ }
7834
+ let status = null;
7835
+ if (manager.warmServer) try {
7836
+ status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
7837
+ } catch (err) {
7838
+ logger$1.warn({
7839
+ id: p.id,
7840
+ err: err instanceof Error ? err.message : String(err)
7841
+ }, "mcp.add_server warm probe threw");
7842
+ }
7843
+ return {
7844
+ ok: true,
7845
+ payload: {
7846
+ id: p.id,
7847
+ connected: status?.connected ?? false,
7848
+ toolCount: status?.toolCount ?? 0,
7849
+ ...status?.lastError !== void 0 ? { error: status.lastError } : {}
7850
+ }
7851
+ };
7852
+ }
7853
+ function buildServerConfig(p) {
7854
+ if (typeof p.command === "string" && p.command.length > 0) {
7855
+ const cfg = { command: p.command };
7856
+ if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
7857
+ if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
7858
+ const env = {};
7859
+ for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
7860
+ cfg.env = env;
7861
+ }
7862
+ if (typeof p.cwd === "string") cfg.cwd = p.cwd;
7863
+ return cfg;
7864
+ }
7865
+ if (typeof p.url === "string" && p.url.length > 0) {
7866
+ const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
7867
+ const cfg = {
7868
+ url: p.url,
7869
+ transport
7870
+ };
7871
+ if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
7872
+ const headers = {};
7873
+ for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
7874
+ cfg.headers = headers;
7875
+ }
7876
+ return cfg;
7877
+ }
7878
+ return null;
7879
+ }
7880
+ /**
7881
+ * Drop a server entry the agent previously registered. Restricted to
7882
+ * `manual`-owned entries so the agent can't accidentally clobber
7883
+ * integration-installed or CLI-installed servers (the daemon owns
7884
+ * those; the agent can ask the user to uninstall an integration via
7885
+ * the dashboard).
7886
+ */
7887
+ async function handleMcpRemoveServer(params, manager) {
7888
+ if (!manager) return {
7889
+ ok: false,
7890
+ error: {
7891
+ code: "MCP_MANAGER_UNAVAILABLE",
7892
+ message: "MCP manager not initialized"
7893
+ }
7894
+ };
7895
+ const { id } = params;
7896
+ if (typeof id !== "string" || id.length === 0) return {
7897
+ ok: false,
7898
+ error: {
7899
+ code: "INVALID_PARAMS",
7900
+ message: "id is required (string)"
7901
+ }
7902
+ };
7903
+ try {
7904
+ return {
7905
+ ok: true,
7906
+ payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
7907
+ };
7908
+ } catch (err) {
7909
+ const message = err instanceof Error ? err.message : String(err);
7910
+ if (message.includes("owned by")) return {
7911
+ ok: false,
7912
+ error: {
7913
+ code: "MCP_OWNER_MISMATCH",
7914
+ message
7915
+ }
7916
+ };
7917
+ logger$1.warn({
7918
+ id,
7919
+ err: message
7920
+ }, "mcp.remove_server failed");
7921
+ return {
7922
+ ok: false,
7923
+ error: {
7924
+ code: "MCP_REMOVE_FAILED",
7925
+ message
7926
+ }
7927
+ };
7928
+ }
7929
+ }
7930
+ async function handleMcpCallTool(bundler, params) {
7931
+ if (!bundler) return {
7932
+ ok: false,
7933
+ error: {
7934
+ code: "MCP_BUNDLER_UNAVAILABLE",
7935
+ message: "MCP bundler not initialized"
7936
+ }
7937
+ };
7938
+ const { name, args } = params;
7939
+ if (typeof name !== "string" || name.length === 0) return {
7940
+ ok: false,
7941
+ error: {
7942
+ code: "INVALID_PARAMS",
7943
+ message: "name is required (string)"
7944
+ }
7945
+ };
7946
+ try {
7947
+ return {
7948
+ ok: true,
7949
+ payload: await bundler.callTool(name, args)
7950
+ };
7951
+ } catch (err) {
7952
+ const message = err instanceof Error ? err.message : String(err);
7953
+ logger$1.warn({
7954
+ tool: name,
7955
+ err: message
7956
+ }, "mcp.call_tool failed");
7957
+ return {
7958
+ ok: false,
7959
+ error: {
7960
+ code: "MCP_CALL_FAILED",
7961
+ message
7962
+ }
7963
+ };
7964
+ }
7965
+ }
7966
+ //#endregion
7967
+ //#region src/integration-bootstrap.ts
7968
+ /**
7969
+ * Adapter from `AgentApiClient`'s per-provider methods to the
7970
+ * `CredentialsResolver` shape the MCP applier expects. Returns
7971
+ * `undefined` for unknown providers and on 404/network error so the
7972
+ * applier can skip registration silently (its documented contract).
7973
+ */
7974
+ async function fetchProviderCredentials(agentApi, provider, connectionId) {
7975
+ if (connectionId) try {
7976
+ const raw = await agentApi.getConnectionCredentials(connectionId);
7977
+ const fields = { ...raw.providerMetadata ?? {} };
7978
+ if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
7979
+ const bundle = JSON.parse(raw.accessToken);
7980
+ if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
7981
+ } catch (err) {
7982
+ logger$1.warn({
7983
+ connectionId,
7984
+ err: err instanceof Error ? err.message : String(err)
7985
+ }, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
7986
+ fields.accessToken = raw.accessToken;
7987
+ }
7988
+ return fields;
7989
+ } catch (err) {
7990
+ logger$1.warn({
7991
+ connectionId,
7992
+ err: err instanceof Error ? err.message : String(err)
7993
+ }, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
7994
+ return;
7995
+ }
7996
+ const key = provider.toLowerCase();
7997
+ try {
7998
+ switch (key) {
7999
+ case "atlassian": return await agentApi.getAtlassianCredentials();
8000
+ case "github": return await agentApi.getGithubCredentials();
8001
+ case "xero": return await agentApi.getXeroCredentials();
8002
+ case "notion": return await agentApi.getNotionCredentials();
8003
+ case "myob": return await agentApi.getMYOBCredentials();
8004
+ case "google": return await agentApi.getGoogleCredentials();
8005
+ default:
8006
+ logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
8007
+ return;
8008
+ }
8009
+ } catch {
8010
+ return;
8011
+ }
8012
+ }
8013
+ /**
8014
+ * Post-reconcile trigger: rebuild the command registry from the manager's
8015
+ * active integrations and report the available commands to the cloud
8016
+ * gateway. Wired by daemon.ts as the cloud client's
8017
+ * `onReconciliationComplete` callback.
8018
+ */
8019
+ function reportAvailableCommands(deps) {
8020
+ const { integrationManager, commandRegistry, cloudClient } = deps;
8021
+ try {
8022
+ const activeCommands = integrationManager.getActiveCommands();
8023
+ commandRegistry.clear();
8024
+ for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
8025
+ const commandsMsg = {
8026
+ type: "COMMANDS_AVAILABLE",
8027
+ commands: commandRegistry.listCommands()
8028
+ };
8029
+ cloudClient.sendMessage(commandsMsg);
8030
+ logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
8031
+ } catch (err) {
8032
+ logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
8033
+ }
8034
+ }
8035
+ //#endregion
8036
+ //#region ../../node_modules/.pnpm/dotenv@17.2.2/node_modules/dotenv/package.json
8037
+ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8038
+ module.exports = {
8039
+ "name": "dotenv",
8040
+ "version": "17.2.2",
8041
+ "description": "Loads environment variables from .env file",
8042
+ "main": "lib/main.js",
8043
+ "types": "lib/main.d.ts",
8044
+ "exports": {
8045
+ ".": {
8046
+ "types": "./lib/main.d.ts",
8047
+ "require": "./lib/main.js",
8048
+ "default": "./lib/main.js"
8049
+ },
8050
+ "./config": "./config.js",
8051
+ "./config.js": "./config.js",
8052
+ "./lib/env-options": "./lib/env-options.js",
8053
+ "./lib/env-options.js": "./lib/env-options.js",
8054
+ "./lib/cli-options": "./lib/cli-options.js",
8055
+ "./lib/cli-options.js": "./lib/cli-options.js",
8056
+ "./package.json": "./package.json"
8057
+ },
8058
+ "scripts": {
8059
+ "dts-check": "tsc --project tests/types/tsconfig.json",
8060
+ "lint": "standard",
8061
+ "pretest": "npm run lint && npm run dts-check",
8062
+ "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
8063
+ "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
8064
+ "prerelease": "npm test",
8065
+ "release": "standard-version"
8066
+ },
8067
+ "repository": {
8068
+ "type": "git",
8069
+ "url": "git://github.com/motdotla/dotenv.git"
8070
+ },
8071
+ "homepage": "https://github.com/motdotla/dotenv#readme",
8072
+ "funding": "https://dotenvx.com",
8073
+ "keywords": [
8074
+ "dotenv",
8075
+ "env",
8076
+ ".env",
8077
+ "environment",
8078
+ "variables",
8079
+ "config",
8080
+ "settings"
8081
+ ],
8082
+ "readmeFilename": "README.md",
8083
+ "license": "BSD-2-Clause",
8084
+ "devDependencies": {
8085
+ "@types/node": "^18.11.3",
8086
+ "decache": "^4.6.2",
8087
+ "sinon": "^14.0.1",
8088
+ "standard": "^17.0.0",
8089
+ "standard-version": "^9.5.0",
8090
+ "tap": "^19.2.0",
8091
+ "typescript": "^4.8.4"
8092
+ },
8093
+ "engines": { "node": ">=12" },
8094
+ "browser": { "fs": false }
8095
+ };
8096
+ }));
8097
+ (/* @__PURE__ */ __commonJSMin(((exports, module) => {
8098
+ const fs$1 = __require("fs");
8099
+ const path$1 = __require("path");
8100
+ const os$1 = __require("os");
8101
+ const crypto$2 = __require("crypto");
8102
+ const version = require_package().version;
8103
+ const TIPS = [
8104
+ "🔐 encrypt with Dotenvx: https://dotenvx.com",
8105
+ "🔐 prevent committing .env to code: https://dotenvx.com/precommit",
8106
+ "🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
8107
+ "📡 observe env with Radar: https://dotenvx.com/radar",
8108
+ "📡 auto-backup env with Radar: https://dotenvx.com/radar",
8109
+ "📡 version env with Radar: https://dotenvx.com/radar",
8110
+ "🛠️ run anywhere with `dotenvx run -- yourcommand`",
8111
+ "⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
8112
+ "⚙️ enable debug logging with { debug: true }",
8113
+ "⚙️ override existing env vars with { override: true }",
8114
+ "⚙️ suppress all logs with { quiet: true }",
8115
+ "⚙️ write to custom object with { processEnv: myObject }",
8116
+ "⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
8117
+ ];
8118
+ function _getRandomTip() {
8119
+ return TIPS[Math.floor(Math.random() * TIPS.length)];
8120
+ }
8121
+ function parseBoolean(value) {
8122
+ if (typeof value === "string") return ![
8123
+ "false",
8124
+ "0",
8125
+ "no",
8126
+ "off",
8127
+ ""
8128
+ ].includes(value.toLowerCase());
8129
+ return Boolean(value);
8130
+ }
8131
+ function supportsAnsi() {
8132
+ return process.stdout.isTTY;
8133
+ }
8134
+ function dim(text) {
8135
+ return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text;
8136
+ }
8137
+ const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
8138
+ function parse(src) {
8139
+ const obj = {};
8140
+ let lines = src.toString();
8141
+ lines = lines.replace(/\r\n?/gm, "\n");
8142
+ let match;
8143
+ while ((match = LINE.exec(lines)) != null) {
8144
+ const key = match[1];
8145
+ let value = match[2] || "";
8146
+ value = value.trim();
8147
+ const maybeQuote = value[0];
8148
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
8149
+ if (maybeQuote === "\"") {
8150
+ value = value.replace(/\\n/g, "\n");
8151
+ value = value.replace(/\\r/g, "\r");
8152
+ }
8153
+ obj[key] = value;
8154
+ }
8155
+ return obj;
8156
+ }
8157
+ function _parseVault(options) {
8158
+ options = options || {};
8159
+ const vaultPath = _vaultPath(options);
8160
+ options.path = vaultPath;
8161
+ const result = DotenvModule.configDotenv(options);
8162
+ if (!result.parsed) {
8163
+ const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
8164
+ err.code = "MISSING_DATA";
8165
+ throw err;
8166
+ }
8167
+ const keys = _dotenvKey(options).split(",");
8168
+ const length = keys.length;
8169
+ let decrypted;
8170
+ for (let i = 0; i < length; i++) try {
8171
+ const attrs = _instructions(result, keys[i].trim());
8172
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
8173
+ break;
8174
+ } catch (error) {
8175
+ if (i + 1 >= length) throw error;
8176
+ }
8177
+ return DotenvModule.parse(decrypted);
8178
+ }
8179
+ function _warn(message) {
8180
+ console.error(`[dotenv@${version}][WARN] ${message}`);
8181
+ }
8182
+ function _debug(message) {
8183
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
8184
+ }
8185
+ function _log(message) {
8186
+ console.log(`[dotenv@${version}] ${message}`);
8187
+ }
8188
+ function _dotenvKey(options) {
8189
+ if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
8190
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
8191
+ return "";
8192
+ }
8193
+ function _instructions(result, dotenvKey) {
8194
+ let uri;
8195
+ try {
8196
+ uri = new URL(dotenvKey);
8197
+ } catch (error) {
8198
+ if (error.code === "ERR_INVALID_URL") {
8199
+ const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
8200
+ err.code = "INVALID_DOTENV_KEY";
8201
+ throw err;
8202
+ }
8203
+ throw error;
8204
+ }
8205
+ const key = uri.password;
7448
8206
  if (!key) {
7449
8207
  const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing key part");
7450
8208
  err.code = "INVALID_DOTENV_KEY";
@@ -22568,7 +23326,7 @@ var RuntimeProcess = class {
22568
23326
  * Start the runtime process.
22569
23327
  */
22570
23328
  start() {
22571
- if (this.stopped) return;
23329
+ if (this.stopped || this.child !== null) return;
22572
23330
  const { command, args } = this.resolveCommand();
22573
23331
  this.lastStartTime = Date.now();
22574
23332
  log$3.info({
@@ -22577,7 +23335,7 @@ var RuntimeProcess = class {
22577
23335
  args,
22578
23336
  workspace: this.options.workspace
22579
23337
  }, "Starting runtime process");
22580
- this.child = spawn(command, args, {
23338
+ const child = spawn(command, args, {
22581
23339
  cwd: this.options.workspace,
22582
23340
  env: {
22583
23341
  ...process.env,
@@ -22589,7 +23347,9 @@ var RuntimeProcess = class {
22589
23347
  "pipe"
22590
23348
  ]
22591
23349
  });
22592
- this.child.stdout?.on("data", (data) => {
23350
+ this.child = child;
23351
+ let terminalHandled = false;
23352
+ child.stdout.on("data", (data) => {
22593
23353
  const lines = data.toString().trim().split("\n");
22594
23354
  for (const line of lines) {
22595
23355
  log$3.info({
@@ -22599,7 +23359,7 @@ var RuntimeProcess = class {
22599
23359
  this.observeLine("stdout", line);
22600
23360
  }
22601
23361
  });
22602
- this.child.stderr?.on("data", (data) => {
23362
+ child.stderr.on("data", (data) => {
22603
23363
  const lines = data.toString().trim().split("\n");
22604
23364
  for (const line of lines) {
22605
23365
  log$3.warn({
@@ -22609,8 +23369,14 @@ var RuntimeProcess = class {
22609
23369
  this.observeLine("stderr", line);
22610
23370
  }
22611
23371
  });
22612
- this.child.on("exit", (code, signal) => {
22613
- this.child = null;
23372
+ const handleTermination = (code, signal, spawnError) => {
23373
+ if (terminalHandled) return;
23374
+ terminalHandled = true;
23375
+ if (this.child === child) this.child = null;
23376
+ if (spawnError) log$3.error({
23377
+ runtime: this.options.runtime,
23378
+ err: spawnError.message
23379
+ }, "Runtime process error");
22614
23380
  if (this.stopped) {
22615
23381
  log$3.info({
22616
23382
  runtime: this.options.runtime,
@@ -22619,7 +23385,7 @@ var RuntimeProcess = class {
22619
23385
  }, "Runtime stopped (expected)");
22620
23386
  return;
22621
23387
  }
22622
- if (code === 0 && signal == null) {
23388
+ if (!spawnError && code === 0 && signal == null) {
22623
23389
  log$3.info({
22624
23390
  runtime: this.options.runtime,
22625
23391
  code
@@ -22634,6 +23400,7 @@ var RuntimeProcess = class {
22634
23400
  runtime: this.options.runtime,
22635
23401
  code,
22636
23402
  signal,
23403
+ spawnError: spawnError?.message,
22637
23404
  backoffMs: this.backoffMs
22638
23405
  }, "Runtime crashed — scheduling restart with backoff");
22639
23406
  const uptime = Date.now() - this.lastStartTime;
@@ -22649,7 +23416,8 @@ var RuntimeProcess = class {
22649
23416
  signal,
22650
23417
  uptimeMs: uptime,
22651
23418
  recentOutput: this.ringBuffer.snapshot(),
22652
- crashesSuppressed: crash.suppressedCount
23419
+ crashesSuppressed: crash.suppressedCount,
23420
+ ...spawnError ? { spawnError } : {}
22653
23421
  });
22654
23422
  else log$3.debug({
22655
23423
  runtime: this.options.runtime,
@@ -22661,23 +23429,12 @@ var RuntimeProcess = class {
22661
23429
  this.start();
22662
23430
  }, this.backoffMs);
22663
23431
  this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
23432
+ };
23433
+ child.on("exit", (code, signal) => {
23434
+ handleTermination(code, signal);
22664
23435
  });
22665
- this.child.on("error", (err) => {
22666
- log$3.error({
22667
- runtime: this.options.runtime,
22668
- err: err.message
22669
- }, "Runtime process error");
22670
- if (this.stopped) return;
22671
- const crash = this.throttle.allowCrashCapture(Date.now() - this.lastStartTime);
22672
- if (crash.allow) captureRuntimeCrash({
22673
- runtime: this.options.runtime,
22674
- code: null,
22675
- signal: null,
22676
- uptimeMs: Date.now() - this.lastStartTime,
22677
- recentOutput: this.ringBuffer.snapshot(),
22678
- crashesSuppressed: crash.suppressedCount,
22679
- spawnError: err
22680
- });
23436
+ child.on("error", (err) => {
23437
+ handleTermination(null, null, err);
22681
23438
  });
22682
23439
  }
22683
23440
  /**
@@ -22746,14 +23503,19 @@ var RuntimeProcess = class {
22746
23503
  const child = this.child;
22747
23504
  if (!child) return;
22748
23505
  return new Promise((resolve) => {
23506
+ let settled = false;
23507
+ const finish = () => {
23508
+ if (settled) return;
23509
+ settled = true;
23510
+ clearTimeout(killTimer);
23511
+ resolve();
23512
+ };
22749
23513
  const killTimer = setTimeout(() => {
22750
23514
  log$3.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22751
23515
  child.kill("SIGKILL");
22752
23516
  }, 5e3);
22753
- child.on("exit", () => {
22754
- clearTimeout(killTimer);
22755
- resolve();
22756
- });
23517
+ child.once("exit", finish);
23518
+ child.once("error", finish);
22757
23519
  child.kill("SIGTERM");
22758
23520
  });
22759
23521
  }
@@ -22925,6 +23687,8 @@ var IpcTurnActivityProbe = class {
22925
23687
  * via ESM dynamic import on first use and cached until the registry is cleared.
22926
23688
  */
22927
23689
  const log$1 = createLogger("CommandRegistry");
23690
+ const DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
23691
+ const MAX_COMMAND_TIMEOUT_MS = 1800 * 1e3;
22928
23692
  var CommandRegistry = class {
22929
23693
  commands = /* @__PURE__ */ new Map();
22930
23694
  version = 0;
@@ -22932,7 +23696,7 @@ var CommandRegistry = class {
22932
23696
  * Register a command from an integration.
22933
23697
  * Rejects duplicate command names — first registration wins.
22934
23698
  */
22935
- register(integrationId, name, handlerPath, method = "handle", timeoutMs = 3e4) {
23699
+ register(integrationId, name, handlerPath, method = "handle", timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
22936
23700
  const existing = this.commands.get(name);
22937
23701
  if (existing) {
22938
23702
  log$1.warn({
@@ -22950,12 +23714,13 @@ var CommandRegistry = class {
22950
23714
  }, "Handler file does not exist — skipping registration");
22951
23715
  return;
22952
23716
  }
23717
+ const boundedTimeoutMs = Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_COMMAND_TIMEOUT_MS) : DEFAULT_COMMAND_TIMEOUT_MS;
22953
23718
  this.commands.set(name, {
22954
23719
  commandName: name,
22955
23720
  integrationId,
22956
23721
  handlerPath,
22957
23722
  handlerMethod: method,
22958
- timeoutMs,
23723
+ timeoutMs: boundedTimeoutMs,
22959
23724
  handler: null
22960
23725
  });
22961
23726
  log$1.info({
@@ -22963,7 +23728,7 @@ var CommandRegistry = class {
22963
23728
  integrationId,
22964
23729
  handlerPath,
22965
23730
  method,
22966
- timeoutMs
23731
+ timeoutMs: boundedTimeoutMs
22967
23732
  }, "Registered command");
22968
23733
  }
22969
23734
  /**
@@ -23023,9 +23788,10 @@ var CommandRegistry = class {
23023
23788
  }
23024
23789
  };
23025
23790
  }
23791
+ let timeout;
23026
23792
  try {
23027
23793
  return await Promise.race([entry.handler(payload, context), new Promise((_, reject) => {
23028
- setTimeout(() => {
23794
+ timeout = setTimeout(() => {
23029
23795
  reject(/* @__PURE__ */ new Error(`Command "${name}" timed out after ${String(entry.timeoutMs)}ms`));
23030
23796
  }, entry.timeoutMs);
23031
23797
  })]);
@@ -23042,6 +23808,8 @@ var CommandRegistry = class {
23042
23808
  message
23043
23809
  }
23044
23810
  };
23811
+ } finally {
23812
+ if (timeout) clearTimeout(timeout);
23045
23813
  }
23046
23814
  }
23047
23815
  /**
@@ -23091,13 +23859,18 @@ var CommandDedupe = class {
23091
23859
  duplicate: true
23092
23860
  };
23093
23861
  const promise = exec();
23094
- this.entries.set(commandId, {
23862
+ const entry = {
23095
23863
  insertedAt: this.now(),
23096
- promise
23097
- });
23864
+ promise,
23865
+ settled: false
23866
+ };
23867
+ this.entries.set(commandId, entry);
23098
23868
  this.evictOverCap();
23099
- promise.catch(() => {
23100
- this.entries.delete(commandId);
23869
+ promise.then(() => {
23870
+ entry.settled = true;
23871
+ this.evictOverCap();
23872
+ }, () => {
23873
+ if (this.entries.get(commandId) === entry) this.entries.delete(commandId);
23101
23874
  });
23102
23875
  return {
23103
23876
  ack: await promise,
@@ -23112,7 +23885,7 @@ var CommandDedupe = class {
23112
23885
  has(commandId) {
23113
23886
  const e = this.entries.get(commandId);
23114
23887
  if (!e) return false;
23115
- if (this.now() - e.insertedAt > this.ttlMs) {
23888
+ if (e.settled && this.now() - e.insertedAt > this.ttlMs) {
23116
23889
  this.entries.delete(commandId);
23117
23890
  return false;
23118
23891
  }
@@ -23120,13 +23893,13 @@ var CommandDedupe = class {
23120
23893
  }
23121
23894
  evictExpired() {
23122
23895
  const cutoff = this.now() - this.ttlMs;
23123
- for (const [id, entry] of this.entries) if (entry.insertedAt <= cutoff) this.entries.delete(id);
23896
+ for (const [id, entry] of this.entries) if (entry.settled && entry.insertedAt <= cutoff) this.entries.delete(id);
23124
23897
  }
23125
23898
  evictOverCap() {
23126
23899
  while (this.entries.size > this.maxEntries) {
23127
- const oldest = this.entries.keys().next().value;
23128
- if (oldest === void 0) break;
23129
- this.entries.delete(oldest);
23900
+ const oldestSettled = Array.from(this.entries).find(([, entry]) => entry.settled);
23901
+ if (!oldestSettled) break;
23902
+ this.entries.delete(oldestSettled[0]);
23130
23903
  }
23131
23904
  }
23132
23905
  };
@@ -23151,7 +23924,7 @@ var CommandDedupe = class {
23151
23924
  * the gateway when needed and uses `callerScopes: ["operator.admin"]`.
23152
23925
  * 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
23153
23926
  */
23154
- const execFileAsync$2 = promisify(execFile);
23927
+ const execFileAsync$1 = promisify(execFile);
23155
23928
  const APPROVE_TIMEOUT_MS = 1e4;
23156
23929
  function resolveStateDir(override) {
23157
23930
  if (override) return override;
@@ -23250,7 +24023,7 @@ function startPairingApprovalPoller(opts) {
23250
24023
  const intervalMs = opts.intervalMs ?? 3e4;
23251
24024
  const cliLock = opts.cliLock ?? new NoopOpenClawCliLock();
23252
24025
  const rawExec = opts.exec ?? (async (file, args, { timeout }) => {
23253
- const { stdout, stderr } = await execFileAsync$2(file, args, { timeout });
24026
+ const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
23254
24027
  return {
23255
24028
  stdout,
23256
24029
  stderr
@@ -23324,12 +24097,12 @@ function startPairingApprovalPoller(opts) {
23324
24097
  * Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
23325
24098
  * once per agent.
23326
24099
  */
23327
- const execFileAsync$1 = promisify(execFile);
24100
+ const execFileAsync = promisify(execFile);
23328
24101
  const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
23329
24102
  const defaultOpenclaw = {
23330
24103
  async listServers() {
23331
24104
  try {
23332
- const { stdout } = await execFileAsync$1("openclaw", [
24105
+ const { stdout } = await execFileAsync("openclaw", [
23333
24106
  "config",
23334
24107
  "get",
23335
24108
  "mcp.servers"
@@ -23346,7 +24119,7 @@ const defaultOpenclaw = {
23346
24119
  }
23347
24120
  },
23348
24121
  async unsetServer(key) {
23349
- await execFileAsync$1("openclaw", [
24122
+ await execFileAsync("openclaw", [
23350
24123
  "config",
23351
24124
  "unset",
23352
24125
  `mcp.servers.${key}`
@@ -23528,8 +24301,12 @@ function createMcpErrorHooks() {
23528
24301
  * 7. Start agent runtime (managed mode only — e.g. OpenClaw)
23529
24302
  * 8. Write PID file (non-managed only)
23530
24303
  * 9. Handle graceful shutdown
24304
+ *
24305
+ * Split along seams (this file stays the orchestrator):
24306
+ * - daemon-bootstrap.ts — version resolution, persona migration, AI-proxy + IPC start
24307
+ * - mcp-handlers.ts — `mcp.*` IPC request handlers
24308
+ * - integration-bootstrap.ts — provider-credential fetch + post-reconcile command reporting
23531
24309
  */
23532
- const execFileAsync = promisify(execFile);
23533
24310
  let config;
23534
24311
  let cloudClient;
23535
24312
  let ipcServer = null;
@@ -23538,225 +24315,46 @@ let startedAt;
23538
24315
  let integrationManager;
23539
24316
  let mcpBundler = null;
23540
24317
  let mcpManagerRef = null;
23541
- /**
23542
- * Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
23543
- * store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
23544
- * and for managed/self-hosted runtimes other than hermes.
23545
- */
23546
- let hermesMcpSync = null;
23547
- /**
23548
- * claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
23549
- * into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
23550
- * ALFE_API_KEY into every stdio server's env. Null for every other runtime
23551
- * (never constructed) — same shape/role as {@link hermesMcpSync}.
23552
- */
23553
- let claudeCodeMcpSync = null;
23554
- let aiProxyServer = null;
23555
- let runtimeProcess = null;
23556
- let turnActivityProbe = null;
23557
- let aiProxyUrl = null;
23558
- let aiProxyRunning = false;
23559
- let cloudConnected = false;
23560
- /**
23561
- * Dedupe for at-least-once durable command delivery (Phase B). The cloud may
23562
- * push a command over the live WS AND re-drain the same commandId on the next
23563
- * SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
23564
- * so a duplicate is ACKed with the prior result without re-executing.
23565
- */
23566
- const commandDedupe = new CommandDedupe();
23567
- let shuttingDown = false;
23568
- let commandRegistry;
23569
- let resolvedCliVersion;
23570
- let resolvedRuntimeVersion;
23571
- let upgradingRuntime = false;
23572
- let stopPairingApprovalPoller = null;
23573
- /**
23574
- * Module-level handle on the runtime appliers built during start(), so the
23575
- * module-scoped command handler can route `alfe.config_set` to the active
23576
- * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
23577
- */
23578
- let runtimeAppliersRef = null;
23579
- /**
23580
- * Resolve the installed @alfe.ai/cli version.
23581
- *
23582
- * Strategy (in order):
23583
- * 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
23584
- * 2. Walk up from this file to find @alfe.ai/cli/package.json
23585
- * (works when systemd runs the gateway binary directly, since
23586
- * the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
23587
- */
23588
- /**
23589
- * Adapter from `AgentApiClient`'s per-provider methods to the
23590
- * `CredentialsResolver` shape the MCP applier expects. Returns
23591
- * `undefined` for unknown providers and on 404/network error so the
23592
- * applier can skip registration silently (its documented contract).
23593
- */
23594
- async function fetchProviderCredentials(agentApi, provider, connectionId) {
23595
- if (connectionId) try {
23596
- const raw = await agentApi.getConnectionCredentials(connectionId);
23597
- const fields = { ...raw.providerMetadata ?? {} };
23598
- if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
23599
- const bundle = JSON.parse(raw.accessToken);
23600
- if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
23601
- } catch (err) {
23602
- logger$1.warn({
23603
- connectionId,
23604
- err: err instanceof Error ? err.message : String(err)
23605
- }, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
23606
- fields.accessToken = raw.accessToken;
23607
- }
23608
- return fields;
23609
- } catch (err) {
23610
- logger$1.warn({
23611
- connectionId,
23612
- err: err instanceof Error ? err.message : String(err)
23613
- }, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
23614
- return;
23615
- }
23616
- const key = provider.toLowerCase();
23617
- try {
23618
- switch (key) {
23619
- case "atlassian": return await agentApi.getAtlassianCredentials();
23620
- case "github": return await agentApi.getGithubCredentials();
23621
- case "xero": return await agentApi.getXeroCredentials();
23622
- case "notion": return await agentApi.getNotionCredentials();
23623
- case "myob": return await agentApi.getMYOBCredentials();
23624
- case "google": return await agentApi.getGoogleCredentials();
23625
- default:
23626
- logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
23627
- return;
23628
- }
23629
- } catch {
23630
- return;
23631
- }
23632
- }
23633
- async function getCliVersion() {
23634
- if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
23635
- try {
23636
- const { fileURLToPath } = await import("node:url");
23637
- const { dirname } = await import("node:path");
23638
- let dir = dirname(fileURLToPath(import.meta.url));
23639
- for (let i = 0; i < 10; i++) {
23640
- const candidate = join(dir, "package.json");
23641
- try {
23642
- const raw = await readFile(candidate, "utf-8");
23643
- const pkg = JSON.parse(raw);
23644
- if (pkg.name === "@alfe.ai/cli") return pkg.version;
23645
- } catch {}
23646
- const parent = dirname(dir);
23647
- if (parent === dir) break;
23648
- dir = parent;
23649
- }
23650
- } catch {}
23651
- logger$1.debug("Could not resolve @alfe.ai/cli version");
23652
- }
23653
- /**
23654
- * Per-runtime "print installed version" commands. Local to the gateway — we do
23655
- * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
23656
- * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
23657
- * connection-status report degrades gracefully instead of crashing.
23658
- */
23659
- const RUNTIME_VERSION_COMMANDS = {
23660
- openclaw: {
23661
- command: "openclaw",
23662
- args: ["--version"]
23663
- },
23664
- hermes: {
23665
- command: "hermes",
23666
- args: ["version"]
23667
- },
23668
- "claude-code": {
23669
- command: "alfe-claude-host",
23670
- args: ["--version"]
23671
- }
23672
- };
23673
- /**
23674
- * Pure resolver (exported for tests) — the per-runtime version command, or
23675
- * `undefined` for an unknown runtime.
24318
+ /**
24319
+ * Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
24320
+ * store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
24321
+ * and for managed/self-hosted runtimes other than hermes.
23676
24322
  */
23677
- function resolveRuntimeVersionCommand(runtime) {
23678
- return RUNTIME_VERSION_COMMANDS[runtime];
23679
- }
24323
+ let hermesMcpSync = null;
23680
24324
  /**
23681
- * Resolve the installed runtime version by running the per-runtime version
23682
- * command and returning the trimmed stdout. An unknown runtime (no command in
23683
- * the map) returns `undefined` without spawning it must never throw.
24325
+ * claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
24326
+ * into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
24327
+ * ALFE_API_KEY into every stdio server's env. Null for every other runtime
24328
+ * (never constructed) — same shape/role as {@link hermesMcpSync}.
23684
24329
  */
23685
- async function getRuntimeVersion(runtime) {
23686
- const cmd = resolveRuntimeVersionCommand(runtime);
23687
- if (!cmd) {
23688
- logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
23689
- return;
23690
- }
23691
- try {
23692
- const { stdout } = await execFileAsync(cmd.command, cmd.args);
23693
- return stdout.trim() || void 0;
23694
- } catch {
23695
- logger$1.debug({ runtime }, "Could not resolve runtime version");
23696
- return;
23697
- }
23698
- }
24330
+ let claudeCodeMcpSync = null;
24331
+ let aiProxyServer = null;
24332
+ let runtimeProcess = null;
24333
+ let turnActivityProbe = null;
24334
+ let aiProxyUrl = null;
24335
+ let aiProxyRunning = false;
24336
+ let cloudConnected = false;
23699
24337
  /**
23700
- * Flush pino's async transport and exit.
23701
- * process.exit() can drop buffered log lines this ensures they're written first.
24338
+ * Dedupe for at-least-once durable command delivery (Phase B). The cloud may
24339
+ * push a command over the live WS AND re-drain the same commandId on the next
24340
+ * SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
24341
+ * so a duplicate is ACKed with the prior result without re-executing.
23702
24342
  */
23703
- async function flushAndExit(code) {
23704
- await flushSentry();
23705
- await new Promise((resolve) => {
23706
- logger$1.flush();
23707
- setTimeout(resolve, 500);
23708
- });
23709
- process.exit(code);
23710
- }
24343
+ const commandDedupe = new CommandDedupe();
24344
+ let shuttingDown = false;
24345
+ let commandRegistry;
24346
+ let resolvedCliVersion;
24347
+ let resolvedRuntimeVersion;
24348
+ let upgradingRuntime = false;
24349
+ let stopPairingApprovalPoller = null;
23711
24350
  /**
23712
- * One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
23713
- * fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
23714
- * OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
23715
- * (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
23716
- * workspace already has the file. Idempotent and safe to run on every daemon start.
24351
+ * Module-level handle on the runtime appliers built during start(), so the
24352
+ * module-scoped command handler can route `alfe.config_set` to the active
24353
+ * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
23717
24354
  */
23718
- const PERSONA_FILES = [
23719
- "SOUL.md",
23720
- "IDENTITY.md",
23721
- "BOOTSTRAP.md",
23722
- "AGENTS.md"
23723
- ];
23724
- async function migrateLegacyPersonaFiles(home, agentWorkspace) {
23725
- if (home === agentWorkspace) return;
23726
- let moved = 0;
23727
- let skipped = 0;
23728
- for (const filename of PERSONA_FILES) {
23729
- const src = join(home, filename);
23730
- const dst = join(agentWorkspace, filename);
23731
- try {
23732
- await stat(src);
23733
- } catch {
23734
- continue;
23735
- }
23736
- try {
23737
- await stat(dst);
23738
- skipped++;
23739
- continue;
23740
- } catch {}
23741
- try {
23742
- await mkdir(agentWorkspace, { recursive: true });
23743
- await rename(src, dst);
23744
- moved++;
23745
- } catch (err) {
23746
- logger$1.warn({
23747
- src,
23748
- dst,
23749
- err: err instanceof Error ? err.message : String(err)
23750
- }, "Persona file migration: rename failed");
23751
- }
23752
- }
23753
- if (moved > 0 || skipped > 0) logger$1.info({
23754
- home,
23755
- agentWorkspace,
23756
- moved,
23757
- skipped
23758
- }, "Persona file backfill migration complete");
23759
- }
24355
+ let runtimeAppliersRef = null;
24356
+ const LEGACY_CONFIG_SET_KEYS = new Set(["agents.defaults.model"]);
24357
+ const MAX_LEGACY_CONFIG_VALUE_LENGTH = 16 * 1024;
23760
24358
  async function startDaemon() {
23761
24359
  startedAt = Date.now();
23762
24360
  const managed = isManagedMode();
@@ -23764,9 +24362,10 @@ async function startDaemon() {
23764
24362
  managed,
23765
24363
  pid: process.pid
23766
24364
  }, "Starting Alfe Gateway Daemon...");
24365
+ resolvedCliVersion = await getCliVersion();
23767
24366
  await initAgentSentry({
23768
24367
  surface: "daemon",
23769
- release: await getCliVersion()
24368
+ release: resolvedCliVersion
23770
24369
  });
23771
24370
  let fatalExiting = false;
23772
24371
  process.on("uncaughtException", (err) => {
@@ -23816,55 +24415,12 @@ async function startDaemon() {
23816
24415
  commandQueue = new CommandQueue();
23817
24416
  commandQueue.startGC();
23818
24417
  commandRegistry = new CommandRegistry();
23819
- logger$1.debug("Starting AI proxy...");
23820
- try {
23821
- const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
23822
- const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
23823
- const proxyUrl = getAiServiceUrlFromToken(config.apiKey);
23824
- const port = DEFAULT_AI_PROXY_PORT ?? 18193;
23825
- aiProxyServer = createProxyServer({
23826
- port,
23827
- apiKey: config.apiKey,
23828
- proxyUrl
23829
- });
23830
- const server = aiProxyServer;
23831
- await new Promise((resolve, reject) => {
23832
- server.listen(port, "127.0.0.1", () => {
23833
- aiProxyRunning = true;
23834
- aiProxyUrl = `http://127.0.0.1:${String(port)}`;
23835
- logger$1.info({
23836
- port,
23837
- upstream: proxyUrl
23838
- }, "AI proxy started");
23839
- resolve();
23840
- });
23841
- server.on("error", reject);
23842
- });
23843
- } catch (err) {
23844
- const message = err instanceof Error ? err.message : String(err);
23845
- const stack = err instanceof Error ? err.stack : void 0;
23846
- logger$1.error({
23847
- err: message,
23848
- stack
23849
- }, "Failed to start AI proxy — LLM requests will fail");
23850
- }
23851
- logger$1.debug({ socketPath: config.socketPath }, "Starting IPC server...");
23852
- ipcServer = new IPCServer(config.socketPath);
23853
- ipcServer.setRequestHandler(handlePluginRequest);
24418
+ const aiProxy = await startAiProxy(config.apiKey);
24419
+ aiProxyServer = aiProxy.server;
24420
+ aiProxyUrl = aiProxy.url;
24421
+ aiProxyRunning = aiProxy.running;
24422
+ ipcServer = await startIpcServer(config.socketPath, handlePluginRequest, resolvedCliVersion);
23854
24423
  turnActivityProbe = new IpcTurnActivityProbe(() => ipcServer);
23855
- try {
23856
- await ipcServer.start();
23857
- logger$1.debug("IPC server started");
23858
- } catch (err) {
23859
- const message = err instanceof Error ? err.message : String(err);
23860
- const stack = err instanceof Error ? err.stack : void 0;
23861
- logger$1.error({
23862
- err: message,
23863
- stack
23864
- }, "Failed to start IPC server");
23865
- await flushAndExit(1);
23866
- }
23867
- resolvedCliVersion = await getCliVersion();
23868
24424
  resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
23869
24425
  logger$1.info({
23870
24426
  cliVersion: resolvedCliVersion,
@@ -24007,19 +24563,11 @@ async function startDaemon() {
24007
24563
  }
24008
24564
  }
24009
24565
  cloudClient.setOnReconciliationComplete(() => {
24010
- try {
24011
- const activeCommands = integrationManager.getActiveCommands();
24012
- commandRegistry.clear();
24013
- for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
24014
- const commandsMsg = {
24015
- type: "COMMANDS_AVAILABLE",
24016
- commands: commandRegistry.listCommands()
24017
- };
24018
- cloudClient.sendMessage(commandsMsg);
24019
- logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
24020
- } catch (err) {
24021
- logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
24022
- }
24566
+ reportAvailableCommands({
24567
+ integrationManager,
24568
+ commandRegistry,
24569
+ cloudClient
24570
+ });
24023
24571
  });
24024
24572
  cloudClient.start();
24025
24573
  logger$1.debug("Cloud client started");
@@ -24228,7 +24776,7 @@ async function executeCloudCommand(command) {
24228
24776
  };
24229
24777
  const payload = command.payload;
24230
24778
  const runtime = config.runtime;
24231
- const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.11" : runtime === "claude-code" ? "0.1.3" : void 0);
24779
+ const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.11" : runtime === "claude-code" ? "0.1.6" : void 0);
24232
24780
  upgradingRuntime = true;
24233
24781
  setTimeout(() => {
24234
24782
  (async () => {
@@ -24334,504 +24882,258 @@ async function executeCloudCommand(command) {
24334
24882
  const payload = command.payload;
24335
24883
  const key = payload?.key;
24336
24884
  const value = payload?.value;
24337
- if (!key || value === void 0) return {
24338
- type: "COMMAND_ACK",
24339
- commandId: command.commandId,
24340
- status: "error",
24341
- result: {
24342
- code: "INVALID_PAYLOAD",
24343
- message: "alfe.config_set requires key and value"
24344
- }
24345
- };
24346
- const runtime = config.runtime;
24347
- const applier = runtimeAppliersRef?.get(runtime);
24348
- if (!applier || typeof applier.setConfigRaw !== "function") {
24349
- const message = `No runtime applier supports config_set for runtime "${runtime}"`;
24350
- logger$1.warn({
24351
- runtime,
24352
- key
24353
- }, message);
24354
- return {
24355
- type: "COMMAND_ACK",
24356
- commandId: command.commandId,
24357
- status: "error",
24358
- result: {
24359
- code: "CONFIG_SET_UNSUPPORTED",
24360
- message
24361
- }
24362
- };
24363
- }
24364
- try {
24365
- await applier.setConfigRaw(key, value);
24366
- logger$1.info({
24367
- runtime,
24368
- key
24369
- }, "Applied config via runtime applier setConfigRaw");
24370
- return {
24371
- type: "COMMAND_ACK",
24372
- commandId: command.commandId,
24373
- status: "ok",
24374
- result: {
24375
- key,
24376
- value
24377
- }
24378
- };
24379
- } catch (err) {
24380
- const message = err instanceof Error ? err.message : String(err);
24381
- logger$1.error({
24382
- err: message,
24383
- runtime,
24384
- key
24385
- }, "Failed to apply config via runtime applier");
24386
- return {
24387
- type: "COMMAND_ACK",
24388
- commandId: command.commandId,
24389
- status: "error",
24390
- result: {
24391
- code: "CONFIG_SET_FAILED",
24392
- message
24393
- }
24394
- };
24395
- }
24396
- }
24397
- if (commandRegistry.has(command.command)) try {
24398
- const ctx = buildCommandContext();
24399
- const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
24400
- return {
24401
- type: "COMMAND_ACK",
24402
- commandId: command.commandId,
24403
- status: result.status,
24404
- result: result.result
24405
- };
24406
- } catch (err) {
24407
- const message = err instanceof Error ? err.message : String(err);
24408
- logger$1.error({
24409
- err: message,
24410
- command: command.command
24411
- }, "Command registry execution failed");
24412
- return {
24413
- type: "COMMAND_ACK",
24414
- commandId: command.commandId,
24415
- status: "error",
24416
- result: {
24417
- code: "REGISTRY_ERROR",
24418
- message
24419
- }
24420
- };
24421
- }
24422
- const ipcRequest = cloudCommandToIPCRequest(command);
24423
- if (!ipcRequest) {
24424
- logger$1.warn({ command: command.command }, "Unrecognized cloud command");
24425
- return {
24426
- type: "COMMAND_ACK",
24427
- commandId: command.commandId,
24428
- status: "error",
24429
- result: {
24430
- code: "UNKNOWN_COMMAND",
24431
- message: `Unrecognized command: ${command.command}`
24432
- }
24433
- };
24434
- }
24435
- const plugins = ipcServer?.getRegisteredPlugins() ?? [];
24436
- if (plugins.length === 0) {
24437
- logger$1.info({
24438
- commandId: command.commandId,
24439
- command: command.command
24440
- }, "No plugins connected — queuing command");
24441
- commandQueue.enqueue("_default", ipcRequest, command.commandId);
24442
- return {
24885
+ if (!key || value === void 0) return {
24443
24886
  type: "COMMAND_ACK",
24444
24887
  commandId: command.commandId,
24445
- status: "ok",
24888
+ status: "error",
24446
24889
  result: {
24447
- queued: true,
24448
- message: "Command queued no plugins connected"
24890
+ code: "INVALID_PAYLOAD",
24891
+ message: "alfe.config_set requires key and value"
24449
24892
  }
24450
24893
  };
24451
- }
24452
- const [pluginId] = plugins[0];
24453
- if (!ipcServer) return {
24454
- type: "COMMAND_ACK",
24455
- commandId: command.commandId,
24456
- status: "error",
24457
- result: {
24458
- code: "NO_IPC",
24459
- message: "IPC server not available"
24460
- }
24461
- };
24462
- try {
24463
- const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
24464
- return ipcResponseToCloudAck(command.commandId, response);
24465
- } catch (err) {
24466
- const message = err instanceof Error ? err.message : String(err);
24467
- return {
24894
+ if (!LEGACY_CONFIG_SET_KEYS.has(key) || value.length > MAX_LEGACY_CONFIG_VALUE_LENGTH) return {
24468
24895
  type: "COMMAND_ACK",
24469
24896
  commandId: command.commandId,
24470
24897
  status: "error",
24471
24898
  result: {
24472
- code: "PLUGIN_ERROR",
24473
- message
24899
+ code: "CONFIG_KEY_UNSUPPORTED",
24900
+ message: "Config key is not supported by the legacy command path"
24474
24901
  }
24475
24902
  };
24476
- }
24477
- }
24478
- function buildCommandContext() {
24479
- const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
24480
- return {
24481
- workspacePath,
24482
- aiProxyUrl: aiProxyUrl ?? void 0,
24483
- aiProxyRunning,
24484
- apiKey: config.apiKey,
24485
- async exec(cmd, opts) {
24486
- const { exec: execCb } = await import("child_process");
24487
- const { promisify } = await import("util");
24488
- const { stdout, stderr } = await promisify(execCb)(cmd, {
24489
- cwd: workspacePath,
24490
- timeout: opts?.timeoutMs ?? 25e3,
24491
- maxBuffer: opts?.maxBuffer ?? 512 * 1024
24492
- });
24903
+ const runtime = config.runtime;
24904
+ const applier = runtimeAppliersRef?.get(runtime);
24905
+ if (!applier || typeof applier.setConfigRaw !== "function") {
24906
+ const message = `No runtime applier supports config_set for runtime "${runtime}"`;
24907
+ logger$1.warn({
24908
+ runtime,
24909
+ key
24910
+ }, message);
24493
24911
  return {
24494
- stdout: stdout.trim(),
24495
- stderr: stderr.trim()
24912
+ type: "COMMAND_ACK",
24913
+ commandId: command.commandId,
24914
+ status: "error",
24915
+ result: {
24916
+ code: "CONFIG_SET_UNSUPPORTED",
24917
+ message
24918
+ }
24496
24919
  };
24497
24920
  }
24498
- };
24499
- }
24500
- function handlePluginRequest(method, params, pluginId) {
24501
- switch (method) {
24502
- case "status": return Promise.resolve(handleStatus());
24503
- case "integration.list": return Promise.resolve(handleIntegrationList());
24504
- case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
24505
- case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
24506
- case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
24507
- case "mcp.list_servers": return Promise.resolve(handleMcpListServers());
24508
- case "mcp.add_server": return handleMcpAddServer(params);
24509
- case "mcp.remove_server": return handleMcpRemoveServer(params);
24510
- default: return Promise.resolve({
24511
- ok: false,
24512
- error: {
24513
- code: "UNKNOWN_METHOD",
24514
- message: `Unknown method: ${method}`
24515
- }
24516
- });
24517
- }
24518
- }
24519
- function handleStatus() {
24520
- return {
24521
- ok: true,
24522
- payload: {
24523
- daemon: {
24524
- status: "running",
24525
- pid: process.pid,
24526
- uptime: (Date.now() - startedAt) / 1e3,
24527
- version: "0.1.0",
24528
- runtimeVersion: resolvedRuntimeVersion
24529
- },
24530
- cloud: {
24531
- status: cloudConnected ? "connected" : "disconnected",
24532
- latencyMs: cloudClient.getLatencyMs()
24533
- },
24534
- aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
24535
- plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
24536
- name: info.name,
24537
- version: info.version,
24538
- capabilities: info.capabilities,
24539
- connectedAt: info.connectedAt,
24540
- lastSeen: info.lastSeen
24541
- })),
24542
- commandQueue: { totalPending: commandQueue.totalPending() }
24543
- }
24544
- };
24545
- }
24546
- function handleIntegrationList() {
24547
- return {
24548
- ok: true,
24549
- payload: { integrations: integrationManager.list() }
24550
- };
24551
- }
24552
- /**
24553
- * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
24554
- * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
24555
- * cheap (in-memory snapshot, no I/O).
24556
- */
24557
- function handleMcpListTools(bundler) {
24558
- if (!bundler) return {
24559
- ok: false,
24560
- error: {
24561
- code: "MCP_BUNDLER_UNAVAILABLE",
24562
- message: "MCP bundler not initialized"
24563
- }
24564
- };
24565
- return {
24566
- ok: true,
24567
- payload: { tools: bundler.listTools() }
24568
- };
24569
- }
24570
- /**
24571
- * Observability backstop for the INTEGRATION warm path. `applyForIntegration`
24572
- * registers MCP servers into the store and the daemon warms them silently via
24573
- * the store-change `onChange` hook — so a server that fails its connect (e.g.
24574
- * an MCP server whose backing account/credential wasn't resolvable at warm
24575
- * time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
24576
- * making the black hole undiagnosable. After a warm, read `bundler.statuses()`
24577
- * and warn once per still-failed server so the failure is visible. These
24578
- * self-heal on the bundler's background retry sweep once the dependency
24579
- * appears. Returns the servers it warned about (for tests / callers).
24580
- */
24581
- function warnFailedMcpServers(bundler, log, reason) {
24582
- if (!bundler) return [];
24583
- const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
24584
- for (const status of failed) log.warn({
24585
- server: status.name,
24586
- reason,
24587
- consecutiveFailures: status.consecutiveFailures,
24588
- lastError: status.lastError
24589
- }, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
24590
- return failed;
24591
- }
24592
- /**
24593
- * Route a tool call to the appropriate MCP child via the daemon-hosted
24594
- * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
24595
- * the raw JSON object the LLM produced.
24596
- */
24597
- /**
24598
- * List every server entry in the alfe bundler store. Lets agents inspect
24599
- * what they've already registered before adding a new one.
24600
- */
24601
- function handleMcpListServers(manager = mcpManagerRef) {
24602
- if (!manager) return {
24603
- ok: false,
24604
- error: {
24605
- code: "MCP_MANAGER_UNAVAILABLE",
24606
- message: "MCP manager not initialized"
24607
- }
24608
- };
24609
- const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
24610
- const byName = new Map(statuses.map((s) => [s.name, s]));
24611
- return {
24612
- ok: true,
24613
- payload: { servers: manager.listServers().map(({ id, entry }) => ({
24614
- id,
24615
- entry,
24616
- status: byName.get(id) ?? {
24617
- name: id,
24618
- connected: false,
24619
- toolCount: 0,
24620
- consecutiveFailures: 0
24621
- }
24622
- })) }
24623
- };
24624
- }
24625
- /**
24626
- * How long the add-confirm probe waits for the freshly-added server to
24627
- * connect before replying. This is effectively the whole budget for
24628
- * `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
24629
- * BEFORE resolving, so the Connection object already exists and
24630
- * `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
24631
- * plugin caller's 30s IPC timeout. A slower server still connects in the
24632
- * background and surfaces on the next `alfe_mcp_list_tools` — the probe just
24633
- * reports what it saw within the window; a missed connect is re-attempted by
24634
- * the bundler's `retryNeverConnected` sweep.
24635
- */
24636
- const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
24637
- /**
24638
- * Register a new MCP server in the alfe bundler store on behalf of the agent,
24639
- * then CONFIRM the connect before replying so the agent learns whether the
24640
- * server actually works. Owned as `'manual'` so the agent can later remove it
24641
- * without an expectedOwner conflict — matches what `alfe mcp add` does from the
24642
- * CLI. Registration is durable regardless of the probe outcome: a probe
24643
- * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
24644
- * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
24645
- */
24646
- async function handleMcpAddServer(params, manager = mcpManagerRef) {
24647
- if (!manager) return {
24648
- ok: false,
24649
- error: {
24650
- code: "MCP_MANAGER_UNAVAILABLE",
24651
- message: "MCP manager not initialized"
24652
- }
24653
- };
24654
- const p = params;
24655
- if (typeof p.id !== "string" || p.id.length === 0) return {
24656
- ok: false,
24657
- error: {
24658
- code: "INVALID_PARAMS",
24659
- message: "id is required (string)"
24660
- }
24661
- };
24662
- const config = buildServerConfig(p);
24663
- if (!config) return {
24664
- ok: false,
24665
- error: {
24666
- code: "INVALID_PARAMS",
24667
- message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
24668
- }
24669
- };
24670
- try {
24671
- await manager.addServer(config, {
24672
- id: p.id,
24673
- owner: "manual"
24674
- });
24921
+ try {
24922
+ await applier.setConfigRaw(key, value);
24923
+ logger$1.info({
24924
+ runtime,
24925
+ key
24926
+ }, "Applied config via runtime applier setConfigRaw");
24927
+ return {
24928
+ type: "COMMAND_ACK",
24929
+ commandId: command.commandId,
24930
+ status: "ok",
24931
+ result: {
24932
+ key,
24933
+ applied: true
24934
+ }
24935
+ };
24936
+ } catch (err) {
24937
+ const message = err instanceof Error ? err.message : String(err);
24938
+ logger$1.error({
24939
+ err: message,
24940
+ runtime,
24941
+ key
24942
+ }, "Failed to apply config via runtime applier");
24943
+ return {
24944
+ type: "COMMAND_ACK",
24945
+ commandId: command.commandId,
24946
+ status: "error",
24947
+ result: {
24948
+ code: "CONFIG_SET_FAILED",
24949
+ message
24950
+ }
24951
+ };
24952
+ }
24953
+ }
24954
+ if (commandRegistry.has(command.command)) try {
24955
+ const ctx = buildCommandContext();
24956
+ const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
24957
+ return {
24958
+ type: "COMMAND_ACK",
24959
+ commandId: command.commandId,
24960
+ status: result.status,
24961
+ result: result.result
24962
+ };
24675
24963
  } catch (err) {
24676
24964
  const message = err instanceof Error ? err.message : String(err);
24677
- logger$1.warn({
24678
- id: p.id,
24679
- err: message
24680
- }, "mcp.add_server failed");
24965
+ logger$1.error({
24966
+ err: message,
24967
+ command: command.command
24968
+ }, "Command registry execution failed");
24681
24969
  return {
24682
- ok: false,
24683
- error: {
24684
- code: "MCP_ADD_FAILED",
24970
+ type: "COMMAND_ACK",
24971
+ commandId: command.commandId,
24972
+ status: "error",
24973
+ result: {
24974
+ code: "REGISTRY_ERROR",
24685
24975
  message
24686
24976
  }
24687
24977
  };
24688
24978
  }
24689
- let status = null;
24690
- if (manager.warmServer) try {
24691
- status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
24692
- } catch (err) {
24693
- logger$1.warn({
24694
- id: p.id,
24695
- err: err instanceof Error ? err.message : String(err)
24696
- }, "mcp.add_server warm probe threw");
24979
+ const ipcRequest = cloudCommandToIPCRequest(command);
24980
+ if (!ipcRequest) {
24981
+ logger$1.warn({ command: command.command }, "Unrecognized cloud command");
24982
+ return {
24983
+ type: "COMMAND_ACK",
24984
+ commandId: command.commandId,
24985
+ status: "error",
24986
+ result: {
24987
+ code: "UNKNOWN_COMMAND",
24988
+ message: `Unrecognized command: ${command.command}`
24989
+ }
24990
+ };
24697
24991
  }
24698
- return {
24699
- ok: true,
24700
- payload: {
24701
- id: p.id,
24702
- connected: status?.connected ?? false,
24703
- toolCount: status?.toolCount ?? 0,
24704
- ...status?.lastError !== void 0 ? { error: status.lastError } : {}
24705
- }
24706
- };
24707
- }
24708
- function buildServerConfig(p) {
24709
- if (typeof p.command === "string" && p.command.length > 0) {
24710
- const cfg = { command: p.command };
24711
- if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
24712
- if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
24713
- const env = {};
24714
- for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
24715
- cfg.env = env;
24992
+ const plugins = ipcServer?.getRegisteredPlugins() ?? [];
24993
+ if (plugins.length === 0) {
24994
+ logger$1.info({
24995
+ commandId: command.commandId,
24996
+ command: command.command
24997
+ }, "No plugins connected — queuing command");
24998
+ if (!commandQueue.enqueue("_default", ipcRequest, command.commandId)) {
24999
+ logger$1.warn({
25000
+ commandId: command.commandId,
25001
+ command: command.command
25002
+ }, "Plugin command queue is full");
25003
+ return {
25004
+ type: "COMMAND_ACK",
25005
+ commandId: command.commandId,
25006
+ status: "error",
25007
+ result: {
25008
+ code: "QUEUE_FULL",
25009
+ message: "Plugin command queue is at capacity"
25010
+ }
25011
+ };
24716
25012
  }
24717
- if (typeof p.cwd === "string") cfg.cwd = p.cwd;
24718
- return cfg;
24719
- }
24720
- if (typeof p.url === "string" && p.url.length > 0) {
24721
- const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
24722
- const cfg = {
24723
- url: p.url,
24724
- transport
25013
+ return {
25014
+ type: "COMMAND_ACK",
25015
+ commandId: command.commandId,
25016
+ status: "ok",
25017
+ result: {
25018
+ queued: true,
25019
+ message: "Command queued — no plugins connected"
25020
+ }
24725
25021
  };
24726
- if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
24727
- const headers = {};
24728
- for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
24729
- cfg.headers = headers;
24730
- }
24731
- return cfg;
24732
25022
  }
24733
- return null;
24734
- }
24735
- /**
24736
- * Drop a server entry the agent previously registered. Restricted to
24737
- * `manual`-owned entries so the agent can't accidentally clobber
24738
- * integration-installed or CLI-installed servers (the daemon owns
24739
- * those; the agent can ask the user to uninstall an integration via
24740
- * the dashboard).
24741
- */
24742
- async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
24743
- if (!manager) return {
24744
- ok: false,
24745
- error: {
24746
- code: "MCP_MANAGER_UNAVAILABLE",
24747
- message: "MCP manager not initialized"
24748
- }
24749
- };
24750
- const { id } = params;
24751
- if (typeof id !== "string" || id.length === 0) return {
24752
- ok: false,
24753
- error: {
24754
- code: "INVALID_PARAMS",
24755
- message: "id is required (string)"
25023
+ const [pluginId] = plugins[0];
25024
+ if (!ipcServer) return {
25025
+ type: "COMMAND_ACK",
25026
+ commandId: command.commandId,
25027
+ status: "error",
25028
+ result: {
25029
+ code: "NO_IPC",
25030
+ message: "IPC server not available"
24756
25031
  }
24757
25032
  };
24758
25033
  try {
24759
- return {
24760
- ok: true,
24761
- payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
24762
- };
25034
+ const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
25035
+ return ipcResponseToCloudAck(command.commandId, response);
24763
25036
  } catch (err) {
24764
25037
  const message = err instanceof Error ? err.message : String(err);
24765
- if (message.includes("owned by")) return {
24766
- ok: false,
24767
- error: {
24768
- code: "MCP_OWNER_MISMATCH",
24769
- message
24770
- }
24771
- };
24772
- logger$1.warn({
24773
- id,
24774
- err: message
24775
- }, "mcp.remove_server failed");
24776
25038
  return {
24777
- ok: false,
24778
- error: {
24779
- code: "MCP_REMOVE_FAILED",
25039
+ type: "COMMAND_ACK",
25040
+ commandId: command.commandId,
25041
+ status: "error",
25042
+ result: {
25043
+ code: "PLUGIN_ERROR",
24780
25044
  message
24781
25045
  }
24782
25046
  };
24783
25047
  }
24784
25048
  }
24785
- async function handleMcpCallTool(bundler, params) {
24786
- if (!bundler) return {
24787
- ok: false,
24788
- error: {
24789
- code: "MCP_BUNDLER_UNAVAILABLE",
24790
- message: "MCP bundler not initialized"
24791
- }
24792
- };
24793
- const { name, args } = params;
24794
- if (typeof name !== "string" || name.length === 0) return {
24795
- ok: false,
24796
- error: {
24797
- code: "INVALID_PARAMS",
24798
- message: "name is required (string)"
25049
+ function buildCommandContext() {
25050
+ const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
25051
+ return {
25052
+ workspacePath,
25053
+ aiProxyUrl: aiProxyUrl ?? void 0,
25054
+ aiProxyRunning,
25055
+ apiKey: config.apiKey,
25056
+ async exec(cmd, opts) {
25057
+ const { exec: execCb } = await import("child_process");
25058
+ const { promisify } = await import("util");
25059
+ const { stdout, stderr } = await promisify(execCb)(cmd, {
25060
+ cwd: workspacePath,
25061
+ timeout: opts?.timeoutMs ?? 25e3,
25062
+ maxBuffer: opts?.maxBuffer ?? 512 * 1024
25063
+ });
25064
+ return {
25065
+ stdout: stdout.trim(),
25066
+ stderr: stderr.trim()
25067
+ };
24799
25068
  }
24800
25069
  };
24801
- try {
24802
- return {
24803
- ok: true,
24804
- payload: await bundler.callTool(name, args)
24805
- };
24806
- } catch (err) {
24807
- const message = err instanceof Error ? err.message : String(err);
24808
- logger$1.warn({
24809
- tool: name,
24810
- err: message
24811
- }, "mcp.call_tool failed");
24812
- return {
25070
+ }
25071
+ function handlePluginRequest(method, params, pluginId) {
25072
+ switch (method) {
25073
+ case "status": return Promise.resolve(handleStatus());
25074
+ case "integration.list": return Promise.resolve(handleIntegrationList());
25075
+ case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
25076
+ case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
25077
+ case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
25078
+ case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef));
25079
+ case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef);
25080
+ case "mcp.remove_server": return handleMcpRemoveServer(params, mcpManagerRef);
25081
+ default: return Promise.resolve({
24813
25082
  ok: false,
24814
25083
  error: {
24815
- code: "MCP_CALL_FAILED",
24816
- message
25084
+ code: "UNKNOWN_METHOD",
25085
+ message: `Unknown method: ${method}`
24817
25086
  }
24818
- };
25087
+ });
24819
25088
  }
24820
25089
  }
25090
+ function handleStatus() {
25091
+ return {
25092
+ ok: true,
25093
+ payload: {
25094
+ daemon: {
25095
+ status: "running",
25096
+ pid: process.pid,
25097
+ uptime: (Date.now() - startedAt) / 1e3,
25098
+ version: resolvedCliVersion,
25099
+ runtimeVersion: resolvedRuntimeVersion
25100
+ },
25101
+ cloud: {
25102
+ status: cloudConnected ? "connected" : "disconnected",
25103
+ latencyMs: cloudClient.getLatencyMs()
25104
+ },
25105
+ aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
25106
+ plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
25107
+ name: info.name,
25108
+ version: info.version,
25109
+ capabilities: info.capabilities,
25110
+ connectedAt: info.connectedAt,
25111
+ lastSeen: info.lastSeen
25112
+ })),
25113
+ commandQueue: { totalPending: commandQueue.totalPending() }
25114
+ }
25115
+ };
25116
+ }
25117
+ function handleIntegrationList() {
25118
+ return {
25119
+ ok: true,
25120
+ payload: { integrations: integrationManager.list() }
25121
+ };
25122
+ }
24821
25123
  function handleIntegrationReport(params, pluginId) {
24822
25124
  const { name, status, detail } = params;
24823
- if (!name || !status) return {
25125
+ if (typeof name !== "string" || name.length === 0 || name.length > 256 || typeof status !== "string" || status.length === 0 || status.length > 64) return {
24824
25126
  ok: false,
24825
25127
  error: {
24826
25128
  code: "INVALID_PARAMS",
24827
- message: "name and status are required"
25129
+ message: "name and status must be non-empty bounded strings"
24828
25130
  }
24829
25131
  };
24830
25132
  logger$1.info({
24831
25133
  pluginId,
24832
25134
  integration: name,
24833
25135
  status,
24834
- detail
25136
+ hasDetail: detail !== void 0
24835
25137
  }, "Integration status report");
24836
25138
  return {
24837
25139
  ok: true,
@@ -24869,6 +25171,12 @@ async function queryDaemonHealth(socketPath, timeoutMs = 5e3) {
24869
25171
  }, timeoutMs);
24870
25172
  socket.on("data", (data) => {
24871
25173
  buffer += data.toString();
25174
+ if (Buffer.byteLength(buffer, "utf8") > 1048576) {
25175
+ clearTimeout(timer);
25176
+ socket.destroy();
25177
+ reject(/* @__PURE__ */ new Error("Health response exceeded IPC size limit"));
25178
+ return;
25179
+ }
24872
25180
  const newlineIdx = buffer.indexOf("\n");
24873
25181
  if (newlineIdx === -1) return;
24874
25182
  const line = buffer.slice(0, newlineIdx).trim();
@@ -24877,7 +25185,8 @@ async function queryDaemonHealth(socketPath, timeoutMs = 5e3) {
24877
25185
  socket.end();
24878
25186
  try {
24879
25187
  const response = JSON.parse(line);
24880
- if (response.ok && response.payload) resolve(response.payload);
25188
+ if (!isIPCResponse(response)) reject(/* @__PURE__ */ new Error("Invalid health response"));
25189
+ else if (response.ok && response.payload) resolve(response.payload);
24881
25190
  else reject(new Error(response.error?.message ?? "Health check failed"));
24882
25191
  } catch {
24883
25192
  reject(/* @__PURE__ */ new Error("Invalid health response"));