@alfe.ai/gateway 0.0.12 → 0.0.14

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
@@ -3152,7 +3152,16 @@ enumValues({
3152
3152
  object({ gracePeriodDays: number().int().positive().optional() });
3153
3153
  enumValues({
3154
3154
  Active: "active",
3155
- Inactive: "inactive",
3155
+ Pending: "pending",
3156
+ Provisioning: "provisioning",
3157
+ Starting: "starting",
3158
+ Running: "running",
3159
+ Stopping: "stopping",
3160
+ Stopped: "stopped",
3161
+ Rebooting: "rebooting",
3162
+ Failing: "failing",
3163
+ Failed: "failed",
3164
+ BillingSuspended: "billing_suspended",
3156
3165
  Deleted: "deleted"
3157
3166
  });
3158
3167
  enumValues({
@@ -3163,16 +3172,6 @@ enumValues({
3163
3172
  OpenClaw: "openclaw",
3164
3173
  NanoClaw: "nanoclaw"
3165
3174
  });
3166
- enumValues({
3167
- None: "none",
3168
- Pending: "pending",
3169
- Provisioning: "provisioning",
3170
- Running: "running",
3171
- Stopped: "stopped",
3172
- Failing: "failing",
3173
- Failed: "failed",
3174
- BillingSuspended: "billing_suspended"
3175
- });
3176
3175
  enumValues({
3177
3176
  Active: "active",
3178
3177
  Removed: "removed"
@@ -3873,6 +3872,7 @@ var CloudClient = class {
3873
3872
  onCommand = null;
3874
3873
  onConnectionChange = null;
3875
3874
  onServiceRelay = null;
3875
+ onPluginsChanged = null;
3876
3876
  reconciliationEngine = null;
3877
3877
  constructor(config) {
3878
3878
  this.config = config;
@@ -3910,6 +3910,12 @@ var CloudClient = class {
3910
3910
  * Set the integration manager for reconciliation.
3911
3911
  * When set, the cloud client will handle DESIRED_STATE messages automatically.
3912
3912
  */
3913
+ /**
3914
+ * Set a callback for when plugins change (install/uninstall) — used to restart the runtime.
3915
+ */
3916
+ setPluginsChangedHandler(handler) {
3917
+ this.onPluginsChanged = handler;
3918
+ }
3913
3919
  setIntegrationManager(manager) {
3914
3920
  this.reconciliationEngine = new ReconciliationEngine(manager);
3915
3921
  }
@@ -4106,6 +4112,7 @@ var CloudClient = class {
4106
4112
  }, "Cloud: reconciliation complete");
4107
4113
  const reportMsg = createReconciliationReport(report.results);
4108
4114
  this.send(reportMsg);
4115
+ if ((report.installed.length > 0 || report.uninstalled.length > 0) && this.onPluginsChanged) this.onPluginsChanged();
4109
4116
  } catch (err) {
4110
4117
  const message = err instanceof Error ? err.message : String(err);
4111
4118
  logger$1.error({ err: message }, "Cloud: reconciliation failed");
@@ -4594,22 +4601,13 @@ function getSystemdServicePath() {
4594
4601
  return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
4595
4602
  }
4596
4603
  /**
4597
- * Resolve the path to the gateway binary.
4604
+ * Resolve the alfe CLI path. Globally installed via npm, always in PATH.
4598
4605
  */
4599
- function getGatewayBinPath() {
4600
- const globalBin = process.env.ALFE_GATEWAY_BIN;
4601
- if (globalBin) return globalBin;
4602
- for (const rel of ["../bin/gateway.js", "bin/gateway.js"]) {
4603
- const candidate = join(import.meta.dirname, rel);
4604
- if (existsSync(candidate)) return candidate;
4605
- }
4606
- return "alfe-gateway";
4607
- }
4608
- function getNodePath() {
4606
+ function getAlfeBinPath() {
4609
4607
  try {
4610
- return execSync("which node", { encoding: "utf-8" }).trim();
4608
+ return execSync("which alfe", { encoding: "utf-8" }).trim();
4611
4609
  } catch {
4612
- return "/usr/local/bin/node";
4610
+ return "alfe";
4613
4611
  }
4614
4612
  }
4615
4613
  /**
@@ -4624,8 +4622,8 @@ function generateLaunchdPlist() {
4624
4622
  <string>${LAUNCHD_LABEL}</string>
4625
4623
  <key>ProgramArguments</key>
4626
4624
  <array>
4627
- <string>${getNodePath()}</string>
4628
- <string>${getGatewayBinPath()}</string>
4625
+ <string>${getAlfeBinPath()}</string>
4626
+ <string>gateway</string>
4629
4627
  <string>daemon</string>
4630
4628
  </array>
4631
4629
  <key>RunAtLoad</key>
@@ -4653,8 +4651,7 @@ function generateLaunchdPlist() {
4653
4651
  * Root users get a system-level unit; non-root get a user-level unit.
4654
4652
  */
4655
4653
  function generateSystemdUnit() {
4656
- const nodePath = getNodePath();
4657
- const gatewayBin = getGatewayBinPath();
4654
+ const alfeBin = getAlfeBinPath();
4658
4655
  const root = isRootUser();
4659
4656
  const envLines = [
4660
4657
  "ALFE_MANAGED",
@@ -4668,7 +4665,7 @@ Wants=network-online.target
4668
4665
 
4669
4666
  [Service]
4670
4667
  Type=simple
4671
- ExecStart=${nodePath} ${gatewayBin} daemon
4668
+ ExecStart=${alfeBin} gateway daemon
4672
4669
  Restart=always
4673
4670
  RestartSec=10
4674
4671
  Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
@@ -19902,6 +19899,16 @@ var RuntimeProcess = class {
19902
19899
  });
19903
19900
  }
19904
19901
  /**
19902
+ * Restart the runtime process (stop then start with fresh backoff).
19903
+ */
19904
+ async restart() {
19905
+ log$1.info({ runtime: this.options.runtime }, "Restarting runtime...");
19906
+ await this.stop();
19907
+ this.stopped = false;
19908
+ this.backoffMs = BACKOFF_INITIAL_MS;
19909
+ this.start();
19910
+ }
19911
+ /**
19905
19912
  * Whether the runtime process is currently running.
19906
19913
  */
19907
19914
  get isRunning() {
@@ -20187,18 +20194,19 @@ async function startDaemon() {
20187
20194
  const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
20188
20195
  const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
20189
20196
  const proxyUrl = getAiServiceUrlFromToken(config.apiKey);
20197
+ const port = DEFAULT_AI_PROXY_PORT ?? 18193;
20190
20198
  aiProxyServer = createProxyServer({
20191
- port: DEFAULT_AI_PROXY_PORT,
20199
+ port,
20192
20200
  apiKey: config.apiKey,
20193
20201
  proxyUrl
20194
20202
  });
20195
20203
  const server = aiProxyServer;
20196
20204
  await new Promise((resolve, reject) => {
20197
- server.listen(DEFAULT_AI_PROXY_PORT, "127.0.0.1", () => {
20205
+ server.listen(port, "127.0.0.1", () => {
20198
20206
  aiProxyRunning = true;
20199
- aiProxyUrl = `http://127.0.0.1:${String(DEFAULT_AI_PROXY_PORT)}`;
20207
+ aiProxyUrl = `http://127.0.0.1:${String(port)}`;
20200
20208
  logger$1.info({
20201
- port: DEFAULT_AI_PROXY_PORT,
20209
+ port,
20202
20210
  upstream: proxyUrl
20203
20211
  }, "AI proxy started");
20204
20212
  resolve();
@@ -20268,6 +20276,14 @@ async function startDaemon() {
20268
20276
  });
20269
20277
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
20270
20278
  cloudClient.setIntegrationManager(integrationAdapter);
20279
+ cloudClient.setPluginsChangedHandler(() => {
20280
+ if (runtimeProcess) {
20281
+ logger$1.info("Plugins changed — restarting runtime to load new plugins");
20282
+ runtimeProcess.restart().catch((err) => {
20283
+ logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
20284
+ });
20285
+ }
20286
+ });
20271
20287
  cloudClient.start();
20272
20288
  logger$1.debug("Cloud client started");
20273
20289
  if (config.autoStartRuntime && config.runtime) {
@@ -20415,44 +20431,78 @@ async function handleCloudCommand(command) {
20415
20431
  };
20416
20432
  }
20417
20433
  }
20418
- if (command.command === "daemon.update") {
20419
- const version = command.payload?.version ?? "latest";
20420
- if (!isManagedMode()) return {
20434
+ if (command.command === "support.bash") {
20435
+ const payload = command.payload;
20436
+ if (!payload.cmd) return {
20421
20437
  type: "COMMAND_ACK",
20422
20438
  commandId: command.commandId,
20423
20439
  status: "error",
20424
20440
  result: {
20425
- code: "NOT_MANAGED",
20426
- message: "daemon.update is only supported in managed mode"
20441
+ code: "MISSING_CMD",
20442
+ message: "No command provided"
20427
20443
  }
20428
20444
  };
20445
+ const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
20429
20446
  try {
20430
- const { spawnUpgradeScript } = await import("./upgrade.js");
20431
- await spawnUpgradeScript(version);
20432
- logger$1.info({ version }, "Upgrade script spawned daemon will restart shortly");
20447
+ const { exec } = await import("child_process");
20448
+ const { promisify } = await import("util");
20449
+ const { stdout, stderr } = await promisify(exec)(payload.cmd, {
20450
+ cwd: workspacePath,
20451
+ timeout: 25e3,
20452
+ maxBuffer: 512 * 1024
20453
+ });
20433
20454
  return {
20434
20455
  type: "COMMAND_ACK",
20435
20456
  commandId: command.commandId,
20436
20457
  status: "ok",
20437
20458
  result: {
20438
- upgrading: true,
20439
- version
20459
+ stdout: stdout.trim(),
20460
+ stderr: stderr.trim()
20440
20461
  }
20441
20462
  };
20442
20463
  } catch (err) {
20443
- const message = err instanceof Error ? err.message : String(err);
20444
- logger$1.error({ err: message }, "Failed to spawn upgrade script");
20464
+ const execErr = err;
20445
20465
  return {
20446
20466
  type: "COMMAND_ACK",
20447
20467
  commandId: command.commandId,
20448
20468
  status: "error",
20449
20469
  result: {
20450
- code: "UPGRADE_FAILED",
20451
- message
20470
+ code: "EXEC_FAILED",
20471
+ stdout: execErr.stdout?.trim() ?? "",
20472
+ stderr: execErr.stderr?.trim() ?? "",
20473
+ message: execErr.message ?? String(err),
20474
+ exitCode: execErr.code
20452
20475
  }
20453
20476
  };
20454
20477
  }
20455
20478
  }
20479
+ if (command.command === "daemon.update") {
20480
+ const version = command.payload?.version ?? "latest";
20481
+ if (!isManagedMode()) return {
20482
+ type: "COMMAND_ACK",
20483
+ commandId: command.commandId,
20484
+ status: "error",
20485
+ result: {
20486
+ code: "NOT_MANAGED",
20487
+ message: "daemon.update is only supported in managed mode"
20488
+ }
20489
+ };
20490
+ setTimeout(() => {
20491
+ import("./upgrade.js").then(({ upgradeAndExit }) => upgradeAndExit(version)).catch((err) => {
20492
+ logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Upgrade failed");
20493
+ process.exit(1);
20494
+ });
20495
+ }, 500);
20496
+ return {
20497
+ type: "COMMAND_ACK",
20498
+ commandId: command.commandId,
20499
+ status: "ok",
20500
+ result: {
20501
+ upgrading: true,
20502
+ version
20503
+ }
20504
+ };
20505
+ }
20456
20506
  if (command.command === "integration.status") {
20457
20507
  const payload = command.payload;
20458
20508
  try {
package/dist/upgrade.js CHANGED
@@ -1,62 +1,44 @@
1
1
  import { n as logger } from "./logger.js";
2
- import { chmod, writeFile } from "node:fs/promises";
3
- import { spawn } from "node:child_process";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
4
  //#region src/upgrade.ts
5
5
  /**
6
- * CLI upgrade — spawns a detached shell script to upgrade @alfe.ai/cli.
6
+ * CLI upgrade — runs npm install inline then exits.
7
7
  *
8
- * The daemon process IS the service being upgraded, so it cannot manage the
9
- * upgrade itself. Instead we:
10
- * 1. Write a bash script to /tmp
11
- * 2. Spawn it detached (survives parent death)
12
- * 3. The script stops the service, upgrades, re-runs setup, and restarts
8
+ * Systemd has Restart=always, so after the daemon exits the service
9
+ * restarts automatically with the new CLI version. No detached scripts,
10
+ * no race conditions.
13
11
  *
14
- * The daemon ACKs the COMMAND before the script runs, so the cloud gets
15
- * confirmation. After restart the daemon reconnects with the new version.
12
+ * Flow:
13
+ * 1. Daemon ACKs the COMMAND (caller handles this before calling upgrade)
14
+ * 2. npm install -g @alfe.ai/cli@{version} runs as a child process
15
+ * 3. Daemon exits with code 0
16
+ * 4. Systemd restarts the service → new version boots
16
17
  */
18
+ const execFileAsync = promisify(execFile);
17
19
  /**
18
- * Spawn a detached upgrade script that will:
19
- * 1. Wait briefly for the daemon to finish sending its ACK
20
- * 2. Stop the systemd service
21
- * 3. Install the target CLI version globally
22
- * 4. Re-run `alfe setup --managed` to regenerate the systemd unit
23
- * 5. Start the service again
24
- * 6. Clean up the script file
20
+ * Upgrade the CLI to a target version and exit.
21
+ * Systemd will restart the daemon with the new version.
25
22
  */
26
- async function spawnUpgradeScript(version) {
27
- const scriptPath = `/tmp/alfe-upgrade-${String(Date.now())}.sh`;
28
- await writeFile(scriptPath, `#!/bin/bash
29
- set -euo pipefail
30
-
31
- # Wait for daemon to finish sending COMMAND_ACK
32
- sleep 2
33
-
34
- # Stop the gateway service
35
- systemctl stop alfe-gateway
36
-
37
- # Upgrade CLI to target version
38
- npm install -g @alfe.ai/cli@${version}
39
-
40
- # Re-run setup to regenerate systemd unit with new binary path
41
- alfe setup --managed
42
-
43
- # Start the service — daemon will reconnect with new version
44
- systemctl start alfe-gateway
45
-
46
- # Clean up
47
- rm -f "$0"
48
- `, "utf-8");
49
- await chmod(scriptPath, 493);
50
- const child = spawn("bash", [scriptPath], {
51
- detached: true,
52
- stdio: "ignore"
53
- });
54
- child.unref();
55
- logger.info({
56
- scriptPath,
57
- version,
58
- pid: child.pid
59
- }, "Spawned detached upgrade script");
23
+ async function upgradeAndExit(version) {
24
+ logger.info({ version }, "Upgrading CLI...");
25
+ try {
26
+ const { stdout, stderr } = await execFileAsync("npm", [
27
+ "install",
28
+ "-g",
29
+ `@alfe.ai/cli@${version}`
30
+ ], { timeout: 12e4 });
31
+ if (stdout) logger.debug({ stdout: stdout.trim() }, "npm install stdout");
32
+ if (stderr) logger.debug({ stderr: stderr.trim() }, "npm install stderr");
33
+ logger.info({ version }, "CLI upgraded — exiting for systemd restart");
34
+ } catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ logger.error({
37
+ err: message,
38
+ version
39
+ }, "CLI upgrade failed");
40
+ }
41
+ process.exit(0);
60
42
  }
61
43
  //#endregion
62
- export { spawnUpgradeScript };
44
+ export { upgradeAndExit };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,10 +22,10 @@
22
22
  "pino-roll": "^1.2.0",
23
23
  "smol-toml": ">=1.6.1",
24
24
  "ws": "^8.18.0",
25
- "@alfe.ai/ai-proxy-local": "^0.0.2",
26
- "@alfe.ai/config": "^0.0.2",
27
- "@alfe.ai/doctor": "^0.0.2",
28
- "@alfe.ai/integrations": "^0.0.5"
25
+ "@alfe.ai/ai-proxy-local": "^0.0.4",
26
+ "@alfe.ai/config": "^0.0.4",
27
+ "@alfe.ai/doctor": "^0.0.4",
28
+ "@alfe.ai/integrations": "^0.0.7"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.5.13",