@alfe.ai/gateway 0.8.1 → 0.8.3

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
@@ -23814,7 +23814,10 @@ async function startDaemon() {
23814
23814
  await mcpManager.loadIntoBundler(mcpBundler);
23815
23815
  const warmBundler = (reason) => {
23816
23816
  if (!mcpBundler) return;
23817
- mcpBundler.warmup().catch((err) => {
23817
+ const target = mcpBundler;
23818
+ target.warmup().then(() => {
23819
+ warnFailedMcpServers(target, logger$1, reason);
23820
+ }).catch((err) => {
23818
23821
  logger$1.warn({
23819
23822
  err: err instanceof Error ? err.message : String(err),
23820
23823
  reason
@@ -24442,6 +24445,28 @@ function handleMcpListTools(bundler) {
24442
24445
  };
24443
24446
  }
24444
24447
  /**
24448
+ * Observability backstop for the INTEGRATION warm path. `applyForIntegration`
24449
+ * registers MCP servers into the store and the daemon warms them silently via
24450
+ * the store-change `onChange` hook — so a server that fails its connect (e.g.
24451
+ * an MCP server whose backing account/credential wasn't resolvable at warm
24452
+ * time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
24453
+ * making the black hole undiagnosable. After a warm, read `bundler.statuses()`
24454
+ * and warn once per still-failed server so the failure is visible. These
24455
+ * self-heal on the bundler's background retry sweep once the dependency
24456
+ * appears. Returns the servers it warned about (for tests / callers).
24457
+ */
24458
+ function warnFailedMcpServers(bundler, log, reason) {
24459
+ if (!bundler) return [];
24460
+ const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
24461
+ for (const status of failed) log.warn({
24462
+ server: status.name,
24463
+ reason,
24464
+ consecutiveFailures: status.consecutiveFailures,
24465
+ lastError: status.lastError
24466
+ }, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
24467
+ return failed;
24468
+ }
24469
+ /**
24445
24470
  * Route a tool call to the appropriate MCP child via the daemon-hosted
24446
24471
  * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
24447
24472
  * the raw JSON object the LLM produced.
@@ -1,5 +1,9 @@
1
1
  import pino from "pino";
2
2
 
3
+ //#region src/logger.d.ts
4
+
5
+ declare const logger: pino.Logger<never, boolean>;
6
+ //#endregion
3
7
  //#region src/protocol.d.ts
4
8
 
5
9
  interface IPCRequest {
@@ -335,9 +339,6 @@ declare function queryDaemonHealth(socketPath: string, timeoutMs?: number): Prom
335
339
  */
336
340
  declare function formatHealthReport(health: DaemonHealth): string;
337
341
  //#endregion
338
- //#region src/logger.d.ts
339
- declare const logger: pino.Logger<never, boolean>;
340
- //#endregion
341
342
  //#region src/process-manager.d.ts
342
343
  /**
343
344
  * Install the service unit for the current platform.
package/dist/upgrade.js CHANGED
@@ -30,7 +30,7 @@ import { existsSync } from "node:fs";
30
30
  * This module holds only pure decision logic + a thin exec wrapper so the
31
31
  * decision can be unit-tested without a real install on disk.
32
32
  */
33
- const execFileAsync$1 = promisify(execFile);
33
+ const execFileAsync$2 = promisify(execFile);
34
34
  /**
35
35
  * Pure verification decision. Given a probe of the install on disk, decide
36
36
  * whether it is intact. When `expectedVersion` is provided, the reported
@@ -96,7 +96,7 @@ async function resolveCliEntryPath(fromUrl) {
96
96
  */
97
97
  async function tryVersion(cmd, args) {
98
98
  try {
99
- const { stdout } = await execFileAsync$1(cmd, args, { timeout: 15e3 });
99
+ const { stdout } = await execFileAsync$2(cmd, args, { timeout: 15e3 });
100
100
  const out = stdout.trim();
101
101
  return out.length > 0 ? out : null;
102
102
  } catch {
@@ -127,6 +127,121 @@ async function verifyCliInstall(expectedVersion, entryPath) {
127
127
  }, expectedVersion);
128
128
  }
129
129
  //#endregion
130
+ //#region src/orphan-cleanup.ts
131
+ /**
132
+ * Orphan npm-reify temp-dir cleanup — makes CLI self-upgrade ENOTEMPTY-safe.
133
+ *
134
+ * When npm installs a global package it uses `reify`, which RENAMES the existing
135
+ * package dir to a `.`-prefixed temp sibling before swapping in the new tree:
136
+ *
137
+ * /usr/lib/node_modules/@alfe.ai/cli → /usr/lib/node_modules/@alfe.ai/.cli-<hash>
138
+ *
139
+ * If that install is KILLED mid-reify — exactly what happens when the daemon
140
+ * self-updates by running `npm install -g @alfe.ai/cli@<v>` out of its OWN live
141
+ * dir and systemd SIGKILLs it — the `.cli-<hash>` temp is left ORPHANED. The next
142
+ * install then fails with:
143
+ *
144
+ * ENOTEMPTY: rename '.../@alfe.ai/cli' → '.../@alfe.ai/.cli-<hash>'
145
+ *
146
+ * because npm's rename TARGET already exists. This bricked GoldRush's upgrade
147
+ * (see `.claude/agent-memory/agent-runtime-engineer/incident_2026_07_13_...`).
148
+ *
149
+ * Fix: sweep stale `.`-prefixed reify temps for our package BEFORE running the
150
+ * install. Guarded to only ever remove `.`-prefixed temp SIBLINGS of the real
151
+ * package (`.cli-*`), never the real `cli` dir and never unrelated packages.
152
+ *
153
+ * This module keeps the SELECTION logic pure (`findOrphanReifyTempDirs`) so it is
154
+ * unit-testable without touching the filesystem; the side-effecting sweep is a
155
+ * thin wrapper.
156
+ */
157
+ const execFileAsync$1 = promisify(execFile);
158
+ /** The npm scope dir the CLI + gateway live under (`@alfe.ai`). */
159
+ const CLI_SCOPE = "@alfe.ai";
160
+ /**
161
+ * Pure: given the directory entries SIBLING to the real package dir, return the
162
+ * `.`-prefixed reify temps that belong to `<packageBasename>` — i.e. entries
163
+ * matching `.<packageBasename>-*`. Excludes the real dir (`<packageBasename>`),
164
+ * the bare `.<packageBasename>` (no `-<hash>` suffix — never a reify temp), and
165
+ * any temp for a different package.
166
+ */
167
+ function findOrphanReifyTempDirs(siblings, packageBasename) {
168
+ const prefix = `.${packageBasename}-`;
169
+ return siblings.filter((name) => name.startsWith(prefix));
170
+ }
171
+ /**
172
+ * Resolve the npm global scope dir (`<globalNodeModules>/@alfe.ai`) that holds
173
+ * the CLI package and, potentially, its orphaned reify temps.
174
+ *
175
+ * Detection order (robust — never hardcode `/usr/lib`):
176
+ * 1. `npm root -g` → the global `node_modules` root. Authoritative on the box.
177
+ * 2. Derive from the resolved CLI entry path (`.../node_modules/@alfe.ai/cli/
178
+ * dist/index.js`) by walking up to the `node_modules` segment.
179
+ *
180
+ * Returns undefined when neither resolves (dev checkout / non-global run); the
181
+ * caller then skips the sweep (there is no global install to clean).
182
+ */
183
+ async function resolveGlobalScopeDir(cliEntryPath) {
184
+ const { join, sep } = await import("node:path");
185
+ try {
186
+ const { stdout } = await execFileAsync$1("npm", ["root", "-g"], { timeout: 15e3 });
187
+ const root = stdout.trim();
188
+ if (root.length > 0) return join(root, CLI_SCOPE);
189
+ } catch {}
190
+ if (cliEntryPath) {
191
+ const marker = `${sep}node_modules${sep}`;
192
+ const idx = cliEntryPath.lastIndexOf(marker);
193
+ if (idx !== -1) return join(cliEntryPath.slice(0, idx + marker.length - 1), CLI_SCOPE);
194
+ }
195
+ }
196
+ /**
197
+ * Sweep orphaned `.cli-*` reify temp dirs from the CLI's global scope dir before
198
+ * a self-upgrade. Best-effort and non-throwing: a cleanup failure must never
199
+ * block the upgrade (worst case, the install ENOTEMPTYs and we're no worse off
200
+ * than before this defense existed).
201
+ *
202
+ * @param scopeDir the `<globalNodeModules>/@alfe.ai` dir (from
203
+ * {@link resolveGlobalScopeDir}). When undefined, no-op.
204
+ */
205
+ async function sweepOrphanReifyTempDirs(scopeDir) {
206
+ if (!scopeDir) {
207
+ logger.debug("Orphan sweep skipped — could not resolve global scope dir");
208
+ return;
209
+ }
210
+ try {
211
+ const { readdir, rm } = await import("node:fs/promises");
212
+ const { join } = await import("node:path");
213
+ let siblings;
214
+ try {
215
+ siblings = await readdir(scopeDir);
216
+ } catch {
217
+ return;
218
+ }
219
+ const orphans = findOrphanReifyTempDirs(siblings, "cli");
220
+ if (orphans.length === 0) return;
221
+ logger.warn({
222
+ scopeDir,
223
+ orphans
224
+ }, "Removing orphaned npm reify temp dirs before upgrade (ENOTEMPTY guard)");
225
+ for (const orphan of orphans) {
226
+ const target = join(scopeDir, orphan);
227
+ try {
228
+ await rm(target, {
229
+ recursive: true,
230
+ force: true
231
+ });
232
+ logger.info({ target }, "Removed orphaned reify temp dir");
233
+ } catch (err) {
234
+ logger.warn({
235
+ err: err instanceof Error ? err.message : String(err),
236
+ target
237
+ }, "Failed to remove orphaned reify temp dir (non-fatal)");
238
+ }
239
+ }
240
+ } catch (err) {
241
+ logger.warn({ err: err instanceof Error ? err.message : String(err) }, "Orphan reify temp-dir sweep failed (non-fatal)");
242
+ }
243
+ }
244
+ //#endregion
130
245
  //#region src/upgrade.ts
131
246
  /**
132
247
  * CLI upgrade — runs npm install inline then exits.
@@ -160,6 +275,11 @@ const execFileAsync = promisify(execFile);
160
275
  */
161
276
  async function upgradeAndExit(version) {
162
277
  logger.info({ version }, "Upgrading CLI...");
278
+ try {
279
+ await sweepOrphanReifyTempDirs(await resolveGlobalScopeDir(await resolveCliEntryPath(import.meta.url)));
280
+ } catch (err) {
281
+ logger.warn({ err: err instanceof Error ? err.message : String(err) }, "Orphan reify-temp sweep threw unexpectedly — proceeding with upgrade anyway");
282
+ }
163
283
  try {
164
284
  const { stdout, stderr } = await execFileAsync("npm", [
165
285
  "install",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.13",
28
28
  "@alfe.ai/config": "^0.3.0",
29
29
  "@alfe.ai/integration-manifest": "^0.3.2",
30
- "@alfe.ai/integrations": "^0.5.1",
31
- "@alfe.ai/mcp-bundler": "^0.3.1"
30
+ "@alfe.ai/integrations": "^0.5.2",
31
+ "@alfe.ai/mcp-bundler": "^0.3.2"
32
32
  },
33
33
  "license": "UNLICENSED",
34
34
  "homepage": "https://alfe.ai",