@alfe.ai/gateway 0.9.3 → 0.9.5

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
@@ -2,20 +2,20 @@ 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
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";
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";
16
15
  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
16
  import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
17
+ import { promisify } from "node:util";
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";
@@ -4327,6 +4327,7 @@ enumValues({
4327
4327
  //#endregion
4328
4328
  //#region ../../packages-internal/types/dist/models.js
4329
4329
  const AnthropicModel = {
4330
+ Opus5: "claude-opus-5",
4330
4331
  Opus48: "claude-opus-4-8",
4331
4332
  Opus47: "claude-opus-4-7",
4332
4333
  Opus46: "claude-opus-4-6",
@@ -4414,8 +4415,7 @@ const MoonshotModel = {
4414
4415
  K3: "kimi-k3",
4415
4416
  K26: "kimi-k2.6",
4416
4417
  K27Code: "kimi-k2.7-code",
4417
- K27CodeHighSpeed: "kimi-k2.7-code-highspeed",
4418
- K25: "kimi-k2.5"
4418
+ K27CodeHighSpeed: "kimi-k2.7-code-highspeed"
4419
4419
  };
4420
4420
  const MOONSHOT_MODELS = enumValues(MoonshotModel);
4421
4421
  const QwenModel = {
@@ -4449,8 +4449,8 @@ _enum(ZHIPU_MODELS);
4449
4449
  _enum(MOONSHOT_MODELS);
4450
4450
  _enum(QWEN_MODELS);
4451
4451
  string().min(1);
4452
- 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, MoonshotModel.K25, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4453
- 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, MoonshotModel.K25, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4452
+ 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
+ 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
4454
  enumValues({
4455
4455
  PendingChallenge: "pending_challenge",
4456
4456
  Creating: "creating",
@@ -4528,6 +4528,7 @@ const NotificationType = {
4528
4528
  SubscriptionCancelled: "subscription.cancelled",
4529
4529
  StartupGrantEnded: "startup.grant_ended",
4530
4530
  GrantEnded: "subscription.grant_ended",
4531
+ EnterpriseGrantEnded: "subscription.enterprise_grant_ended",
4531
4532
  SubscriptionPastDue: "subscription.past_due",
4532
4533
  BalanceThresholdWarning: "balance.threshold.warning",
4533
4534
  PlatformTierPriceIncrease: "platform.tier_price_increase",
@@ -4552,7 +4553,7 @@ const RecipientStrategy = {
4552
4553
  TenantAdmins: "tenant_admins",
4553
4554
  SpecificUser: "specific_user"
4554
4555
  };
4555
- 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.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;
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;
4556
4557
  //#endregion
4557
4558
  //#region ../../packages-internal/api-client/dist/services/auth.js
4558
4559
  var AuthService = class {
@@ -5203,63 +5204,54 @@ async function loadDaemonConfig() {
5203
5204
  * Returns null if the agent has no template assigned or the fetch fails.
5204
5205
  */
5205
5206
  async function fetchAgentConfig(apiKey, apiEndpoint) {
5207
+ const client = new AgentApiClient({
5208
+ apiKey,
5209
+ apiUrl: apiEndpoint
5210
+ });
5211
+ logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
5212
+ let workspace;
5206
5213
  try {
5207
- logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
5208
- const wsResponse = await fetch(`${apiEndpoint}/agents/me/workspace`, {
5209
- method: "GET",
5210
- headers: { "Authorization": `Bearer ${apiKey}` }
5211
- });
5212
- if (!wsResponse.ok) {
5213
- logger$1.debug({ status: wsResponse.status }, "Workspace config fetch failed");
5214
- return null;
5215
- }
5216
- const wsResult = await wsResponse.json();
5217
- const templateKey = wsResult.data?.templateKey;
5218
- const defaultModel = wsResult.data?.defaultModel ?? void 0;
5219
- const installedFrom = wsResult.data?.installedFrom;
5220
- if (!templateKey) {
5221
- logger$1.debug("No templateKey in workspace response");
5222
- return defaultModel ? {
5223
- defaultModel,
5224
- files: {}
5225
- } : null;
5226
- }
5227
- const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
5228
- const filesUrl = new URL(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`);
5229
- if (pinnedVersion !== void 0) filesUrl.searchParams.set("version", String(pinnedVersion));
5230
- logger$1.debug({
5231
- templateKey,
5232
- version: pinnedVersion
5233
- }, "Fetching template files...");
5234
- const filesResponse = await fetch(filesUrl.toString(), {
5235
- method: "GET",
5236
- headers: { "Authorization": `Bearer ${apiKey}` }
5237
- });
5238
- if (!filesResponse.ok) {
5239
- logger$1.debug({ status: filesResponse.status }, "Template files fetch failed");
5240
- return {
5241
- templateKey,
5242
- defaultModel,
5243
- files: {}
5244
- };
5245
- }
5246
- const files = (await filesResponse.json()).data?.files ?? {};
5247
- const fileCount = Object.keys(files).length;
5248
- logger$1.debug({
5249
- templateKey,
5250
- version: pinnedVersion,
5251
- fileCount
5252
- }, "Template files fetched");
5214
+ workspace = await client.getWorkspace();
5215
+ } catch (err) {
5216
+ logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Workspace config fetch failed");
5217
+ return null;
5218
+ }
5219
+ const { templateKey, installedFrom } = workspace;
5220
+ const defaultModel = workspace.defaultModel ?? void 0;
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
+ logger$1.debug({
5230
+ templateKey,
5231
+ version: pinnedVersion
5232
+ }, "Fetching template files...");
5233
+ let files;
5234
+ try {
5235
+ files = (await client.getTemplateFiles(templateKey, { version: pinnedVersion })).files;
5236
+ } catch (err) {
5237
+ logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Template files fetch failed");
5253
5238
  return {
5254
5239
  templateKey,
5255
5240
  defaultModel,
5256
- files,
5257
- personaFiles: Object.keys(files)
5241
+ files: {}
5258
5242
  };
5259
- } catch (err) {
5260
- logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
5261
- return null;
5262
5243
  }
5244
+ logger$1.debug({
5245
+ templateKey,
5246
+ version: pinnedVersion,
5247
+ fileCount: Object.keys(files).length
5248
+ }, "Template files fetched");
5249
+ return {
5250
+ templateKey,
5251
+ defaultModel,
5252
+ files,
5253
+ personaFiles: Object.keys(files)
5254
+ };
5263
5255
  }
5264
5256
  //#endregion
5265
5257
  //#region src/protocol.ts
@@ -6359,190 +6351,759 @@ var ConfigReconciler = class {
6359
6351
  }
6360
6352
  };
6361
6353
  //#endregion
6362
- //#region src/ipc-server.ts
6363
- /**
6364
- * IPC Server Unix socket server for local plugin connections.
6365
- *
6366
- * Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
6367
- * Request: { type: 'req', id, method, params }
6368
- * Response: { id, ok, payload?, error? }
6369
- * Event: { type: 'event', event, payload }
6370
- *
6371
- * Each connected plugin registers with its name/version and receives
6372
- * commands from the daemon (forwarded from cloud).
6373
- */
6374
- var IPCServer = class {
6375
- server = null;
6376
- connections = /* @__PURE__ */ new Map();
6377
- socketPath;
6378
- requestHandler = null;
6379
- constructor(socketPath) {
6380
- this.socketPath = socketPath;
6381
- }
6382
- /**
6383
- * Set the handler for incoming IPC requests from plugins.
6384
- */
6385
- setRequestHandler(handler) {
6386
- this.requestHandler = handler;
6354
+ //#region src/command-queue.ts
6355
+ const DEFAULT_TTL_MS$1 = 300 * 1e3;
6356
+ var CommandQueue = class {
6357
+ queues = /* @__PURE__ */ new Map();
6358
+ ttlMs;
6359
+ gcTimer = null;
6360
+ constructor(ttlMs = DEFAULT_TTL_MS$1) {
6361
+ this.ttlMs = ttlMs;
6387
6362
  }
6388
6363
  /**
6389
- * Start listening on the Unix socket.
6364
+ * Start periodic garbage collection of expired commands.
6390
6365
  */
6391
- async start() {
6392
- try {
6393
- unlinkSync(this.socketPath);
6394
- } catch {}
6395
- return new Promise((resolve, reject) => {
6396
- this.server = createServer((socket) => {
6397
- this.handleConnection(socket);
6398
- });
6399
- this.server.on("error", (err) => {
6400
- logger$1.error({ err: err.message }, "IPC server error");
6401
- reject(err);
6402
- });
6403
- this.server.listen(this.socketPath, () => {
6404
- try {
6405
- chmodSync(this.socketPath, 384);
6406
- } catch (err) {
6407
- logger$1.warn({ err }, "Failed to chmod socket");
6408
- }
6409
- logger$1.info({ path: this.socketPath }, "IPC server listening");
6410
- resolve();
6411
- });
6412
- });
6366
+ startGC(intervalMs = 3e4) {
6367
+ this.stopGC();
6368
+ this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
6413
6369
  }
6414
6370
  /**
6415
- * Stop the IPC server and disconnect all plugins.
6371
+ * Stop periodic garbage collection.
6416
6372
  */
6417
- async stop() {
6418
- for (const conn of this.connections.values()) try {
6419
- this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
6420
- conn.socket.end();
6421
- } catch {}
6422
- return new Promise((resolve) => {
6423
- if (!this.server) {
6424
- resolve();
6425
- return;
6426
- }
6427
- this.server.close(() => {
6428
- try {
6429
- unlinkSync(this.socketPath);
6430
- } catch {}
6431
- logger$1.info("IPC server stopped");
6432
- resolve();
6433
- });
6434
- for (const conn of this.connections.values()) conn.socket.destroy();
6435
- this.connections.clear();
6436
- });
6373
+ stopGC() {
6374
+ if (this.gcTimer) {
6375
+ clearInterval(this.gcTimer);
6376
+ this.gcTimer = null;
6377
+ }
6437
6378
  }
6438
6379
  /**
6439
- * Send an IPC request to a specific plugin and wait for response.
6380
+ * Enqueue a command for a specific service.
6440
6381
  */
6441
- async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
6442
- const conn = this.connections.get(pluginId);
6443
- if (!conn?.info) return {
6444
- id: "",
6445
- ok: false,
6446
- error: {
6447
- code: "PLUGIN_NOT_CONNECTED",
6448
- message: `Plugin ${pluginId} not connected`
6449
- }
6450
- };
6451
- const id = pluginConnectionId();
6452
- const request = {
6453
- type: "req",
6454
- id,
6455
- method,
6456
- params
6457
- };
6458
- return new Promise((resolve, reject) => {
6459
- const timer = setTimeout(() => {
6460
- conn.pending.delete(id);
6461
- resolve({
6462
- id,
6463
- ok: false,
6464
- error: {
6465
- code: "TIMEOUT",
6466
- message: `Request ${method} timed out after ${String(timeoutMs)}ms`
6467
- }
6468
- });
6469
- }, timeoutMs);
6470
- conn.pending.set(id, {
6471
- resolve,
6472
- reject,
6473
- timer
6474
- });
6475
- try {
6476
- conn.socket.write(JSON.stringify(request) + "\n");
6477
- } catch (err) {
6478
- clearTimeout(timer);
6479
- conn.pending.delete(id);
6480
- resolve({
6481
- id,
6482
- ok: false,
6483
- error: {
6484
- code: "SEND_FAILED",
6485
- message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
6486
- }
6487
- });
6488
- }
6382
+ enqueue(serviceId, request, commandId) {
6383
+ let queue = this.queues.get(serviceId);
6384
+ if (!queue) {
6385
+ queue = [];
6386
+ this.queues.set(serviceId, queue);
6387
+ }
6388
+ queue.push({
6389
+ request,
6390
+ queuedAt: Date.now(),
6391
+ commandId
6489
6392
  });
6490
6393
  }
6491
6394
  /**
6492
- * Send an IPC request to ALL registered plugins.
6493
- * Returns responses keyed by plugin ID.
6395
+ * Drain all pending (non-expired) commands for a service.
6396
+ * Returns them in order and removes them from the queue.
6494
6397
  */
6495
- async broadcastRequest(method, params, timeoutMs = 3e4) {
6496
- const results = /* @__PURE__ */ new Map();
6497
- const registered = this.getRegisteredPlugins();
6498
- await Promise.all(registered.map(async ([pluginId]) => {
6499
- const response = await this.sendRequest(pluginId, method, params, timeoutMs);
6500
- results.set(pluginId, response);
6501
- }));
6502
- return results;
6398
+ drain(serviceId) {
6399
+ const queue = this.queues.get(serviceId);
6400
+ if (!queue || queue.length === 0) return [];
6401
+ const now = Date.now();
6402
+ const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6403
+ this.queues.delete(serviceId);
6404
+ return valid;
6503
6405
  }
6504
6406
  /**
6505
- * Send an event to a specific plugin (fire-and-forget).
6407
+ * Get the number of pending commands for a service.
6506
6408
  */
6507
- sendEventToPlugin(pluginId, event, payload) {
6508
- const conn = this.connections.get(pluginId);
6509
- if (!conn) return false;
6510
- return this.sendEvent(conn, event, payload);
6409
+ pendingCount(serviceId) {
6410
+ const queue = this.queues.get(serviceId);
6411
+ if (!queue) return 0;
6412
+ const now = Date.now();
6413
+ return queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs).length;
6511
6414
  }
6512
6415
  /**
6513
- * Broadcast an event to all connected plugins.
6416
+ * Get total pending commands across all services.
6514
6417
  */
6515
- broadcastEvent(event, payload) {
6516
- for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
6418
+ totalPending() {
6419
+ let total = 0;
6420
+ for (const [serviceId] of this.queues) total += this.pendingCount(serviceId);
6421
+ return total;
6517
6422
  }
6518
6423
  /**
6519
- * Get all registered plugins and their info.
6424
+ * Remove expired commands from all queues.
6520
6425
  */
6521
- getRegisteredPlugins() {
6522
- const plugins = [];
6523
- for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
6524
- return plugins;
6426
+ purgeExpired() {
6427
+ const now = Date.now();
6428
+ let purged = 0;
6429
+ for (const [serviceId, queue] of this.queues) {
6430
+ const before = queue.length;
6431
+ const remaining = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6432
+ if (remaining.length === 0) this.queues.delete(serviceId);
6433
+ else this.queues.set(serviceId, remaining);
6434
+ purged += before - remaining.length;
6435
+ }
6436
+ return purged;
6525
6437
  }
6526
6438
  /**
6527
- * Get number of connected plugins (including unregistered).
6439
+ * Clear all queues.
6528
6440
  */
6529
- get connectionCount() {
6530
- return this.connections.size;
6441
+ clear() {
6442
+ this.queues.clear();
6531
6443
  }
6532
6444
  /**
6533
- * Get number of registered plugins.
6445
+ * Get all service IDs that have pending commands.
6534
6446
  */
6535
- get registeredCount() {
6536
- let count = 0;
6537
- for (const conn of this.connections.values()) if (conn.info) count++;
6538
- return count;
6447
+ serviceIds() {
6448
+ return Array.from(this.queues.keys());
6539
6449
  }
6540
- handleConnection(socket) {
6541
- const connId = pluginConnectionId();
6542
- const conn = {
6543
- id: connId,
6544
- socket,
6545
- info: null,
6450
+ };
6451
+ //#endregion
6452
+ //#region src/process-manager.ts
6453
+ /**
6454
+ * Process management — launchd/systemd service installation.
6455
+ *
6456
+ * Generates and installs user-space service units for auto-start on boot.
6457
+ * No root required — uses user agents (Mac) or user units (Linux).
6458
+ */
6459
+ const LAUNCHD_LABEL = "ai.alfe.gateway";
6460
+ const SYSTEMD_SERVICE = "alfe-gateway";
6461
+ /**
6462
+ * On-disk path for the boot-time self-heal guard script (Linux only).
6463
+ * Written next to the systemd unit at setup time and invoked via
6464
+ * `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
6465
+ */
6466
+ function getGuardScriptPath() {
6467
+ return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
6468
+ }
6469
+ function isRootUser() {
6470
+ return process.getuid?.() === 0;
6471
+ }
6472
+ function getSystemdSystemServicePath() {
6473
+ return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
6474
+ }
6475
+ function getLaunchdPlistPath() {
6476
+ return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6477
+ }
6478
+ function getSystemdServicePath() {
6479
+ return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
6480
+ }
6481
+ /**
6482
+ * Resolve the alfe CLI path. Globally installed via npm, always in PATH.
6483
+ */
6484
+ function getAlfeBinPath() {
6485
+ try {
6486
+ return execSync("which alfe", { encoding: "utf-8" }).trim();
6487
+ } catch {
6488
+ return "alfe";
6489
+ }
6490
+ }
6491
+ /**
6492
+ * Build the PATH for the launchd plist, ensuring the directory containing the
6493
+ * resolved alfe binary is included (e.g. /opt/homebrew/bin on Apple Silicon).
6494
+ */
6495
+ function buildLaunchdPath(alfeBin) {
6496
+ const basePaths = [
6497
+ "/usr/local/bin",
6498
+ "/usr/bin",
6499
+ "/bin",
6500
+ join(homedir(), ".local", "bin")
6501
+ ];
6502
+ const binDir = dirname(alfeBin);
6503
+ if (binDir && !basePaths.includes(binDir)) basePaths.unshift(binDir);
6504
+ return basePaths.join(":");
6505
+ }
6506
+ /**
6507
+ * Generate a launchd plist for macOS.
6508
+ */
6509
+ function generateLaunchdPlist() {
6510
+ const alfeBin = getAlfeBinPath();
6511
+ return `<?xml version="1.0" encoding="UTF-8"?>
6512
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6513
+ <plist version="1.0">
6514
+ <dict>
6515
+ <key>Label</key>
6516
+ <string>${LAUNCHD_LABEL}</string>
6517
+ <key>ProgramArguments</key>
6518
+ <array>
6519
+ <string>${alfeBin}</string>
6520
+ <string>gateway</string>
6521
+ <string>daemon</string>
6522
+ </array>
6523
+ <key>RunAtLoad</key>
6524
+ <true/>
6525
+ <key>KeepAlive</key>
6526
+ <true/>
6527
+ <key>ThrottleInterval</key>
6528
+ <integer>10</integer>
6529
+ <key>StandardOutPath</key>
6530
+ <string>${join(homedir(), ".alfe", "logs", "gateway.log")}</string>
6531
+ <key>StandardErrorPath</key>
6532
+ <string>${join(homedir(), ".alfe", "logs", "gateway.err.log")}</string>
6533
+ <key>EnvironmentVariables</key>
6534
+ <dict>
6535
+ <key>NODE_ENV</key>
6536
+ <string>production</string>
6537
+ <key>PATH</key>
6538
+ <string>${buildLaunchdPath(alfeBin)}</string>
6539
+ </dict>
6540
+ </dict>
6541
+ </plist>`;
6542
+ }
6543
+ /**
6544
+ * Boot-time self-heal guard (Linux only).
6545
+ *
6546
+ * Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
6547
+ * (possibly-broken) `alfe` binary. It exists for the case defense #1
6548
+ * (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
6549
+ * is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
6550
+ * package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
6551
+ * systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
6552
+ * permanent brick with no automatic recovery (real prod incident,
6553
+ * ~13,570 restarts over 2 days).
6554
+ *
6555
+ * The guard:
6556
+ * - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
6557
+ * `alfe --version` exits 0;
6558
+ * - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
6559
+ * baked into the unit's `Environment=`; falls back to `@latest` when
6560
+ * unset), so the daemon then execs a working binary;
6561
+ * - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
6562
+ * its own failures log + `exit 0` so a human can still SSH in.
6563
+ *
6564
+ * POSIX sh (dash-safe): the box may not have bash. Keep the two intactness
6565
+ * checks in lockstep with `verifyCliInstall` in `verify-cli-install.ts`.
6566
+ */
6567
+ /** @internal exported for unit tests; not re-exported from the barrel. */
6568
+ function generateGuardScript() {
6569
+ const version = process.env.ALFE_CLI_VERSION;
6570
+ return `#!/bin/sh
6571
+ # Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
6572
+ # Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
6573
+ # so a mid-install reboot can't brick the agent with a 203/EXEC crash-loop.
6574
+ # Must never wedge boot: every failure path logs and exits 0.
6575
+ set -u
6576
+
6577
+ TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
6578
+ log() { echo "[alfe-cli-guard] $*" >&2; }
6579
+
6580
+ # Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
6581
+ # the whole symlink chain; fall back to the bin path itself if unavailable.
6582
+ BIN="$(command -v alfe 2>/dev/null || true)"
6583
+ if [ -n "$BIN" ]; then
6584
+ ENTRY="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
6585
+ else
6586
+ ENTRY=""
6587
+ fi
6588
+
6589
+ healthy=1
6590
+ if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
6591
+ healthy=0
6592
+ log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
6593
+ elif ! alfe --version >/dev/null 2>&1; then
6594
+ healthy=0
6595
+ log "alfe --version failed"
6596
+ fi
6597
+
6598
+ if [ "$healthy" -eq 1 ]; then
6599
+ exit 0
6600
+ fi
6601
+
6602
+ log "CLI install looks broken — reinstalling $TARGET"
6603
+ # Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
6604
+ GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
6605
+ if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
6606
+ rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
6607
+ fi
6608
+
6609
+ # Bound the reinstall so a slow/unreachable registry at boot can't stall the
6610
+ # ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
6611
+ # - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
6612
+ # - a wall-clock 'timeout' wrapper caps the whole install when present
6613
+ # ('timeout' isn't guaranteed, so fall back to a bare install).
6614
+ # stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
6615
+ if command -v timeout >/dev/null 2>&1; then
6616
+ timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6617
+ rc=$?
6618
+ else
6619
+ npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6620
+ rc=$?
6621
+ fi
6622
+ if [ "$rc" -eq 0 ]; then
6623
+ log "reinstall succeeded"
6624
+ else
6625
+ log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
6626
+ fi
6627
+
6628
+ # Always exit 0: even a failed repair must not block the unit from attempting
6629
+ # ExecStart (which then fails visibly) rather than wedging silently in pre-start.
6630
+ exit 0
6631
+ `;
6632
+ }
6633
+ /**
6634
+ * Generate a systemd unit for Linux.
6635
+ * Root users get a system-level unit; non-root get a user-level unit.
6636
+ */
6637
+ /** @internal exported for unit tests; not re-exported from the barrel. */
6638
+ function generateSystemdUnit() {
6639
+ const alfeBin = getAlfeBinPath();
6640
+ const root = isRootUser();
6641
+ const envLines = [
6642
+ "ALFE_MANAGED",
6643
+ "ALFE_API_KEY",
6644
+ "LOG_LEVEL",
6645
+ "ALFE_CLI_VERSION"
6646
+ ].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
6647
+ return `[Unit]
6648
+ Description=Alfe Gateway Daemon
6649
+ After=network-online.target
6650
+ Wants=network-online.target
6651
+
6652
+ [Service]
6653
+ Type=simple
6654
+ ExecStartPre=-/bin/sh ${getGuardScriptPath()}
6655
+ ExecStart=${alfeBin} gateway daemon
6656
+ Restart=always
6657
+ RestartSec=10
6658
+ # SIGTERM only the daemon on stop/restart; it closes its MCP/runtime children
6659
+ # itself. The default (control-group) SIGTERMs the children simultaneously, so
6660
+ # they die before the daemon's orderly dispose reaches them and every planned
6661
+ # restart reports MCP "server-crash" noise to Sentry. Stragglers still get
6662
+ # SIGKILL when the stop timeout expires.
6663
+ KillMode=mixed
6664
+ Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
6665
+ ${envLines}
6666
+
6667
+ [Install]
6668
+ WantedBy=${root ? "multi-user.target" : "default.target"}`;
6669
+ }
6670
+ /**
6671
+ * Write the boot-time self-heal guard script to disk (Linux only) and make it
6672
+ * executable. Called from `installSystemd` before the unit is written.
6673
+ */
6674
+ async function writeGuardScript() {
6675
+ const guardPath = getGuardScriptPath();
6676
+ await mkdir(dirname(guardPath), { recursive: true });
6677
+ await writeFile(guardPath, generateGuardScript(), {
6678
+ encoding: "utf-8",
6679
+ mode: 493
6680
+ });
6681
+ logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
6682
+ }
6683
+ /**
6684
+ * Install the service unit for the current platform.
6685
+ */
6686
+ async function installService() {
6687
+ const platform = process.platform;
6688
+ if (platform === "darwin") return installLaunchd();
6689
+ if (platform === "linux") return installSystemd();
6690
+ throw new Error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`);
6691
+ }
6692
+ /**
6693
+ * Uninstall the service unit for the current platform.
6694
+ */
6695
+ async function uninstallService() {
6696
+ const platform = process.platform;
6697
+ if (platform === "darwin") return uninstallLaunchd();
6698
+ if (platform === "linux") return uninstallSystemd();
6699
+ throw new Error(`Unsupported platform: ${platform}`);
6700
+ }
6701
+ function getLaunchdUid() {
6702
+ return execSync("id -u", { encoding: "utf-8" }).trim();
6703
+ }
6704
+ async function installLaunchd() {
6705
+ const plistPath = getLaunchdPlistPath();
6706
+ const dir = join(homedir(), "Library", "LaunchAgents");
6707
+ const logsDir = join(homedir(), ".alfe", "logs");
6708
+ await mkdir(dir, { recursive: true });
6709
+ await mkdir(logsDir, { recursive: true });
6710
+ await writeFile(plistPath, generateLaunchdPlist(), "utf-8");
6711
+ logger$1.info({ path: plistPath }, "Wrote launchd plist");
6712
+ const uid = getLaunchdUid();
6713
+ try {
6714
+ execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
6715
+ } catch {}
6716
+ try {
6717
+ execSync(`launchctl bootstrap gui/${uid} ${plistPath}`, { stdio: "pipe" });
6718
+ } catch (err) {
6719
+ const msg = err instanceof Error ? err.message : String(err);
6720
+ logger$1.error({
6721
+ err: msg,
6722
+ plistPath
6723
+ }, "launchctl bootstrap failed");
6724
+ return `Installed plist at ${plistPath}, but failed to bootstrap service: ${msg}`;
6725
+ }
6726
+ return `Installed: ${plistPath}\nService will start on boot and restart on crash.`;
6727
+ }
6728
+ async function uninstallLaunchd() {
6729
+ const plistPath = getLaunchdPlistPath();
6730
+ const uid = getLaunchdUid();
6731
+ try {
6732
+ execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
6733
+ } catch {}
6734
+ try {
6735
+ await unlink(plistPath);
6736
+ } catch {}
6737
+ return `Uninstalled: ${plistPath}`;
6738
+ }
6739
+ async function installSystemd() {
6740
+ const root = isRootUser();
6741
+ const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
6742
+ const dir = root ? "/etc/systemd/system" : join(homedir(), ".config", "systemd", "user");
6743
+ const ctl = root ? "systemctl" : "systemctl --user";
6744
+ if (!root) await mkdir(dir, { recursive: true });
6745
+ await writeGuardScript();
6746
+ await writeFile(unitPath, generateSystemdUnit(), "utf-8");
6747
+ logger$1.info({
6748
+ path: unitPath,
6749
+ root
6750
+ }, "Wrote systemd unit");
6751
+ try {
6752
+ execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
6753
+ execSync(`${ctl} enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6754
+ } catch {}
6755
+ return `Installed: ${unitPath}\nService enabled${root ? " (system-level)" : " for user session"}.`;
6756
+ }
6757
+ async function uninstallSystemd() {
6758
+ const root = isRootUser();
6759
+ const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
6760
+ const ctl = root ? "systemctl" : "systemctl --user";
6761
+ try {
6762
+ execSync(`${ctl} disable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6763
+ execSync(`${ctl} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6764
+ } catch {}
6765
+ try {
6766
+ await unlink(unitPath);
6767
+ } catch {}
6768
+ try {
6769
+ await unlink(getGuardScriptPath());
6770
+ } catch {}
6771
+ try {
6772
+ execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
6773
+ } catch {}
6774
+ return `Uninstalled: ${unitPath}`;
6775
+ }
6776
+ /**
6777
+ * Bootstrap (load) the launchd service from its plist if it isn't already
6778
+ * loaded. Already-loaded is an error we ignore. Needed so `start`/`restart`
6779
+ * work after `stopService()` boots the service OUT.
6780
+ */
6781
+ function ensureLaunchdLoaded(uid) {
6782
+ try {
6783
+ execSync(`launchctl bootstrap gui/${uid} ${getLaunchdPlistPath()}`, { stdio: "pipe" });
6784
+ } catch {}
6785
+ }
6786
+ /**
6787
+ * Start the installed service via systemctl/launchctl. Bootstraps the launchd
6788
+ * service first so this also works after `stopService()` booted it out.
6789
+ */
6790
+ function startService() {
6791
+ const platform = process.platform;
6792
+ if (platform === "darwin") {
6793
+ const uid = getLaunchdUid();
6794
+ ensureLaunchdLoaded(uid);
6795
+ execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
6796
+ logger$1.info("Started launchd service");
6797
+ return;
6798
+ }
6799
+ if (platform === "linux") {
6800
+ execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} start ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6801
+ logger$1.info("Started systemd service");
6802
+ return;
6803
+ }
6804
+ throw new Error(`Unsupported platform: ${platform}`);
6805
+ }
6806
+ /**
6807
+ * Restart the installed service via systemctl/launchctl. This is the
6808
+ * service-manager-native restart (`kickstart -k` / `systemctl restart`) — the
6809
+ * CLI uses it instead of killing the process inline, which would fight launchd
6810
+ * `KeepAlive` / systemd `Restart=always`.
6811
+ */
6812
+ function restartService() {
6813
+ const platform = process.platform;
6814
+ if (platform === "darwin") {
6815
+ const uid = getLaunchdUid();
6816
+ ensureLaunchdLoaded(uid);
6817
+ execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
6818
+ logger$1.info("Restarted launchd service");
6819
+ return;
6820
+ }
6821
+ if (platform === "linux") {
6822
+ execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} restart ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6823
+ logger$1.info("Restarted systemd service");
6824
+ return;
6825
+ }
6826
+ throw new Error(`Unsupported platform: ${platform}`);
6827
+ }
6828
+ /**
6829
+ * Stop the installed service via systemctl/launchctl. On macOS this boots the
6830
+ * service OUT (unloads it) so launchd's `KeepAlive` does NOT respawn it;
6831
+ * `startService()` bootstraps it again. On Linux the unit stays enabled (starts
6832
+ * on next boot); `systemctl stop` just halts the current run.
6833
+ */
6834
+ function stopService() {
6835
+ const platform = process.platform;
6836
+ if (platform === "darwin") {
6837
+ execSync(`launchctl bootout gui/${getLaunchdUid()}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
6838
+ logger$1.info("Stopped launchd service");
6839
+ return;
6840
+ }
6841
+ if (platform === "linux") {
6842
+ execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
6843
+ logger$1.info("Stopped systemd service");
6844
+ return;
6845
+ }
6846
+ throw new Error(`Unsupported platform: ${platform}`);
6847
+ }
6848
+ /**
6849
+ * True when the gateway is installed as a system service (launchd plist /
6850
+ * systemd unit present). Lets the CLI restart/stop/start THROUGH the service
6851
+ * manager instead of driving the daemon inline via the PID file.
6852
+ */
6853
+ function isServiceInstalled() {
6854
+ const platform = process.platform;
6855
+ if (platform === "darwin") return existsSync(getLaunchdPlistPath());
6856
+ if (platform === "linux") return existsSync(isRootUser() ? getSystemdSystemServicePath() : getSystemdServicePath());
6857
+ return false;
6858
+ }
6859
+ /**
6860
+ * Write the current process PID to the PID file.
6861
+ */
6862
+ async function writePidFile() {
6863
+ await writeFile(PID_PATH, String(process.pid), "utf-8");
6864
+ }
6865
+ /**
6866
+ * Remove the PID file.
6867
+ */
6868
+ async function removePidFile() {
6869
+ try {
6870
+ await unlink(PID_PATH);
6871
+ } catch {}
6872
+ }
6873
+ /**
6874
+ * Check if a daemon is already running.
6875
+ * Returns the PID if alive, null if not running.
6876
+ */
6877
+ async function checkExistingDaemon() {
6878
+ if (!existsSync(PID_PATH)) return null;
6879
+ try {
6880
+ const pidStr = await readFile(PID_PATH, "utf-8");
6881
+ const pid = parseInt(pidStr.trim(), 10);
6882
+ if (isNaN(pid)) {
6883
+ await removePidFile();
6884
+ return null;
6885
+ }
6886
+ try {
6887
+ process.kill(pid, 0);
6888
+ return pid;
6889
+ } catch {
6890
+ await removePidFile();
6891
+ return null;
6892
+ }
6893
+ } catch {
6894
+ return null;
6895
+ }
6896
+ }
6897
+ /**
6898
+ * Send SIGTERM to an existing daemon process.
6899
+ */
6900
+ async function stopExistingDaemon() {
6901
+ const pid = await checkExistingDaemon();
6902
+ if (!pid) return false;
6903
+ try {
6904
+ process.kill(pid, "SIGTERM");
6905
+ for (let i = 0; i < 50; i++) {
6906
+ await new Promise((r) => setTimeout(r, 100));
6907
+ try {
6908
+ process.kill(pid, 0);
6909
+ } catch {
6910
+ await removePidFile();
6911
+ return true;
6912
+ }
6913
+ }
6914
+ process.kill(pid, "SIGKILL");
6915
+ await removePidFile();
6916
+ return true;
6917
+ } catch {
6918
+ await removePidFile();
6919
+ return false;
6920
+ }
6921
+ }
6922
+ //#endregion
6923
+ //#region src/ipc-server.ts
6924
+ /**
6925
+ * IPC Server — Unix socket server for local plugin connections.
6926
+ *
6927
+ * Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
6928
+ * Request: { type: 'req', id, method, params }
6929
+ * Response: { id, ok, payload?, error? }
6930
+ * Event: { type: 'event', event, payload }
6931
+ *
6932
+ * Each connected plugin registers with its name/version and receives
6933
+ * commands from the daemon (forwarded from cloud).
6934
+ */
6935
+ var IPCServer = class {
6936
+ server = null;
6937
+ connections = /* @__PURE__ */ new Map();
6938
+ socketPath;
6939
+ requestHandler = null;
6940
+ constructor(socketPath) {
6941
+ this.socketPath = socketPath;
6942
+ }
6943
+ /**
6944
+ * Set the handler for incoming IPC requests from plugins.
6945
+ */
6946
+ setRequestHandler(handler) {
6947
+ this.requestHandler = handler;
6948
+ }
6949
+ /**
6950
+ * Start listening on the Unix socket.
6951
+ */
6952
+ async start() {
6953
+ try {
6954
+ unlinkSync(this.socketPath);
6955
+ } catch {}
6956
+ return new Promise((resolve, reject) => {
6957
+ this.server = createServer((socket) => {
6958
+ this.handleConnection(socket);
6959
+ });
6960
+ this.server.on("error", (err) => {
6961
+ logger$1.error({ err: err.message }, "IPC server error");
6962
+ reject(err);
6963
+ });
6964
+ this.server.listen(this.socketPath, () => {
6965
+ try {
6966
+ chmodSync(this.socketPath, 384);
6967
+ } catch (err) {
6968
+ logger$1.warn({ err }, "Failed to chmod socket");
6969
+ }
6970
+ logger$1.info({ path: this.socketPath }, "IPC server listening");
6971
+ resolve();
6972
+ });
6973
+ });
6974
+ }
6975
+ /**
6976
+ * Stop the IPC server and disconnect all plugins.
6977
+ */
6978
+ async stop() {
6979
+ for (const conn of this.connections.values()) try {
6980
+ this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
6981
+ conn.socket.end();
6982
+ } catch {}
6983
+ return new Promise((resolve) => {
6984
+ if (!this.server) {
6985
+ resolve();
6986
+ return;
6987
+ }
6988
+ this.server.close(() => {
6989
+ try {
6990
+ unlinkSync(this.socketPath);
6991
+ } catch {}
6992
+ logger$1.info("IPC server stopped");
6993
+ resolve();
6994
+ });
6995
+ for (const conn of this.connections.values()) conn.socket.destroy();
6996
+ this.connections.clear();
6997
+ });
6998
+ }
6999
+ /**
7000
+ * Send an IPC request to a specific plugin and wait for response.
7001
+ */
7002
+ async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
7003
+ const conn = this.connections.get(pluginId);
7004
+ if (!conn?.info) return {
7005
+ id: "",
7006
+ ok: false,
7007
+ error: {
7008
+ code: "PLUGIN_NOT_CONNECTED",
7009
+ message: `Plugin ${pluginId} not connected`
7010
+ }
7011
+ };
7012
+ const id = pluginConnectionId();
7013
+ const request = {
7014
+ type: "req",
7015
+ id,
7016
+ method,
7017
+ params
7018
+ };
7019
+ return new Promise((resolve, reject) => {
7020
+ const timer = setTimeout(() => {
7021
+ conn.pending.delete(id);
7022
+ resolve({
7023
+ id,
7024
+ ok: false,
7025
+ error: {
7026
+ code: "TIMEOUT",
7027
+ message: `Request ${method} timed out after ${String(timeoutMs)}ms`
7028
+ }
7029
+ });
7030
+ }, timeoutMs);
7031
+ conn.pending.set(id, {
7032
+ resolve,
7033
+ reject,
7034
+ timer
7035
+ });
7036
+ try {
7037
+ conn.socket.write(JSON.stringify(request) + "\n");
7038
+ } catch (err) {
7039
+ clearTimeout(timer);
7040
+ conn.pending.delete(id);
7041
+ resolve({
7042
+ id,
7043
+ ok: false,
7044
+ error: {
7045
+ code: "SEND_FAILED",
7046
+ message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
7047
+ }
7048
+ });
7049
+ }
7050
+ });
7051
+ }
7052
+ /**
7053
+ * Send an IPC request to ALL registered plugins.
7054
+ * Returns responses keyed by plugin ID.
7055
+ */
7056
+ async broadcastRequest(method, params, timeoutMs = 3e4) {
7057
+ const results = /* @__PURE__ */ new Map();
7058
+ const registered = this.getRegisteredPlugins();
7059
+ await Promise.all(registered.map(async ([pluginId]) => {
7060
+ const response = await this.sendRequest(pluginId, method, params, timeoutMs);
7061
+ results.set(pluginId, response);
7062
+ }));
7063
+ return results;
7064
+ }
7065
+ /**
7066
+ * Send an event to a specific plugin (fire-and-forget).
7067
+ */
7068
+ sendEventToPlugin(pluginId, event, payload) {
7069
+ const conn = this.connections.get(pluginId);
7070
+ if (!conn) return false;
7071
+ return this.sendEvent(conn, event, payload);
7072
+ }
7073
+ /**
7074
+ * Broadcast an event to all connected plugins.
7075
+ */
7076
+ broadcastEvent(event, payload) {
7077
+ for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
7078
+ }
7079
+ /**
7080
+ * Get all registered plugins and their info.
7081
+ */
7082
+ getRegisteredPlugins() {
7083
+ const plugins = [];
7084
+ for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
7085
+ return plugins;
7086
+ }
7087
+ /**
7088
+ * Get number of connected plugins (including unregistered).
7089
+ */
7090
+ get connectionCount() {
7091
+ return this.connections.size;
7092
+ }
7093
+ /**
7094
+ * Get number of registered plugins.
7095
+ */
7096
+ get registeredCount() {
7097
+ let count = 0;
7098
+ for (const conn of this.connections.values()) if (conn.info) count++;
7099
+ return count;
7100
+ }
7101
+ handleConnection(socket) {
7102
+ const connId = pluginConnectionId();
7103
+ const conn = {
7104
+ id: connId,
7105
+ socket,
7106
+ info: null,
6546
7107
  buffer: "",
6547
7108
  pending: /* @__PURE__ */ new Map()
6548
7109
  };
@@ -6687,590 +7248,576 @@ var IPCServer = class {
6687
7248
  sendResponse(conn, response) {
6688
7249
  try {
6689
7250
  conn.socket.write(JSON.stringify(response) + "\n");
6690
- } catch (err) {
6691
- logger$1.error({
6692
- connId: conn.id,
6693
- err
6694
- }, "IPC: failed to send response");
6695
- }
6696
- }
6697
- sendEvent(conn, event, payload) {
6698
- try {
6699
- const msg = createIPCEvent(event, payload);
6700
- conn.socket.write(JSON.stringify(msg) + "\n");
6701
- return true;
6702
- } catch {
6703
- return false;
6704
- }
6705
- }
6706
- };
6707
- //#endregion
6708
- //#region src/command-queue.ts
6709
- const DEFAULT_TTL_MS$1 = 300 * 1e3;
6710
- var CommandQueue = class {
6711
- queues = /* @__PURE__ */ new Map();
6712
- ttlMs;
6713
- gcTimer = null;
6714
- constructor(ttlMs = DEFAULT_TTL_MS$1) {
6715
- this.ttlMs = ttlMs;
6716
- }
6717
- /**
6718
- * Start periodic garbage collection of expired commands.
6719
- */
6720
- startGC(intervalMs = 3e4) {
6721
- this.stopGC();
6722
- this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
6723
- }
6724
- /**
6725
- * Stop periodic garbage collection.
6726
- */
6727
- stopGC() {
6728
- if (this.gcTimer) {
6729
- clearInterval(this.gcTimer);
6730
- this.gcTimer = null;
6731
- }
6732
- }
6733
- /**
6734
- * Enqueue a command for a specific service.
6735
- */
6736
- enqueue(serviceId, request, commandId) {
6737
- let queue = this.queues.get(serviceId);
6738
- if (!queue) {
6739
- queue = [];
6740
- this.queues.set(serviceId, queue);
6741
- }
6742
- queue.push({
6743
- request,
6744
- queuedAt: Date.now(),
6745
- commandId
6746
- });
6747
- }
6748
- /**
6749
- * Drain all pending (non-expired) commands for a service.
6750
- * Returns them in order and removes them from the queue.
6751
- */
6752
- drain(serviceId) {
6753
- const queue = this.queues.get(serviceId);
6754
- if (!queue || queue.length === 0) return [];
6755
- const now = Date.now();
6756
- const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6757
- this.queues.delete(serviceId);
6758
- return valid;
6759
- }
6760
- /**
6761
- * Get the number of pending commands for a service.
6762
- */
6763
- pendingCount(serviceId) {
6764
- const queue = this.queues.get(serviceId);
6765
- if (!queue) return 0;
6766
- const now = Date.now();
6767
- return queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs).length;
6768
- }
6769
- /**
6770
- * Get total pending commands across all services.
6771
- */
6772
- totalPending() {
6773
- let total = 0;
6774
- for (const [serviceId] of this.queues) total += this.pendingCount(serviceId);
6775
- return total;
6776
- }
6777
- /**
6778
- * Remove expired commands from all queues.
6779
- */
6780
- purgeExpired() {
6781
- const now = Date.now();
6782
- let purged = 0;
6783
- for (const [serviceId, queue] of this.queues) {
6784
- const before = queue.length;
6785
- const remaining = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
6786
- if (remaining.length === 0) this.queues.delete(serviceId);
6787
- else this.queues.set(serviceId, remaining);
6788
- purged += before - remaining.length;
6789
- }
6790
- return purged;
6791
- }
6792
- /**
6793
- * Clear all queues.
6794
- */
6795
- clear() {
6796
- this.queues.clear();
7251
+ } catch (err) {
7252
+ logger$1.error({
7253
+ connId: conn.id,
7254
+ err
7255
+ }, "IPC: failed to send response");
7256
+ }
6797
7257
  }
6798
- /**
6799
- * Get all service IDs that have pending commands.
6800
- */
6801
- serviceIds() {
6802
- return Array.from(this.queues.keys());
7258
+ sendEvent(conn, event, payload) {
7259
+ try {
7260
+ const msg = createIPCEvent(event, payload);
7261
+ conn.socket.write(JSON.stringify(msg) + "\n");
7262
+ return true;
7263
+ } catch {
7264
+ return false;
7265
+ }
6803
7266
  }
6804
7267
  };
6805
7268
  //#endregion
6806
- //#region src/process-manager.ts
7269
+ //#region src/daemon-bootstrap.ts
6807
7270
  /**
6808
- * Process managementlaunchd/systemd service installation.
7271
+ * Daemon bootstrap helpers startup ordering, config/version resolution,
7272
+ * AI-proxy + IPC server wiring.
6809
7273
  *
6810
- * Generates and installs user-space service units for auto-start on boot.
6811
- * No root required — uses user agents (Mac) or user units (Linux).
6812
- */
6813
- const LAUNCHD_LABEL = "ai.alfe.gateway";
6814
- const SYSTEMD_SERVICE = "alfe-gateway";
6815
- /**
6816
- * On-disk path for the boot-time self-heal guard script (Linux only).
6817
- * Written next to the systemd unit at setup time and invoked via
6818
- * `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
6819
- */
6820
- function getGuardScriptPath() {
6821
- return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
6822
- }
6823
- function isRootUser() {
6824
- return process.getuid?.() === 0;
6825
- }
6826
- function getSystemdSystemServicePath() {
6827
- return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
6828
- }
6829
- function getLaunchdPlistPath() {
6830
- return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6831
- }
6832
- function getSystemdServicePath() {
6833
- return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
6834
- }
6835
- /**
6836
- * Resolve the alfe CLI path. Globally installed via npm, always in PATH.
6837
- */
6838
- function getAlfeBinPath() {
6839
- try {
6840
- return execSync("which alfe", { encoding: "utf-8" }).trim();
6841
- } catch {
6842
- return "alfe";
6843
- }
6844
- }
6845
- /**
6846
- * Build the PATH for the launchd plist, ensuring the directory containing the
6847
- * resolved alfe binary is included (e.g. /opt/homebrew/bin on Apple Silicon).
6848
- */
6849
- function buildLaunchdPath(alfeBin) {
6850
- const basePaths = [
6851
- "/usr/local/bin",
6852
- "/usr/bin",
6853
- "/bin",
6854
- join(homedir(), ".local", "bin")
6855
- ];
6856
- const binDir = dirname(alfeBin);
6857
- if (binDir && !basePaths.includes(binDir)) basePaths.unshift(binDir);
6858
- return basePaths.join(":");
6859
- }
6860
- /**
6861
- * Generate a launchd plist for macOS.
7274
+ * Extracted from daemon.ts (which remains the orchestrator). These are the
7275
+ * standalone pieces of `startDaemon()`: version detection, the one-shot
7276
+ * persona-file migration, the AI proxy listener, and the IPC server start.
6862
7277
  */
6863
- function generateLaunchdPlist() {
6864
- const alfeBin = getAlfeBinPath();
6865
- return `<?xml version="1.0" encoding="UTF-8"?>
6866
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6867
- <plist version="1.0">
6868
- <dict>
6869
- <key>Label</key>
6870
- <string>${LAUNCHD_LABEL}</string>
6871
- <key>ProgramArguments</key>
6872
- <array>
6873
- <string>${alfeBin}</string>
6874
- <string>gateway</string>
6875
- <string>daemon</string>
6876
- </array>
6877
- <key>RunAtLoad</key>
6878
- <true/>
6879
- <key>KeepAlive</key>
6880
- <true/>
6881
- <key>ThrottleInterval</key>
6882
- <integer>10</integer>
6883
- <key>StandardOutPath</key>
6884
- <string>${join(homedir(), ".alfe", "logs", "gateway.log")}</string>
6885
- <key>StandardErrorPath</key>
6886
- <string>${join(homedir(), ".alfe", "logs", "gateway.err.log")}</string>
6887
- <key>EnvironmentVariables</key>
6888
- <dict>
6889
- <key>NODE_ENV</key>
6890
- <string>production</string>
6891
- <key>PATH</key>
6892
- <string>${buildLaunchdPath(alfeBin)}</string>
6893
- </dict>
6894
- </dict>
6895
- </plist>`;
6896
- }
7278
+ const execFileAsync$2 = promisify(execFile);
6897
7279
  /**
6898
- * Boot-time self-heal guard (Linux only).
6899
- *
6900
- * Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
6901
- * (possibly-broken) `alfe` binary. It exists for the case defense #1
6902
- * (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
6903
- * is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
6904
- * package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
6905
- * systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
6906
- * permanent brick with no automatic recovery (real prod incident,
6907
- * ~13,570 restarts over 2 days).
6908
- *
6909
- * The guard:
6910
- * - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
6911
- * `alfe --version` exits 0;
6912
- * - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
6913
- * baked into the unit's `Environment=`; falls back to `@latest` when
6914
- * unset), so the daemon then execs a working binary;
6915
- * - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
6916
- * its own failures log + `exit 0` so a human can still SSH in.
7280
+ * Resolve the installed @alfe.ai/cli version.
6917
7281
  *
6918
- * POSIX sh (dash-safe): the box may not have bash. Keep the two intactness
6919
- * checks in lockstep with `verifyCliInstall` in `verify-cli-install.ts`.
7282
+ * Strategy (in order):
7283
+ * 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
7284
+ * 2. Walk up from this file to find @alfe.ai/cli/package.json
7285
+ * (works when systemd runs the gateway binary directly, since
7286
+ * the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
6920
7287
  */
6921
- /** @internal exported for unit tests; not re-exported from the barrel. */
6922
- function generateGuardScript() {
6923
- const version = process.env.ALFE_CLI_VERSION;
6924
- return `#!/bin/sh
6925
- # Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
6926
- # Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
6927
- # so a mid-install reboot can't brick the agent with a 203/EXEC crash-loop.
6928
- # Must never wedge boot: every failure path logs and exits 0.
6929
- set -u
6930
-
6931
- TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
6932
- log() { echo "[alfe-cli-guard] $*" >&2; }
6933
-
6934
- # Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
6935
- # the whole symlink chain; fall back to the bin path itself if unavailable.
6936
- BIN="$(command -v alfe 2>/dev/null || true)"
6937
- if [ -n "$BIN" ]; then
6938
- ENTRY="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
6939
- else
6940
- ENTRY=""
6941
- fi
6942
-
6943
- healthy=1
6944
- if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
6945
- healthy=0
6946
- log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
6947
- elif ! alfe --version >/dev/null 2>&1; then
6948
- healthy=0
6949
- log "alfe --version failed"
6950
- fi
6951
-
6952
- if [ "$healthy" -eq 1 ]; then
6953
- exit 0
6954
- fi
6955
-
6956
- log "CLI install looks broken — reinstalling $TARGET"
6957
- # Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
6958
- GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
6959
- if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
6960
- rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
6961
- fi
6962
-
6963
- # Bound the reinstall so a slow/unreachable registry at boot can't stall the
6964
- # ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
6965
- # - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
6966
- # - a wall-clock 'timeout' wrapper caps the whole install when present
6967
- # ('timeout' isn't guaranteed, so fall back to a bare install).
6968
- # stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
6969
- if command -v timeout >/dev/null 2>&1; then
6970
- timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6971
- rc=$?
6972
- else
6973
- npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6974
- rc=$?
6975
- fi
6976
- if [ "$rc" -eq 0 ]; then
6977
- log "reinstall succeeded"
6978
- else
6979
- log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
6980
- fi
6981
-
6982
- # Always exit 0: even a failed repair must not block the unit from attempting
6983
- # ExecStart (which then fails visibly) rather than wedging silently in pre-start.
6984
- exit 0
6985
- `;
7288
+ async function getCliVersion() {
7289
+ if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
7290
+ try {
7291
+ const { fileURLToPath } = await import("node:url");
7292
+ const { dirname } = await import("node:path");
7293
+ let dir = dirname(fileURLToPath(import.meta.url));
7294
+ for (let i = 0; i < 10; i++) {
7295
+ const candidate = join(dir, "package.json");
7296
+ try {
7297
+ const raw = await readFile(candidate, "utf-8");
7298
+ const pkg = JSON.parse(raw);
7299
+ if (pkg.name === "@alfe.ai/cli") return pkg.version;
7300
+ } catch {}
7301
+ const parent = dirname(dir);
7302
+ if (parent === dir) break;
7303
+ dir = parent;
7304
+ }
7305
+ } catch {}
7306
+ logger$1.debug("Could not resolve @alfe.ai/cli version");
6986
7307
  }
6987
7308
  /**
6988
- * Generate a systemd unit for Linux.
6989
- * Root users get a system-level unit; non-root get a user-level unit.
6990
- */
6991
- /** @internal exported for unit tests; not re-exported from the barrel. */
6992
- function generateSystemdUnit() {
6993
- const alfeBin = getAlfeBinPath();
6994
- const root = isRootUser();
6995
- const envLines = [
6996
- "ALFE_MANAGED",
6997
- "ALFE_API_KEY",
6998
- "LOG_LEVEL",
6999
- "ALFE_CLI_VERSION"
7000
- ].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
7001
- return `[Unit]
7002
- Description=Alfe Gateway Daemon
7003
- After=network-online.target
7004
- Wants=network-online.target
7005
-
7006
- [Service]
7007
- Type=simple
7008
- ExecStartPre=-/bin/sh ${getGuardScriptPath()}
7009
- ExecStart=${alfeBin} gateway daemon
7010
- Restart=always
7011
- RestartSec=10
7012
- # SIGTERM only the daemon on stop/restart; it closes its MCP/runtime children
7013
- # itself. The default (control-group) SIGTERMs the children simultaneously, so
7014
- # they die before the daemon's orderly dispose reaches them and every planned
7015
- # restart reports MCP "server-crash" noise to Sentry. Stragglers still get
7016
- # SIGKILL when the stop timeout expires.
7017
- KillMode=mixed
7018
- Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
7019
- ${envLines}
7020
-
7021
- [Install]
7022
- WantedBy=${root ? "multi-user.target" : "default.target"}`;
7023
- }
7309
+ * Per-runtime "print installed version" commands. Local to the gateway — we do
7310
+ * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
7311
+ * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
7312
+ * connection-status report degrades gracefully instead of crashing.
7313
+ */
7314
+ const RUNTIME_VERSION_COMMANDS = {
7315
+ openclaw: {
7316
+ command: "openclaw",
7317
+ args: ["--version"]
7318
+ },
7319
+ hermes: {
7320
+ command: "hermes",
7321
+ args: ["version"]
7322
+ },
7323
+ "claude-code": {
7324
+ command: "alfe-claude-host",
7325
+ args: ["--version"]
7326
+ }
7327
+ };
7024
7328
  /**
7025
- * Write the boot-time self-heal guard script to disk (Linux only) and make it
7026
- * executable. Called from `installSystemd` before the unit is written.
7329
+ * Pure resolver (exported for tests) the per-runtime version command, or
7330
+ * `undefined` for an unknown runtime.
7027
7331
  */
7028
- async function writeGuardScript() {
7029
- const guardPath = getGuardScriptPath();
7030
- await mkdir(dirname(guardPath), { recursive: true });
7031
- await writeFile(guardPath, generateGuardScript(), {
7032
- encoding: "utf-8",
7033
- mode: 493
7034
- });
7035
- logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
7332
+ function resolveRuntimeVersionCommand(runtime) {
7333
+ return RUNTIME_VERSION_COMMANDS[runtime];
7036
7334
  }
7037
7335
  /**
7038
- * Install the service unit for the current platform.
7336
+ * Resolve the installed runtime version by running the per-runtime version
7337
+ * command and returning the trimmed stdout. An unknown runtime (no command in
7338
+ * the map) returns `undefined` without spawning — it must never throw.
7039
7339
  */
7040
- async function installService() {
7041
- const platform = process.platform;
7042
- if (platform === "darwin") return installLaunchd();
7043
- if (platform === "linux") return installSystemd();
7044
- throw new Error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`);
7340
+ async function getRuntimeVersion(runtime) {
7341
+ const cmd = resolveRuntimeVersionCommand(runtime);
7342
+ if (!cmd) {
7343
+ logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
7344
+ return;
7345
+ }
7346
+ try {
7347
+ const { stdout } = await execFileAsync$2(cmd.command, cmd.args);
7348
+ return stdout.trim() || void 0;
7349
+ } catch {
7350
+ logger$1.debug({ runtime }, "Could not resolve runtime version");
7351
+ return;
7352
+ }
7045
7353
  }
7046
7354
  /**
7047
- * Uninstall the service unit for the current platform.
7355
+ * Flush pino's async transport and exit.
7356
+ * process.exit() can drop buffered log lines — this ensures they're written first.
7048
7357
  */
7049
- async function uninstallService() {
7050
- const platform = process.platform;
7051
- if (platform === "darwin") return uninstallLaunchd();
7052
- if (platform === "linux") return uninstallSystemd();
7053
- throw new Error(`Unsupported platform: ${platform}`);
7358
+ async function flushAndExit(code) {
7359
+ await flushSentry();
7360
+ await new Promise((resolve) => {
7361
+ logger$1.flush();
7362
+ setTimeout(resolve, 500);
7363
+ });
7364
+ process.exit(code);
7054
7365
  }
7055
- function getLaunchdUid() {
7056
- return execSync("id -u", { encoding: "utf-8" }).trim();
7366
+ /**
7367
+ * One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
7368
+ * fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
7369
+ * OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
7370
+ * (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
7371
+ * workspace already has the file. Idempotent and safe to run on every daemon start.
7372
+ */
7373
+ const PERSONA_FILES = [
7374
+ "SOUL.md",
7375
+ "IDENTITY.md",
7376
+ "BOOTSTRAP.md",
7377
+ "AGENTS.md"
7378
+ ];
7379
+ async function migrateLegacyPersonaFiles(home, agentWorkspace) {
7380
+ if (home === agentWorkspace) return;
7381
+ let moved = 0;
7382
+ let skipped = 0;
7383
+ for (const filename of PERSONA_FILES) {
7384
+ const src = join(home, filename);
7385
+ const dst = join(agentWorkspace, filename);
7386
+ try {
7387
+ await stat(src);
7388
+ } catch {
7389
+ continue;
7390
+ }
7391
+ try {
7392
+ await stat(dst);
7393
+ skipped++;
7394
+ continue;
7395
+ } catch {}
7396
+ try {
7397
+ await mkdir(agentWorkspace, { recursive: true });
7398
+ await rename(src, dst);
7399
+ moved++;
7400
+ } catch (err) {
7401
+ logger$1.warn({
7402
+ src,
7403
+ dst,
7404
+ err: err instanceof Error ? err.message : String(err)
7405
+ }, "Persona file migration: rename failed");
7406
+ }
7407
+ }
7408
+ if (moved > 0 || skipped > 0) logger$1.info({
7409
+ home,
7410
+ agentWorkspace,
7411
+ moved,
7412
+ skipped
7413
+ }, "Persona file backfill migration complete");
7057
7414
  }
7058
- async function installLaunchd() {
7059
- const plistPath = getLaunchdPlistPath();
7060
- const dir = join(homedir(), "Library", "LaunchAgents");
7061
- const logsDir = join(homedir(), ".alfe", "logs");
7062
- await mkdir(dir, { recursive: true });
7063
- await mkdir(logsDir, { recursive: true });
7064
- await writeFile(plistPath, generateLaunchdPlist(), "utf-8");
7065
- logger$1.info({ path: plistPath }, "Wrote launchd plist");
7066
- const uid = getLaunchdUid();
7067
- try {
7068
- execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
7069
- } catch {}
7415
+ /**
7416
+ * Start the local AI proxy (before the runtime so LLM requests work
7417
+ * immediately). Failure is non-fatal — the daemon starts anyway and LLM
7418
+ * requests fail until the proxy is fixed.
7419
+ */
7420
+ async function startAiProxy(apiKey) {
7421
+ logger$1.debug("Starting AI proxy...");
7422
+ const handle = {
7423
+ server: null,
7424
+ url: null,
7425
+ running: false
7426
+ };
7070
7427
  try {
7071
- execSync(`launchctl bootstrap gui/${uid} ${plistPath}`, { stdio: "pipe" });
7428
+ const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
7429
+ const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
7430
+ const proxyUrl = getAiServiceUrlFromToken(apiKey);
7431
+ const port = DEFAULT_AI_PROXY_PORT ?? 18193;
7432
+ handle.server = createProxyServer({
7433
+ port,
7434
+ apiKey,
7435
+ proxyUrl
7436
+ });
7437
+ const server = handle.server;
7438
+ await new Promise((resolve, reject) => {
7439
+ server.listen(port, "127.0.0.1", () => {
7440
+ handle.running = true;
7441
+ handle.url = `http://127.0.0.1:${String(port)}`;
7442
+ logger$1.info({
7443
+ port,
7444
+ upstream: proxyUrl
7445
+ }, "AI proxy started");
7446
+ resolve();
7447
+ });
7448
+ server.on("error", reject);
7449
+ });
7072
7450
  } catch (err) {
7073
- const msg = err instanceof Error ? err.message : String(err);
7451
+ const message = err instanceof Error ? err.message : String(err);
7452
+ const stack = err instanceof Error ? err.stack : void 0;
7074
7453
  logger$1.error({
7075
- err: msg,
7076
- plistPath
7077
- }, "launchctl bootstrap failed");
7078
- return `Installed plist at ${plistPath}, but failed to bootstrap service: ${msg}`;
7454
+ err: message,
7455
+ stack
7456
+ }, "Failed to start AI proxy — LLM requests will fail");
7079
7457
  }
7080
- return `Installed: ${plistPath}\nService will start on boot and restart on crash.`;
7458
+ return handle;
7081
7459
  }
7082
- async function uninstallLaunchd() {
7083
- const plistPath = getLaunchdPlistPath();
7084
- const uid = getLaunchdUid();
7085
- try {
7086
- execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
7087
- } catch {}
7460
+ /**
7461
+ * Start the IPC server (runtime plugins connect via this socket). A failure
7462
+ * here is fatal — plugins have no other path to the daemon — so this flushes
7463
+ * logs and exits instead of returning.
7464
+ */
7465
+ async function startIpcServer(socketPath, requestHandler) {
7466
+ logger$1.debug({ socketPath }, "Starting IPC server...");
7467
+ const ipcServer = new IPCServer(socketPath);
7468
+ ipcServer.setRequestHandler(requestHandler);
7088
7469
  try {
7089
- await unlink(plistPath);
7090
- } catch {}
7091
- return `Uninstalled: ${plistPath}`;
7470
+ await ipcServer.start();
7471
+ logger$1.debug("IPC server started");
7472
+ } catch (err) {
7473
+ const message = err instanceof Error ? err.message : String(err);
7474
+ const stack = err instanceof Error ? err.stack : void 0;
7475
+ logger$1.error({
7476
+ err: message,
7477
+ stack
7478
+ }, "Failed to start IPC server");
7479
+ await flushAndExit(1);
7480
+ }
7481
+ return ipcServer;
7092
7482
  }
7093
- async function installSystemd() {
7094
- const root = isRootUser();
7095
- const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
7096
- const dir = root ? "/etc/systemd/system" : join(homedir(), ".config", "systemd", "user");
7097
- const ctl = root ? "systemctl" : "systemctl --user";
7098
- if (!root) await mkdir(dir, { recursive: true });
7099
- await writeGuardScript();
7100
- await writeFile(unitPath, generateSystemdUnit(), "utf-8");
7101
- logger$1.info({
7102
- path: unitPath,
7103
- root
7104
- }, "Wrote systemd unit");
7105
- try {
7106
- execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
7107
- execSync(`${ctl} enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7108
- } catch {}
7109
- return `Installed: ${unitPath}\nService enabled${root ? " (system-level)" : " for user session"}.`;
7483
+ //#endregion
7484
+ //#region src/mcp-handlers.ts
7485
+ /**
7486
+ * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
7487
+ * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
7488
+ * cheap (in-memory snapshot, no I/O).
7489
+ */
7490
+ function handleMcpListTools(bundler) {
7491
+ if (!bundler) return {
7492
+ ok: false,
7493
+ error: {
7494
+ code: "MCP_BUNDLER_UNAVAILABLE",
7495
+ message: "MCP bundler not initialized"
7496
+ }
7497
+ };
7498
+ return {
7499
+ ok: true,
7500
+ payload: { tools: bundler.listTools() }
7501
+ };
7110
7502
  }
7111
- async function uninstallSystemd() {
7112
- const root = isRootUser();
7113
- const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
7114
- const ctl = root ? "systemctl" : "systemctl --user";
7115
- try {
7116
- execSync(`${ctl} disable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7117
- execSync(`${ctl} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7118
- } catch {}
7119
- try {
7120
- await unlink(unitPath);
7121
- } catch {}
7122
- try {
7123
- await unlink(getGuardScriptPath());
7124
- } catch {}
7125
- try {
7126
- execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
7127
- } catch {}
7128
- return `Uninstalled: ${unitPath}`;
7503
+ /**
7504
+ * Observability backstop for the INTEGRATION warm path. `applyForIntegration`
7505
+ * registers MCP servers into the store and the daemon warms them silently via
7506
+ * the store-change `onChange` hook so a server that fails its connect (e.g.
7507
+ * an MCP server whose backing account/credential wasn't resolvable at warm
7508
+ * time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
7509
+ * making the black hole undiagnosable. After a warm, read `bundler.statuses()`
7510
+ * and warn once per still-failed server so the failure is visible. These
7511
+ * self-heal on the bundler's background retry sweep once the dependency
7512
+ * appears. Returns the servers it warned about (for tests / callers).
7513
+ */
7514
+ function warnFailedMcpServers(bundler, log, reason) {
7515
+ if (!bundler) return [];
7516
+ const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
7517
+ for (const status of failed) log.warn({
7518
+ server: status.name,
7519
+ reason,
7520
+ consecutiveFailures: status.consecutiveFailures,
7521
+ lastError: status.lastError
7522
+ }, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
7523
+ return failed;
7129
7524
  }
7130
7525
  /**
7131
- * Bootstrap (load) the launchd service from its plist if it isn't already
7132
- * loaded. Already-loaded is an error we ignore. Needed so `start`/`restart`
7133
- * work after `stopService()` boots the service OUT.
7526
+ * Route a tool call to the appropriate MCP child via the daemon-hosted
7527
+ * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
7528
+ * the raw JSON object the LLM produced.
7134
7529
  */
7135
- function ensureLaunchdLoaded(uid) {
7136
- try {
7137
- execSync(`launchctl bootstrap gui/${uid} ${getLaunchdPlistPath()}`, { stdio: "pipe" });
7138
- } catch {}
7139
- }
7140
7530
  /**
7141
- * Start the installed service via systemctl/launchctl. Bootstraps the launchd
7142
- * service first so this also works after `stopService()` booted it out.
7531
+ * List every server entry in the alfe bundler store. Lets agents inspect
7532
+ * what they've already registered before adding a new one.
7143
7533
  */
7144
- function startService() {
7145
- const platform = process.platform;
7146
- if (platform === "darwin") {
7147
- const uid = getLaunchdUid();
7148
- ensureLaunchdLoaded(uid);
7149
- execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
7150
- logger$1.info("Started launchd service");
7151
- return;
7152
- }
7153
- if (platform === "linux") {
7154
- execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} start ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7155
- logger$1.info("Started systemd service");
7156
- return;
7157
- }
7158
- throw new Error(`Unsupported platform: ${platform}`);
7534
+ function handleMcpListServers(manager) {
7535
+ if (!manager) return {
7536
+ ok: false,
7537
+ error: {
7538
+ code: "MCP_MANAGER_UNAVAILABLE",
7539
+ message: "MCP manager not initialized"
7540
+ }
7541
+ };
7542
+ const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
7543
+ const byName = new Map(statuses.map((s) => [s.name, s]));
7544
+ return {
7545
+ ok: true,
7546
+ payload: { servers: manager.listServers().map(({ id, entry }) => ({
7547
+ id,
7548
+ entry,
7549
+ status: byName.get(id) ?? {
7550
+ name: id,
7551
+ connected: false,
7552
+ toolCount: 0,
7553
+ consecutiveFailures: 0
7554
+ }
7555
+ })) }
7556
+ };
7159
7557
  }
7160
7558
  /**
7161
- * Restart the installed service via systemctl/launchctl. This is the
7162
- * service-manager-native restart (`kickstart -k` / `systemctl restart`) the
7163
- * CLI uses it instead of killing the process inline, which would fight launchd
7164
- * `KeepAlive` / systemd `Restart=always`.
7559
+ * How long the add-confirm probe waits for the freshly-added server to
7560
+ * connect before replying. This is effectively the whole budget for
7561
+ * `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
7562
+ * BEFORE resolving, so the Connection object already exists and
7563
+ * `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
7564
+ * plugin caller's 30s IPC timeout. A slower server still connects in the
7565
+ * background and surfaces on the next `alfe_mcp_list_tools` — the probe just
7566
+ * reports what it saw within the window; a missed connect is re-attempted by
7567
+ * the bundler's `retryNeverConnected` sweep.
7165
7568
  */
7166
- function restartService() {
7167
- const platform = process.platform;
7168
- if (platform === "darwin") {
7169
- const uid = getLaunchdUid();
7170
- ensureLaunchdLoaded(uid);
7171
- execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
7172
- logger$1.info("Restarted launchd service");
7173
- return;
7174
- }
7175
- if (platform === "linux") {
7176
- execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} restart ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7177
- logger$1.info("Restarted systemd service");
7178
- return;
7179
- }
7180
- throw new Error(`Unsupported platform: ${platform}`);
7181
- }
7569
+ const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
7182
7570
  /**
7183
- * Stop the installed service via systemctl/launchctl. On macOS this boots the
7184
- * service OUT (unloads it) so launchd's `KeepAlive` does NOT respawn it;
7185
- * `startService()` bootstraps it again. On Linux the unit stays enabled (starts
7186
- * on next boot); `systemctl stop` just halts the current run.
7571
+ * Register a new MCP server in the alfe bundler store on behalf of the agent,
7572
+ * then CONFIRM the connect before replying so the agent learns whether the
7573
+ * server actually works. Owned as `'manual'` so the agent can later remove it
7574
+ * without an expectedOwner conflict — matches what `alfe mcp add` does from the
7575
+ * CLI. Registration is durable regardless of the probe outcome: a probe
7576
+ * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
7577
+ * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
7187
7578
  */
7188
- function stopService() {
7189
- const platform = process.platform;
7190
- if (platform === "darwin") {
7191
- execSync(`launchctl bootout gui/${getLaunchdUid()}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
7192
- logger$1.info("Stopped launchd service");
7193
- return;
7579
+ async function handleMcpAddServer(params, manager) {
7580
+ if (!manager) return {
7581
+ ok: false,
7582
+ error: {
7583
+ code: "MCP_MANAGER_UNAVAILABLE",
7584
+ message: "MCP manager not initialized"
7585
+ }
7586
+ };
7587
+ const p = params;
7588
+ if (typeof p.id !== "string" || p.id.length === 0) return {
7589
+ ok: false,
7590
+ error: {
7591
+ code: "INVALID_PARAMS",
7592
+ message: "id is required (string)"
7593
+ }
7594
+ };
7595
+ const config = buildServerConfig(p);
7596
+ if (!config) return {
7597
+ ok: false,
7598
+ error: {
7599
+ code: "INVALID_PARAMS",
7600
+ message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
7601
+ }
7602
+ };
7603
+ try {
7604
+ await manager.addServer(config, {
7605
+ id: p.id,
7606
+ owner: "manual"
7607
+ });
7608
+ } catch (err) {
7609
+ const message = err instanceof Error ? err.message : String(err);
7610
+ logger$1.warn({
7611
+ id: p.id,
7612
+ err: message
7613
+ }, "mcp.add_server failed");
7614
+ return {
7615
+ ok: false,
7616
+ error: {
7617
+ code: "MCP_ADD_FAILED",
7618
+ message
7619
+ }
7620
+ };
7194
7621
  }
7195
- if (platform === "linux") {
7196
- execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
7197
- logger$1.info("Stopped systemd service");
7198
- return;
7622
+ let status = null;
7623
+ if (manager.warmServer) try {
7624
+ status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
7625
+ } catch (err) {
7626
+ logger$1.warn({
7627
+ id: p.id,
7628
+ err: err instanceof Error ? err.message : String(err)
7629
+ }, "mcp.add_server warm probe threw");
7199
7630
  }
7200
- throw new Error(`Unsupported platform: ${platform}`);
7631
+ return {
7632
+ ok: true,
7633
+ payload: {
7634
+ id: p.id,
7635
+ connected: status?.connected ?? false,
7636
+ toolCount: status?.toolCount ?? 0,
7637
+ ...status?.lastError !== void 0 ? { error: status.lastError } : {}
7638
+ }
7639
+ };
7201
7640
  }
7202
- /**
7203
- * True when the gateway is installed as a system service (launchd plist /
7204
- * systemd unit present). Lets the CLI restart/stop/start THROUGH the service
7205
- * manager instead of driving the daemon inline via the PID file.
7206
- */
7207
- function isServiceInstalled() {
7208
- const platform = process.platform;
7209
- if (platform === "darwin") return existsSync(getLaunchdPlistPath());
7210
- if (platform === "linux") return existsSync(isRootUser() ? getSystemdSystemServicePath() : getSystemdServicePath());
7211
- return false;
7641
+ function buildServerConfig(p) {
7642
+ if (typeof p.command === "string" && p.command.length > 0) {
7643
+ const cfg = { command: p.command };
7644
+ if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
7645
+ if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
7646
+ const env = {};
7647
+ for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
7648
+ cfg.env = env;
7649
+ }
7650
+ if (typeof p.cwd === "string") cfg.cwd = p.cwd;
7651
+ return cfg;
7652
+ }
7653
+ if (typeof p.url === "string" && p.url.length > 0) {
7654
+ const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
7655
+ const cfg = {
7656
+ url: p.url,
7657
+ transport
7658
+ };
7659
+ if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
7660
+ const headers = {};
7661
+ for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
7662
+ cfg.headers = headers;
7663
+ }
7664
+ return cfg;
7665
+ }
7666
+ return null;
7212
7667
  }
7213
7668
  /**
7214
- * Write the current process PID to the PID file.
7669
+ * Drop a server entry the agent previously registered. Restricted to
7670
+ * `manual`-owned entries so the agent can't accidentally clobber
7671
+ * integration-installed or CLI-installed servers (the daemon owns
7672
+ * those; the agent can ask the user to uninstall an integration via
7673
+ * the dashboard).
7215
7674
  */
7216
- async function writePidFile() {
7217
- await writeFile(PID_PATH, String(process.pid), "utf-8");
7675
+ async function handleMcpRemoveServer(params, manager) {
7676
+ if (!manager) return {
7677
+ ok: false,
7678
+ error: {
7679
+ code: "MCP_MANAGER_UNAVAILABLE",
7680
+ message: "MCP manager not initialized"
7681
+ }
7682
+ };
7683
+ const { id } = params;
7684
+ if (typeof id !== "string" || id.length === 0) return {
7685
+ ok: false,
7686
+ error: {
7687
+ code: "INVALID_PARAMS",
7688
+ message: "id is required (string)"
7689
+ }
7690
+ };
7691
+ try {
7692
+ return {
7693
+ ok: true,
7694
+ payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
7695
+ };
7696
+ } catch (err) {
7697
+ const message = err instanceof Error ? err.message : String(err);
7698
+ if (message.includes("owned by")) return {
7699
+ ok: false,
7700
+ error: {
7701
+ code: "MCP_OWNER_MISMATCH",
7702
+ message
7703
+ }
7704
+ };
7705
+ logger$1.warn({
7706
+ id,
7707
+ err: message
7708
+ }, "mcp.remove_server failed");
7709
+ return {
7710
+ ok: false,
7711
+ error: {
7712
+ code: "MCP_REMOVE_FAILED",
7713
+ message
7714
+ }
7715
+ };
7716
+ }
7218
7717
  }
7219
- /**
7220
- * Remove the PID file.
7221
- */
7222
- async function removePidFile() {
7718
+ async function handleMcpCallTool(bundler, params) {
7719
+ if (!bundler) return {
7720
+ ok: false,
7721
+ error: {
7722
+ code: "MCP_BUNDLER_UNAVAILABLE",
7723
+ message: "MCP bundler not initialized"
7724
+ }
7725
+ };
7726
+ const { name, args } = params;
7727
+ if (typeof name !== "string" || name.length === 0) return {
7728
+ ok: false,
7729
+ error: {
7730
+ code: "INVALID_PARAMS",
7731
+ message: "name is required (string)"
7732
+ }
7733
+ };
7223
7734
  try {
7224
- await unlink(PID_PATH);
7225
- } catch {}
7735
+ return {
7736
+ ok: true,
7737
+ payload: await bundler.callTool(name, args)
7738
+ };
7739
+ } catch (err) {
7740
+ const message = err instanceof Error ? err.message : String(err);
7741
+ logger$1.warn({
7742
+ tool: name,
7743
+ err: message
7744
+ }, "mcp.call_tool failed");
7745
+ return {
7746
+ ok: false,
7747
+ error: {
7748
+ code: "MCP_CALL_FAILED",
7749
+ message
7750
+ }
7751
+ };
7752
+ }
7226
7753
  }
7754
+ //#endregion
7755
+ //#region src/integration-bootstrap.ts
7227
7756
  /**
7228
- * Check if a daemon is already running.
7229
- * Returns the PID if alive, null if not running.
7757
+ * Adapter from `AgentApiClient`'s per-provider methods to the
7758
+ * `CredentialsResolver` shape the MCP applier expects. Returns
7759
+ * `undefined` for unknown providers and on 404/network error so the
7760
+ * applier can skip registration silently (its documented contract).
7230
7761
  */
7231
- async function checkExistingDaemon() {
7232
- if (!existsSync(PID_PATH)) return null;
7233
- try {
7234
- const pidStr = await readFile(PID_PATH, "utf-8");
7235
- const pid = parseInt(pidStr.trim(), 10);
7236
- if (isNaN(pid)) {
7237
- await removePidFile();
7238
- return null;
7762
+ async function fetchProviderCredentials(agentApi, provider, connectionId) {
7763
+ if (connectionId) try {
7764
+ const raw = await agentApi.getConnectionCredentials(connectionId);
7765
+ const fields = { ...raw.providerMetadata ?? {} };
7766
+ if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
7767
+ const bundle = JSON.parse(raw.accessToken);
7768
+ if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
7769
+ } catch (err) {
7770
+ logger$1.warn({
7771
+ connectionId,
7772
+ err: err instanceof Error ? err.message : String(err)
7773
+ }, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
7774
+ fields.accessToken = raw.accessToken;
7239
7775
  }
7240
- try {
7241
- process.kill(pid, 0);
7242
- return pid;
7243
- } catch {
7244
- await removePidFile();
7245
- return null;
7776
+ return fields;
7777
+ } catch (err) {
7778
+ logger$1.warn({
7779
+ connectionId,
7780
+ err: err instanceof Error ? err.message : String(err)
7781
+ }, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
7782
+ return;
7783
+ }
7784
+ const key = provider.toLowerCase();
7785
+ try {
7786
+ switch (key) {
7787
+ case "atlassian": return await agentApi.getAtlassianCredentials();
7788
+ case "github": return await agentApi.getGithubCredentials();
7789
+ case "xero": return await agentApi.getXeroCredentials();
7790
+ case "notion": return await agentApi.getNotionCredentials();
7791
+ case "myob": return await agentApi.getMYOBCredentials();
7792
+ case "google": return await agentApi.getGoogleCredentials();
7793
+ default:
7794
+ logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
7795
+ return;
7246
7796
  }
7247
7797
  } catch {
7248
- return null;
7798
+ return;
7249
7799
  }
7250
7800
  }
7251
7801
  /**
7252
- * Send SIGTERM to an existing daemon process.
7802
+ * Post-reconcile trigger: rebuild the command registry from the manager's
7803
+ * active integrations and report the available commands to the cloud
7804
+ * gateway. Wired by daemon.ts as the cloud client's
7805
+ * `onReconciliationComplete` callback.
7253
7806
  */
7254
- async function stopExistingDaemon() {
7255
- const pid = await checkExistingDaemon();
7256
- if (!pid) return false;
7807
+ function reportAvailableCommands(deps) {
7808
+ const { integrationManager, commandRegistry, cloudClient } = deps;
7257
7809
  try {
7258
- process.kill(pid, "SIGTERM");
7259
- for (let i = 0; i < 50; i++) {
7260
- await new Promise((r) => setTimeout(r, 100));
7261
- try {
7262
- process.kill(pid, 0);
7263
- } catch {
7264
- await removePidFile();
7265
- return true;
7266
- }
7267
- }
7268
- process.kill(pid, "SIGKILL");
7269
- await removePidFile();
7270
- return true;
7271
- } catch {
7272
- await removePidFile();
7273
- return false;
7810
+ const activeCommands = integrationManager.getActiveCommands();
7811
+ commandRegistry.clear();
7812
+ for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
7813
+ const commandsMsg = {
7814
+ type: "COMMANDS_AVAILABLE",
7815
+ commands: commandRegistry.listCommands()
7816
+ };
7817
+ cloudClient.sendMessage(commandsMsg);
7818
+ logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
7819
+ } catch (err) {
7820
+ logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
7274
7821
  }
7275
7822
  }
7276
7823
  //#endregion
@@ -23150,7 +23697,7 @@ var CommandDedupe = class {
23150
23697
  * the gateway when needed and uses `callerScopes: ["operator.admin"]`.
23151
23698
  * 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
23152
23699
  */
23153
- const execFileAsync$2 = promisify(execFile);
23700
+ const execFileAsync$1 = promisify(execFile);
23154
23701
  const APPROVE_TIMEOUT_MS = 1e4;
23155
23702
  function resolveStateDir(override) {
23156
23703
  if (override) return override;
@@ -23249,7 +23796,7 @@ function startPairingApprovalPoller(opts) {
23249
23796
  const intervalMs = opts.intervalMs ?? 3e4;
23250
23797
  const cliLock = opts.cliLock ?? new NoopOpenClawCliLock();
23251
23798
  const rawExec = opts.exec ?? (async (file, args, { timeout }) => {
23252
- const { stdout, stderr } = await execFileAsync$2(file, args, { timeout });
23799
+ const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
23253
23800
  return {
23254
23801
  stdout,
23255
23802
  stderr
@@ -23323,12 +23870,12 @@ function startPairingApprovalPoller(opts) {
23323
23870
  * Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
23324
23871
  * once per agent.
23325
23872
  */
23326
- const execFileAsync$1 = promisify(execFile);
23873
+ const execFileAsync = promisify(execFile);
23327
23874
  const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
23328
23875
  const defaultOpenclaw = {
23329
23876
  async listServers() {
23330
23877
  try {
23331
- const { stdout } = await execFileAsync$1("openclaw", [
23878
+ const { stdout } = await execFileAsync("openclaw", [
23332
23879
  "config",
23333
23880
  "get",
23334
23881
  "mcp.servers"
@@ -23345,7 +23892,7 @@ const defaultOpenclaw = {
23345
23892
  }
23346
23893
  },
23347
23894
  async unsetServer(key) {
23348
- await execFileAsync$1("openclaw", [
23895
+ await execFileAsync("openclaw", [
23349
23896
  "config",
23350
23897
  "unset",
23351
23898
  `mcp.servers.${key}`
@@ -23527,8 +24074,12 @@ function createMcpErrorHooks() {
23527
24074
  * 7. Start agent runtime (managed mode only — e.g. OpenClaw)
23528
24075
  * 8. Write PID file (non-managed only)
23529
24076
  * 9. Handle graceful shutdown
24077
+ *
24078
+ * Split along seams (this file stays the orchestrator):
24079
+ * - daemon-bootstrap.ts — version resolution, persona migration, AI-proxy + IPC start
24080
+ * - mcp-handlers.ts — `mcp.*` IPC request handlers
24081
+ * - integration-bootstrap.ts — provider-credential fetch + post-reconcile command reporting
23530
24082
  */
23531
- const execFileAsync = promisify(execFile);
23532
24083
  let config;
23533
24084
  let cloudClient;
23534
24085
  let ipcServer = null;
@@ -23560,202 +24111,21 @@ let cloudConnected = false;
23560
24111
  * Dedupe for at-least-once durable command delivery (Phase B). The cloud may
23561
24112
  * push a command over the live WS AND re-drain the same commandId on the next
23562
24113
  * SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
23563
- * so a duplicate is ACKed with the prior result without re-executing.
23564
- */
23565
- const commandDedupe = new CommandDedupe();
23566
- let shuttingDown = false;
23567
- let commandRegistry;
23568
- let resolvedCliVersion;
23569
- let resolvedRuntimeVersion;
23570
- let upgradingRuntime = false;
23571
- let stopPairingApprovalPoller = null;
23572
- /**
23573
- * Module-level handle on the runtime appliers built during start(), so the
23574
- * module-scoped command handler can route `alfe.config_set` to the active
23575
- * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
23576
- */
23577
- let runtimeAppliersRef = null;
23578
- /**
23579
- * Resolve the installed @alfe.ai/cli version.
23580
- *
23581
- * Strategy (in order):
23582
- * 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
23583
- * 2. Walk up from this file to find @alfe.ai/cli/package.json
23584
- * (works when systemd runs the gateway binary directly, since
23585
- * the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
23586
- */
23587
- /**
23588
- * Adapter from `AgentApiClient`'s per-provider methods to the
23589
- * `CredentialsResolver` shape the MCP applier expects. Returns
23590
- * `undefined` for unknown providers and on 404/network error so the
23591
- * applier can skip registration silently (its documented contract).
23592
- */
23593
- async function fetchProviderCredentials(agentApi, provider, connectionId) {
23594
- if (connectionId) try {
23595
- const raw = await agentApi.getConnectionCredentials(connectionId);
23596
- const fields = { ...raw.providerMetadata ?? {} };
23597
- if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
23598
- const bundle = JSON.parse(raw.accessToken);
23599
- if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
23600
- } catch (err) {
23601
- logger$1.warn({
23602
- connectionId,
23603
- err: err instanceof Error ? err.message : String(err)
23604
- }, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
23605
- fields.accessToken = raw.accessToken;
23606
- }
23607
- return fields;
23608
- } catch (err) {
23609
- logger$1.warn({
23610
- connectionId,
23611
- err: err instanceof Error ? err.message : String(err)
23612
- }, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
23613
- return;
23614
- }
23615
- const key = provider.toLowerCase();
23616
- try {
23617
- switch (key) {
23618
- case "atlassian": return await agentApi.getAtlassianCredentials();
23619
- case "github": return await agentApi.getGithubCredentials();
23620
- case "xero": return await agentApi.getXeroCredentials();
23621
- case "notion": return await agentApi.getNotionCredentials();
23622
- case "myob": return await agentApi.getMYOBCredentials();
23623
- case "google": return await agentApi.getGoogleCredentials();
23624
- default:
23625
- logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
23626
- return;
23627
- }
23628
- } catch {
23629
- return;
23630
- }
23631
- }
23632
- async function getCliVersion() {
23633
- if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
23634
- try {
23635
- const { fileURLToPath } = await import("node:url");
23636
- const { dirname } = await import("node:path");
23637
- let dir = dirname(fileURLToPath(import.meta.url));
23638
- for (let i = 0; i < 10; i++) {
23639
- const candidate = join(dir, "package.json");
23640
- try {
23641
- const raw = await readFile(candidate, "utf-8");
23642
- const pkg = JSON.parse(raw);
23643
- if (pkg.name === "@alfe.ai/cli") return pkg.version;
23644
- } catch {}
23645
- const parent = dirname(dir);
23646
- if (parent === dir) break;
23647
- dir = parent;
23648
- }
23649
- } catch {}
23650
- logger$1.debug("Could not resolve @alfe.ai/cli version");
23651
- }
23652
- /**
23653
- * Per-runtime "print installed version" commands. Local to the gateway — we do
23654
- * NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
23655
- * `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
23656
- * connection-status report degrades gracefully instead of crashing.
23657
- */
23658
- const RUNTIME_VERSION_COMMANDS = {
23659
- openclaw: {
23660
- command: "openclaw",
23661
- args: ["--version"]
23662
- },
23663
- hermes: {
23664
- command: "hermes",
23665
- args: ["version"]
23666
- },
23667
- "claude-code": {
23668
- command: "alfe-claude-host",
23669
- args: ["--version"]
23670
- }
23671
- };
23672
- /**
23673
- * Pure resolver (exported for tests) — the per-runtime version command, or
23674
- * `undefined` for an unknown runtime.
23675
- */
23676
- function resolveRuntimeVersionCommand(runtime) {
23677
- return RUNTIME_VERSION_COMMANDS[runtime];
23678
- }
23679
- /**
23680
- * Resolve the installed runtime version by running the per-runtime version
23681
- * command and returning the trimmed stdout. An unknown runtime (no command in
23682
- * the map) returns `undefined` without spawning — it must never throw.
23683
- */
23684
- async function getRuntimeVersion(runtime) {
23685
- const cmd = resolveRuntimeVersionCommand(runtime);
23686
- if (!cmd) {
23687
- logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
23688
- return;
23689
- }
23690
- try {
23691
- const { stdout } = await execFileAsync(cmd.command, cmd.args);
23692
- return stdout.trim() || void 0;
23693
- } catch {
23694
- logger$1.debug({ runtime }, "Could not resolve runtime version");
23695
- return;
23696
- }
23697
- }
23698
- /**
23699
- * Flush pino's async transport and exit.
23700
- * process.exit() can drop buffered log lines — this ensures they're written first.
23701
- */
23702
- async function flushAndExit(code) {
23703
- await flushSentry();
23704
- await new Promise((resolve) => {
23705
- logger$1.flush();
23706
- setTimeout(resolve, 500);
23707
- });
23708
- process.exit(code);
23709
- }
24114
+ * so a duplicate is ACKed with the prior result without re-executing.
24115
+ */
24116
+ const commandDedupe = new CommandDedupe();
24117
+ let shuttingDown = false;
24118
+ let commandRegistry;
24119
+ let resolvedCliVersion;
24120
+ let resolvedRuntimeVersion;
24121
+ let upgradingRuntime = false;
24122
+ let stopPairingApprovalPoller = null;
23710
24123
  /**
23711
- * One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
23712
- * fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
23713
- * OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
23714
- * (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
23715
- * workspace already has the file. Idempotent and safe to run on every daemon start.
24124
+ * Module-level handle on the runtime appliers built during start(), so the
24125
+ * module-scoped command handler can route `alfe.config_set` to the active
24126
+ * runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
23716
24127
  */
23717
- const PERSONA_FILES = [
23718
- "SOUL.md",
23719
- "IDENTITY.md",
23720
- "BOOTSTRAP.md",
23721
- "AGENTS.md"
23722
- ];
23723
- async function migrateLegacyPersonaFiles(home, agentWorkspace) {
23724
- if (home === agentWorkspace) return;
23725
- let moved = 0;
23726
- let skipped = 0;
23727
- for (const filename of PERSONA_FILES) {
23728
- const src = join(home, filename);
23729
- const dst = join(agentWorkspace, filename);
23730
- try {
23731
- await stat(src);
23732
- } catch {
23733
- continue;
23734
- }
23735
- try {
23736
- await stat(dst);
23737
- skipped++;
23738
- continue;
23739
- } catch {}
23740
- try {
23741
- await mkdir(agentWorkspace, { recursive: true });
23742
- await rename(src, dst);
23743
- moved++;
23744
- } catch (err) {
23745
- logger$1.warn({
23746
- src,
23747
- dst,
23748
- err: err instanceof Error ? err.message : String(err)
23749
- }, "Persona file migration: rename failed");
23750
- }
23751
- }
23752
- if (moved > 0 || skipped > 0) logger$1.info({
23753
- home,
23754
- agentWorkspace,
23755
- moved,
23756
- skipped
23757
- }, "Persona file backfill migration complete");
23758
- }
24128
+ let runtimeAppliersRef = null;
23759
24129
  async function startDaemon() {
23760
24130
  startedAt = Date.now();
23761
24131
  const managed = isManagedMode();
@@ -23815,54 +24185,12 @@ async function startDaemon() {
23815
24185
  commandQueue = new CommandQueue();
23816
24186
  commandQueue.startGC();
23817
24187
  commandRegistry = new CommandRegistry();
23818
- logger$1.debug("Starting AI proxy...");
23819
- try {
23820
- const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
23821
- const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
23822
- const proxyUrl = getAiServiceUrlFromToken(config.apiKey);
23823
- const port = DEFAULT_AI_PROXY_PORT ?? 18193;
23824
- aiProxyServer = createProxyServer({
23825
- port,
23826
- apiKey: config.apiKey,
23827
- proxyUrl
23828
- });
23829
- const server = aiProxyServer;
23830
- await new Promise((resolve, reject) => {
23831
- server.listen(port, "127.0.0.1", () => {
23832
- aiProxyRunning = true;
23833
- aiProxyUrl = `http://127.0.0.1:${String(port)}`;
23834
- logger$1.info({
23835
- port,
23836
- upstream: proxyUrl
23837
- }, "AI proxy started");
23838
- resolve();
23839
- });
23840
- server.on("error", reject);
23841
- });
23842
- } catch (err) {
23843
- const message = err instanceof Error ? err.message : String(err);
23844
- const stack = err instanceof Error ? err.stack : void 0;
23845
- logger$1.error({
23846
- err: message,
23847
- stack
23848
- }, "Failed to start AI proxy — LLM requests will fail");
23849
- }
23850
- logger$1.debug({ socketPath: config.socketPath }, "Starting IPC server...");
23851
- ipcServer = new IPCServer(config.socketPath);
23852
- ipcServer.setRequestHandler(handlePluginRequest);
24188
+ const aiProxy = await startAiProxy(config.apiKey);
24189
+ aiProxyServer = aiProxy.server;
24190
+ aiProxyUrl = aiProxy.url;
24191
+ aiProxyRunning = aiProxy.running;
24192
+ ipcServer = await startIpcServer(config.socketPath, handlePluginRequest);
23853
24193
  turnActivityProbe = new IpcTurnActivityProbe(() => ipcServer);
23854
- try {
23855
- await ipcServer.start();
23856
- logger$1.debug("IPC server started");
23857
- } catch (err) {
23858
- const message = err instanceof Error ? err.message : String(err);
23859
- const stack = err instanceof Error ? err.stack : void 0;
23860
- logger$1.error({
23861
- err: message,
23862
- stack
23863
- }, "Failed to start IPC server");
23864
- await flushAndExit(1);
23865
- }
23866
24194
  resolvedCliVersion = await getCliVersion();
23867
24195
  resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
23868
24196
  logger$1.info({
@@ -23928,6 +24256,7 @@ async function startDaemon() {
23928
24256
  mcpBundler = new McpBundler({
23929
24257
  logger: logger$1,
23930
24258
  idleTtlMs: 0,
24259
+ retryNeverConnected: true,
23931
24260
  ...mcpErrorHooks
23932
24261
  }, { connect: defaultConnect });
23933
24262
  if (config.runtime === "hermes" || config.runtime === "claude-code") logger$1.info({ runtime: config.runtime }, "MCP store mirrored into runtime config by the runtime MCP sync — daemon MCP bundler left idle");
@@ -24005,19 +24334,11 @@ async function startDaemon() {
24005
24334
  }
24006
24335
  }
24007
24336
  cloudClient.setOnReconciliationComplete(() => {
24008
- try {
24009
- const activeCommands = integrationManager.getActiveCommands();
24010
- commandRegistry.clear();
24011
- for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
24012
- const commandsMsg = {
24013
- type: "COMMANDS_AVAILABLE",
24014
- commands: commandRegistry.listCommands()
24015
- };
24016
- cloudClient.sendMessage(commandsMsg);
24017
- logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
24018
- } catch (err) {
24019
- logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
24020
- }
24337
+ reportAvailableCommands({
24338
+ integrationManager,
24339
+ commandRegistry,
24340
+ cloudClient
24341
+ });
24021
24342
  });
24022
24343
  cloudClient.start();
24023
24344
  logger$1.debug("Cloud client started");
@@ -24302,519 +24623,251 @@ async function executeCloudCommand(command) {
24302
24623
  };
24303
24624
  return {
24304
24625
  type: "COMMAND_ACK",
24305
- commandId: command.commandId,
24306
- status: "ok",
24307
- result: { integrations: [info] }
24308
- };
24309
- }
24310
- const integrations = integrationManager.list();
24311
- return {
24312
- type: "COMMAND_ACK",
24313
- commandId: command.commandId,
24314
- status: "ok",
24315
- result: { integrations }
24316
- };
24317
- } catch (err) {
24318
- const message = err instanceof Error ? err.message : String(err);
24319
- logger$1.error({ err: message }, "Integration status failed");
24320
- return {
24321
- type: "COMMAND_ACK",
24322
- commandId: command.commandId,
24323
- status: "error",
24324
- result: {
24325
- code: "STATUS_FAILED",
24326
- message
24327
- }
24328
- };
24329
- }
24330
- }
24331
- if (command.command === "alfe.config_set") {
24332
- const payload = command.payload;
24333
- const key = payload?.key;
24334
- const value = payload?.value;
24335
- if (!key || value === void 0) return {
24336
- type: "COMMAND_ACK",
24337
- commandId: command.commandId,
24338
- status: "error",
24339
- result: {
24340
- code: "INVALID_PAYLOAD",
24341
- message: "alfe.config_set requires key and value"
24342
- }
24343
- };
24344
- const runtime = config.runtime;
24345
- const applier = runtimeAppliersRef?.get(runtime);
24346
- if (!applier || typeof applier.setConfigRaw !== "function") {
24347
- const message = `No runtime applier supports config_set for runtime "${runtime}"`;
24348
- logger$1.warn({
24349
- runtime,
24350
- key
24351
- }, message);
24352
- return {
24353
- type: "COMMAND_ACK",
24354
- commandId: command.commandId,
24355
- status: "error",
24356
- result: {
24357
- code: "CONFIG_SET_UNSUPPORTED",
24358
- message
24359
- }
24360
- };
24361
- }
24362
- try {
24363
- await applier.setConfigRaw(key, value);
24364
- logger$1.info({
24365
- runtime,
24366
- key
24367
- }, "Applied config via runtime applier setConfigRaw");
24368
- return {
24369
- type: "COMMAND_ACK",
24370
- commandId: command.commandId,
24371
- status: "ok",
24372
- result: {
24373
- key,
24374
- value
24375
- }
24376
- };
24377
- } catch (err) {
24378
- const message = err instanceof Error ? err.message : String(err);
24379
- logger$1.error({
24380
- err: message,
24381
- runtime,
24382
- key
24383
- }, "Failed to apply config via runtime applier");
24384
- return {
24385
- type: "COMMAND_ACK",
24386
- commandId: command.commandId,
24387
- status: "error",
24388
- result: {
24389
- code: "CONFIG_SET_FAILED",
24390
- message
24391
- }
24392
- };
24393
- }
24394
- }
24395
- if (commandRegistry.has(command.command)) try {
24396
- const ctx = buildCommandContext();
24397
- const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
24398
- return {
24399
- type: "COMMAND_ACK",
24400
- commandId: command.commandId,
24401
- status: result.status,
24402
- result: result.result
24403
- };
24404
- } catch (err) {
24405
- const message = err instanceof Error ? err.message : String(err);
24406
- logger$1.error({
24407
- err: message,
24408
- command: command.command
24409
- }, "Command registry execution failed");
24410
- return {
24411
- type: "COMMAND_ACK",
24412
- commandId: command.commandId,
24413
- status: "error",
24414
- result: {
24415
- code: "REGISTRY_ERROR",
24416
- message
24417
- }
24418
- };
24419
- }
24420
- const ipcRequest = cloudCommandToIPCRequest(command);
24421
- if (!ipcRequest) {
24422
- logger$1.warn({ command: command.command }, "Unrecognized cloud command");
24423
- return {
24424
- type: "COMMAND_ACK",
24425
- commandId: command.commandId,
24426
- status: "error",
24427
- result: {
24428
- code: "UNKNOWN_COMMAND",
24429
- message: `Unrecognized command: ${command.command}`
24430
- }
24431
- };
24432
- }
24433
- const plugins = ipcServer?.getRegisteredPlugins() ?? [];
24434
- if (plugins.length === 0) {
24435
- logger$1.info({
24436
- commandId: command.commandId,
24437
- command: command.command
24438
- }, "No plugins connected — queuing command");
24439
- commandQueue.enqueue("_default", ipcRequest, command.commandId);
24440
- return {
24441
- type: "COMMAND_ACK",
24442
- commandId: command.commandId,
24443
- status: "ok",
24444
- result: {
24445
- queued: true,
24446
- message: "Command queued — no plugins connected"
24447
- }
24448
- };
24449
- }
24450
- const [pluginId] = plugins[0];
24451
- if (!ipcServer) return {
24452
- type: "COMMAND_ACK",
24453
- commandId: command.commandId,
24454
- status: "error",
24455
- result: {
24456
- code: "NO_IPC",
24457
- message: "IPC server not available"
24626
+ commandId: command.commandId,
24627
+ status: "ok",
24628
+ result: { integrations: [info] }
24629
+ };
24630
+ }
24631
+ const integrations = integrationManager.list();
24632
+ return {
24633
+ type: "COMMAND_ACK",
24634
+ commandId: command.commandId,
24635
+ status: "ok",
24636
+ result: { integrations }
24637
+ };
24638
+ } catch (err) {
24639
+ const message = err instanceof Error ? err.message : String(err);
24640
+ logger$1.error({ err: message }, "Integration status failed");
24641
+ return {
24642
+ type: "COMMAND_ACK",
24643
+ commandId: command.commandId,
24644
+ status: "error",
24645
+ result: {
24646
+ code: "STATUS_FAILED",
24647
+ message
24648
+ }
24649
+ };
24458
24650
  }
24459
- };
24460
- try {
24461
- const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
24462
- return ipcResponseToCloudAck(command.commandId, response);
24463
- } catch (err) {
24464
- const message = err instanceof Error ? err.message : String(err);
24465
- return {
24651
+ }
24652
+ if (command.command === "alfe.config_set") {
24653
+ const payload = command.payload;
24654
+ const key = payload?.key;
24655
+ const value = payload?.value;
24656
+ if (!key || value === void 0) return {
24466
24657
  type: "COMMAND_ACK",
24467
24658
  commandId: command.commandId,
24468
24659
  status: "error",
24469
24660
  result: {
24470
- code: "PLUGIN_ERROR",
24471
- message
24661
+ code: "INVALID_PAYLOAD",
24662
+ message: "alfe.config_set requires key and value"
24472
24663
  }
24473
24664
  };
24474
- }
24475
- }
24476
- function buildCommandContext() {
24477
- const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
24478
- return {
24479
- workspacePath,
24480
- aiProxyUrl: aiProxyUrl ?? void 0,
24481
- aiProxyRunning,
24482
- apiKey: config.apiKey,
24483
- async exec(cmd, opts) {
24484
- const { exec: execCb } = await import("child_process");
24485
- const { promisify } = await import("util");
24486
- const { stdout, stderr } = await promisify(execCb)(cmd, {
24487
- cwd: workspacePath,
24488
- timeout: opts?.timeoutMs ?? 25e3,
24489
- maxBuffer: opts?.maxBuffer ?? 512 * 1024
24490
- });
24665
+ const runtime = config.runtime;
24666
+ const applier = runtimeAppliersRef?.get(runtime);
24667
+ if (!applier || typeof applier.setConfigRaw !== "function") {
24668
+ const message = `No runtime applier supports config_set for runtime "${runtime}"`;
24669
+ logger$1.warn({
24670
+ runtime,
24671
+ key
24672
+ }, message);
24491
24673
  return {
24492
- stdout: stdout.trim(),
24493
- stderr: stderr.trim()
24674
+ type: "COMMAND_ACK",
24675
+ commandId: command.commandId,
24676
+ status: "error",
24677
+ result: {
24678
+ code: "CONFIG_SET_UNSUPPORTED",
24679
+ message
24680
+ }
24494
24681
  };
24495
24682
  }
24496
- };
24497
- }
24498
- function handlePluginRequest(method, params, pluginId) {
24499
- switch (method) {
24500
- case "status": return Promise.resolve(handleStatus());
24501
- case "integration.list": return Promise.resolve(handleIntegrationList());
24502
- case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
24503
- case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
24504
- case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
24505
- case "mcp.list_servers": return Promise.resolve(handleMcpListServers());
24506
- case "mcp.add_server": return handleMcpAddServer(params);
24507
- case "mcp.remove_server": return handleMcpRemoveServer(params);
24508
- default: return Promise.resolve({
24509
- ok: false,
24510
- error: {
24511
- code: "UNKNOWN_METHOD",
24512
- message: `Unknown method: ${method}`
24513
- }
24514
- });
24515
- }
24516
- }
24517
- function handleStatus() {
24518
- return {
24519
- ok: true,
24520
- payload: {
24521
- daemon: {
24522
- status: "running",
24523
- pid: process.pid,
24524
- uptime: (Date.now() - startedAt) / 1e3,
24525
- version: "0.1.0",
24526
- runtimeVersion: resolvedRuntimeVersion
24527
- },
24528
- cloud: {
24529
- status: cloudConnected ? "connected" : "disconnected",
24530
- latencyMs: cloudClient.getLatencyMs()
24531
- },
24532
- aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
24533
- plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
24534
- name: info.name,
24535
- version: info.version,
24536
- capabilities: info.capabilities,
24537
- connectedAt: info.connectedAt,
24538
- lastSeen: info.lastSeen
24539
- })),
24540
- commandQueue: { totalPending: commandQueue.totalPending() }
24541
- }
24542
- };
24543
- }
24544
- function handleIntegrationList() {
24545
- return {
24546
- ok: true,
24547
- payload: { integrations: integrationManager.list() }
24548
- };
24549
- }
24550
- /**
24551
- * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
24552
- * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
24553
- * cheap (in-memory snapshot, no I/O).
24554
- */
24555
- function handleMcpListTools(bundler) {
24556
- if (!bundler) return {
24557
- ok: false,
24558
- error: {
24559
- code: "MCP_BUNDLER_UNAVAILABLE",
24560
- message: "MCP bundler not initialized"
24561
- }
24562
- };
24563
- return {
24564
- ok: true,
24565
- payload: { tools: bundler.listTools() }
24566
- };
24567
- }
24568
- /**
24569
- * Observability backstop for the INTEGRATION warm path. `applyForIntegration`
24570
- * registers MCP servers into the store and the daemon warms them silently via
24571
- * the store-change `onChange` hook — so a server that fails its connect (e.g.
24572
- * an MCP server whose backing account/credential wasn't resolvable at warm
24573
- * time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
24574
- * making the black hole undiagnosable. After a warm, read `bundler.statuses()`
24575
- * and warn once per still-failed server so the failure is visible. These
24576
- * self-heal on the bundler's background retry sweep once the dependency
24577
- * appears. Returns the servers it warned about (for tests / callers).
24578
- */
24579
- function warnFailedMcpServers(bundler, log, reason) {
24580
- if (!bundler) return [];
24581
- const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
24582
- for (const status of failed) log.warn({
24583
- server: status.name,
24584
- reason,
24585
- consecutiveFailures: status.consecutiveFailures,
24586
- lastError: status.lastError
24587
- }, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
24588
- return failed;
24589
- }
24590
- /**
24591
- * Route a tool call to the appropriate MCP child via the daemon-hosted
24592
- * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
24593
- * the raw JSON object the LLM produced.
24594
- */
24595
- /**
24596
- * List every server entry in the alfe bundler store. Lets agents inspect
24597
- * what they've already registered before adding a new one.
24598
- */
24599
- function handleMcpListServers(manager = mcpManagerRef) {
24600
- if (!manager) return {
24601
- ok: false,
24602
- error: {
24603
- code: "MCP_MANAGER_UNAVAILABLE",
24604
- message: "MCP manager not initialized"
24605
- }
24606
- };
24607
- const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
24608
- const byName = new Map(statuses.map((s) => [s.name, s]));
24609
- return {
24610
- ok: true,
24611
- payload: { servers: manager.listServers().map(({ id, entry }) => ({
24612
- id,
24613
- entry,
24614
- status: byName.get(id) ?? {
24615
- name: id,
24616
- connected: false,
24617
- toolCount: 0,
24618
- consecutiveFailures: 0
24619
- }
24620
- })) }
24621
- };
24622
- }
24623
- /**
24624
- * How long the add-confirm probe waits for the freshly-added server to
24625
- * connect before replying. This is effectively the whole budget for
24626
- * `bundler.warmServer`: the `Manager.warmServer` pre-reconcile is cheap here
24627
- * because `addServer` already queued the Connection object into the store +
24628
- * bundler before we probe, so `reconcileBundler()` finds no diff and returns
24629
- * fast. Kept well under the plugin caller's 30s IPC timeout. A slower server
24630
- * still connects in the background and surfaces on the next
24631
- * `alfe_mcp_list_tools` — the probe just reports what it saw within the window.
24632
- */
24633
- const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
24634
- /**
24635
- * Register a new MCP server in the alfe bundler store on behalf of the agent,
24636
- * then CONFIRM the connect before replying so the agent learns whether the
24637
- * server actually works. Owned as `'manual'` so the agent can later remove it
24638
- * without an expectedOwner conflict — matches what `alfe mcp add` does from the
24639
- * CLI. Registration is durable regardless of the probe outcome: a probe
24640
- * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
24641
- * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
24642
- */
24643
- async function handleMcpAddServer(params, manager = mcpManagerRef) {
24644
- if (!manager) return {
24645
- ok: false,
24646
- error: {
24647
- code: "MCP_MANAGER_UNAVAILABLE",
24648
- message: "MCP manager not initialized"
24649
- }
24650
- };
24651
- const p = params;
24652
- if (typeof p.id !== "string" || p.id.length === 0) return {
24653
- ok: false,
24654
- error: {
24655
- code: "INVALID_PARAMS",
24656
- message: "id is required (string)"
24657
- }
24658
- };
24659
- const config = buildServerConfig(p);
24660
- if (!config) return {
24661
- ok: false,
24662
- error: {
24663
- code: "INVALID_PARAMS",
24664
- message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
24683
+ try {
24684
+ await applier.setConfigRaw(key, value);
24685
+ logger$1.info({
24686
+ runtime,
24687
+ key
24688
+ }, "Applied config via runtime applier setConfigRaw");
24689
+ return {
24690
+ type: "COMMAND_ACK",
24691
+ commandId: command.commandId,
24692
+ status: "ok",
24693
+ result: {
24694
+ key,
24695
+ value
24696
+ }
24697
+ };
24698
+ } catch (err) {
24699
+ const message = err instanceof Error ? err.message : String(err);
24700
+ logger$1.error({
24701
+ err: message,
24702
+ runtime,
24703
+ key
24704
+ }, "Failed to apply config via runtime applier");
24705
+ return {
24706
+ type: "COMMAND_ACK",
24707
+ commandId: command.commandId,
24708
+ status: "error",
24709
+ result: {
24710
+ code: "CONFIG_SET_FAILED",
24711
+ message
24712
+ }
24713
+ };
24665
24714
  }
24666
- };
24667
- try {
24668
- await manager.addServer(config, {
24669
- id: p.id,
24670
- owner: "manual"
24671
- });
24715
+ }
24716
+ if (commandRegistry.has(command.command)) try {
24717
+ const ctx = buildCommandContext();
24718
+ const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
24719
+ return {
24720
+ type: "COMMAND_ACK",
24721
+ commandId: command.commandId,
24722
+ status: result.status,
24723
+ result: result.result
24724
+ };
24672
24725
  } catch (err) {
24673
24726
  const message = err instanceof Error ? err.message : String(err);
24674
- logger$1.warn({
24675
- id: p.id,
24676
- err: message
24677
- }, "mcp.add_server failed");
24727
+ logger$1.error({
24728
+ err: message,
24729
+ command: command.command
24730
+ }, "Command registry execution failed");
24678
24731
  return {
24679
- ok: false,
24680
- error: {
24681
- code: "MCP_ADD_FAILED",
24732
+ type: "COMMAND_ACK",
24733
+ commandId: command.commandId,
24734
+ status: "error",
24735
+ result: {
24736
+ code: "REGISTRY_ERROR",
24682
24737
  message
24683
24738
  }
24684
24739
  };
24685
24740
  }
24686
- let status = null;
24687
- if (manager.warmServer) try {
24688
- status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
24689
- } catch (err) {
24690
- logger$1.warn({
24691
- id: p.id,
24692
- err: err instanceof Error ? err.message : String(err)
24693
- }, "mcp.add_server warm probe threw");
24694
- }
24695
- return {
24696
- ok: true,
24697
- payload: {
24698
- id: p.id,
24699
- connected: status?.connected ?? false,
24700
- toolCount: status?.toolCount ?? 0,
24701
- ...status?.lastError !== void 0 ? { error: status.lastError } : {}
24702
- }
24703
- };
24704
- }
24705
- function buildServerConfig(p) {
24706
- if (typeof p.command === "string" && p.command.length > 0) {
24707
- const cfg = { command: p.command };
24708
- if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
24709
- if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
24710
- const env = {};
24711
- for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
24712
- cfg.env = env;
24713
- }
24714
- if (typeof p.cwd === "string") cfg.cwd = p.cwd;
24715
- return cfg;
24741
+ const ipcRequest = cloudCommandToIPCRequest(command);
24742
+ if (!ipcRequest) {
24743
+ logger$1.warn({ command: command.command }, "Unrecognized cloud command");
24744
+ return {
24745
+ type: "COMMAND_ACK",
24746
+ commandId: command.commandId,
24747
+ status: "error",
24748
+ result: {
24749
+ code: "UNKNOWN_COMMAND",
24750
+ message: `Unrecognized command: ${command.command}`
24751
+ }
24752
+ };
24716
24753
  }
24717
- if (typeof p.url === "string" && p.url.length > 0) {
24718
- const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
24719
- const cfg = {
24720
- url: p.url,
24721
- transport
24754
+ const plugins = ipcServer?.getRegisteredPlugins() ?? [];
24755
+ if (plugins.length === 0) {
24756
+ logger$1.info({
24757
+ commandId: command.commandId,
24758
+ command: command.command
24759
+ }, "No plugins connected — queuing command");
24760
+ commandQueue.enqueue("_default", ipcRequest, command.commandId);
24761
+ return {
24762
+ type: "COMMAND_ACK",
24763
+ commandId: command.commandId,
24764
+ status: "ok",
24765
+ result: {
24766
+ queued: true,
24767
+ message: "Command queued — no plugins connected"
24768
+ }
24722
24769
  };
24723
- if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
24724
- const headers = {};
24725
- for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
24726
- cfg.headers = headers;
24727
- }
24728
- return cfg;
24729
24770
  }
24730
- return null;
24731
- }
24732
- /**
24733
- * Drop a server entry the agent previously registered. Restricted to
24734
- * `manual`-owned entries so the agent can't accidentally clobber
24735
- * integration-installed or CLI-installed servers (the daemon owns
24736
- * those; the agent can ask the user to uninstall an integration via
24737
- * the dashboard).
24738
- */
24739
- async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
24740
- if (!manager) return {
24741
- ok: false,
24742
- error: {
24743
- code: "MCP_MANAGER_UNAVAILABLE",
24744
- message: "MCP manager not initialized"
24745
- }
24746
- };
24747
- const { id } = params;
24748
- if (typeof id !== "string" || id.length === 0) return {
24749
- ok: false,
24750
- error: {
24751
- code: "INVALID_PARAMS",
24752
- message: "id is required (string)"
24771
+ const [pluginId] = plugins[0];
24772
+ if (!ipcServer) return {
24773
+ type: "COMMAND_ACK",
24774
+ commandId: command.commandId,
24775
+ status: "error",
24776
+ result: {
24777
+ code: "NO_IPC",
24778
+ message: "IPC server not available"
24753
24779
  }
24754
24780
  };
24755
24781
  try {
24756
- return {
24757
- ok: true,
24758
- payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
24759
- };
24782
+ const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
24783
+ return ipcResponseToCloudAck(command.commandId, response);
24760
24784
  } catch (err) {
24761
24785
  const message = err instanceof Error ? err.message : String(err);
24762
- if (message.includes("owned by")) return {
24763
- ok: false,
24764
- error: {
24765
- code: "MCP_OWNER_MISMATCH",
24766
- message
24767
- }
24768
- };
24769
- logger$1.warn({
24770
- id,
24771
- err: message
24772
- }, "mcp.remove_server failed");
24773
24786
  return {
24774
- ok: false,
24775
- error: {
24776
- code: "MCP_REMOVE_FAILED",
24787
+ type: "COMMAND_ACK",
24788
+ commandId: command.commandId,
24789
+ status: "error",
24790
+ result: {
24791
+ code: "PLUGIN_ERROR",
24777
24792
  message
24778
24793
  }
24779
24794
  };
24780
24795
  }
24781
24796
  }
24782
- async function handleMcpCallTool(bundler, params) {
24783
- if (!bundler) return {
24784
- ok: false,
24785
- error: {
24786
- code: "MCP_BUNDLER_UNAVAILABLE",
24787
- message: "MCP bundler not initialized"
24788
- }
24789
- };
24790
- const { name, args } = params;
24791
- if (typeof name !== "string" || name.length === 0) return {
24792
- ok: false,
24793
- error: {
24794
- code: "INVALID_PARAMS",
24795
- message: "name is required (string)"
24797
+ function buildCommandContext() {
24798
+ const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
24799
+ return {
24800
+ workspacePath,
24801
+ aiProxyUrl: aiProxyUrl ?? void 0,
24802
+ aiProxyRunning,
24803
+ apiKey: config.apiKey,
24804
+ async exec(cmd, opts) {
24805
+ const { exec: execCb } = await import("child_process");
24806
+ const { promisify } = await import("util");
24807
+ const { stdout, stderr } = await promisify(execCb)(cmd, {
24808
+ cwd: workspacePath,
24809
+ timeout: opts?.timeoutMs ?? 25e3,
24810
+ maxBuffer: opts?.maxBuffer ?? 512 * 1024
24811
+ });
24812
+ return {
24813
+ stdout: stdout.trim(),
24814
+ stderr: stderr.trim()
24815
+ };
24796
24816
  }
24797
24817
  };
24798
- try {
24799
- return {
24800
- ok: true,
24801
- payload: await bundler.callTool(name, args)
24802
- };
24803
- } catch (err) {
24804
- const message = err instanceof Error ? err.message : String(err);
24805
- logger$1.warn({
24806
- tool: name,
24807
- err: message
24808
- }, "mcp.call_tool failed");
24809
- return {
24818
+ }
24819
+ function handlePluginRequest(method, params, pluginId) {
24820
+ switch (method) {
24821
+ case "status": return Promise.resolve(handleStatus());
24822
+ case "integration.list": return Promise.resolve(handleIntegrationList());
24823
+ case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
24824
+ case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
24825
+ case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
24826
+ case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef));
24827
+ case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef);
24828
+ case "mcp.remove_server": return handleMcpRemoveServer(params, mcpManagerRef);
24829
+ default: return Promise.resolve({
24810
24830
  ok: false,
24811
24831
  error: {
24812
- code: "MCP_CALL_FAILED",
24813
- message
24832
+ code: "UNKNOWN_METHOD",
24833
+ message: `Unknown method: ${method}`
24814
24834
  }
24815
- };
24835
+ });
24816
24836
  }
24817
24837
  }
24838
+ function handleStatus() {
24839
+ return {
24840
+ ok: true,
24841
+ payload: {
24842
+ daemon: {
24843
+ status: "running",
24844
+ pid: process.pid,
24845
+ uptime: (Date.now() - startedAt) / 1e3,
24846
+ version: "0.1.0",
24847
+ runtimeVersion: resolvedRuntimeVersion
24848
+ },
24849
+ cloud: {
24850
+ status: cloudConnected ? "connected" : "disconnected",
24851
+ latencyMs: cloudClient.getLatencyMs()
24852
+ },
24853
+ aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
24854
+ plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
24855
+ name: info.name,
24856
+ version: info.version,
24857
+ capabilities: info.capabilities,
24858
+ connectedAt: info.connectedAt,
24859
+ lastSeen: info.lastSeen
24860
+ })),
24861
+ commandQueue: { totalPending: commandQueue.totalPending() }
24862
+ }
24863
+ };
24864
+ }
24865
+ function handleIntegrationList() {
24866
+ return {
24867
+ ok: true,
24868
+ payload: { integrations: integrationManager.list() }
24869
+ };
24870
+ }
24818
24871
  function handleIntegrationReport(params, pluginId) {
24819
24872
  const { name, status, detail } = params;
24820
24873
  if (!name || !status) return {