@alfe.ai/integrations 0.5.3 → 0.6.0

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/index.d.ts CHANGED
@@ -290,6 +290,21 @@ declare class Installer {
290
290
  */
291
291
  private installLocalDependencies;
292
292
  private runNpmInstall;
293
+ /**
294
+ * Pre-fetch every npx-executed MCP server package the manifest declares
295
+ * (e.g. `command: npx, args: ["-y", "@alfe.ai/xero-mcp@0.3.8"]`) into the
296
+ * npx cache, WITHOUT executing the server. The first bundler connect then
297
+ * spawns from a warm cache instead of paying a 30-90s npm download during
298
+ * the MCP handshake — where it used to blow the connect budget and strand
299
+ * the server's tools until a daemon restart.
300
+ *
301
+ * Strictly best-effort: a missing/unparseable manifest, a non-npx command,
302
+ * or a failed download logs a warning and moves on — an install must never
303
+ * fail because a pre-warm did.
304
+ */
305
+ prewarmMcpNpxPackages(installPath: string): Promise<void>;
306
+ /** Populate the npx cache for a package spec via a no-op command. */
307
+ private runNpx;
293
308
  /**
294
309
  * List all locally installed integrations.
295
310
  */
package/dist/index.js CHANGED
@@ -157,6 +157,7 @@ const log$6 = createLogger("Installer");
157
157
  const INTEGRATIONS_DIR = join(homedir(), ".alfe", "integrations");
158
158
  const GIT_TIMEOUT_MS = 6e4;
159
159
  const NPM_TIMEOUT_MS = 6e4;
160
+ const NPX_PREWARM_TIMEOUT_MS = 12e4;
160
161
  /**
161
162
  * Dot-prefix for the transient staging directories used by the diff-based
162
163
  * in-place upgrade (`stage()` → `commitStaged()`). Dot-prefixed so `list()`
@@ -214,6 +215,7 @@ var Installer = class {
214
215
  else await this.cloneDirect(resolved, installPath);
215
216
  await this.ensureSharedPackages();
216
217
  await this.installLocalDependencies(installPath);
218
+ await this.prewarmMcpNpxPackages(installPath);
217
219
  return installPath;
218
220
  }
219
221
  /**
@@ -304,6 +306,7 @@ var Installer = class {
304
306
  else await this.cloneDirect(resolved, stagingPath);
305
307
  await this.ensureSharedPackages();
306
308
  await this.installLocalDependencies(stagingPath);
309
+ await this.prewarmMcpNpxPackages(stagingPath);
307
310
  return stagingPath;
308
311
  } catch (err) {
309
312
  if (existsSync(stagingPath)) rmSync(stagingPath, {
@@ -426,6 +429,63 @@ var Installer = class {
426
429
  }
427
430
  }
428
431
  /**
432
+ * Pre-fetch every npx-executed MCP server package the manifest declares
433
+ * (e.g. `command: npx, args: ["-y", "@alfe.ai/xero-mcp@0.3.8"]`) into the
434
+ * npx cache, WITHOUT executing the server. The first bundler connect then
435
+ * spawns from a warm cache instead of paying a 30-90s npm download during
436
+ * the MCP handshake — where it used to blow the connect budget and strand
437
+ * the server's tools until a daemon restart.
438
+ *
439
+ * Strictly best-effort: a missing/unparseable manifest, a non-npx command,
440
+ * or a failed download logs a warning and moves on — an install must never
441
+ * fail because a pre-warm did.
442
+ */
443
+ async prewarmMcpNpxPackages(installPath) {
444
+ let servers;
445
+ try {
446
+ const manifestPath = join(installPath, "alfe-integration.yaml");
447
+ if (!existsSync(manifestPath)) return;
448
+ servers = parseManifestFile(manifestPath).mcp_servers;
449
+ } catch (err) {
450
+ log$6.warn({
451
+ path: installPath,
452
+ err: err instanceof Error ? err.message : String(err)
453
+ }, "Skipping MCP npx pre-warm — manifest unreadable");
454
+ return;
455
+ }
456
+ for (const server of servers) {
457
+ if (server.command !== "npx") continue;
458
+ const spec = (server.args ?? []).find((a) => !a.startsWith("-"));
459
+ if (!spec) continue;
460
+ try {
461
+ log$6.info({
462
+ server: server.id,
463
+ spec
464
+ }, "Pre-warming npx cache for MCP server package");
465
+ await this.runNpx(spec);
466
+ } catch (err) {
467
+ log$6.warn({
468
+ server: server.id,
469
+ spec,
470
+ err: err instanceof Error ? err.message : String(err)
471
+ }, "MCP npx pre-warm failed — first connect will pay the cold download");
472
+ }
473
+ }
474
+ }
475
+ /** Populate the npx cache for a package spec via a no-op command. */
476
+ async runNpx(spec) {
477
+ await execFileAsync$2("npx", [
478
+ "-y",
479
+ "-p",
480
+ spec,
481
+ "-c",
482
+ "exit 0"
483
+ ], {
484
+ timeout: NPX_PREWARM_TIMEOUT_MS,
485
+ maxBuffer: 16 * 1024 * 1024
486
+ });
487
+ }
488
+ /**
429
489
  * List all locally installed integrations.
430
490
  */
431
491
  list() {
@@ -4235,6 +4295,17 @@ const log = createLogger("McpApplier");
4235
4295
  const CONFIG_TEMPLATE_RE = /\{\{config\.([a-zA-Z0-9_]+)\}\}/g;
4236
4296
  const CREDENTIALS_TEMPLATE_RE = /\{\{credentials\.([a-z0-9-]+)\.([a-zA-Z0-9_]+)\}\}/g;
4237
4297
  const ALFE_TEMPLATE_RE = /\{\{alfe\.([a-zA-Z0-9_]+)\}\}/g;
4298
+ /**
4299
+ * Budget for the activation-time warm of each applied server. Matches the
4300
+ * bundler's default `connectTimeoutMs` so the warm never reports "failed"
4301
+ * for a connect that would still succeed within its own bound. Generous
4302
+ * enough to cover a cold `npx` npm download when the installer's pre-warm
4303
+ * failed; typically the pre-warm makes this a <5s cache hit. The warm is
4304
+ * fire-and-forget (see applyForIntegration), so this budget never extends
4305
+ * the reconcile pass — a warm that fails anyway is picked up by the
4306
+ * bundler's `retryNeverConnected` sweep within a minute.
4307
+ */
4308
+ const MCP_ACTIVATION_WARM_TIMEOUT_MS = 12e4;
4238
4309
  var McpApplier = class {
4239
4310
  manager;
4240
4311
  credentials;
@@ -4275,6 +4346,25 @@ var McpApplier = class {
4275
4346
  });
4276
4347
  applied.push(id);
4277
4348
  }
4349
+ for (const id of applied) this.manager.warmServer(id, MCP_ACTIVATION_WARM_TIMEOUT_MS).then((status) => {
4350
+ if (!status) return;
4351
+ if (status.connected) log.info({
4352
+ integrationId,
4353
+ server: id,
4354
+ toolCount: status.toolCount
4355
+ }, "MCP server warmed on activation");
4356
+ else log.warn({
4357
+ integrationId,
4358
+ server: id,
4359
+ lastError: status.lastError
4360
+ }, "MCP server failed activation warm — retry sweep will re-attempt");
4361
+ }).catch((err) => {
4362
+ log.warn({
4363
+ integrationId,
4364
+ server: id,
4365
+ err: errMsg(err)
4366
+ }, "MCP activation warm threw — retry sweep will re-attempt");
4367
+ });
4278
4368
  return applied;
4279
4369
  }
4280
4370
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,8 +14,8 @@
14
14
  "dependencies": {
15
15
  "@auriclabs/logger": "^0.1.1",
16
16
  "yaml": ">=2.8.3",
17
- "@alfe.ai/integration-manifest": "^0.3.3",
18
- "@alfe.ai/mcp-bundler": "^0.3.2"
17
+ "@alfe.ai/integration-manifest": "^0.3.4",
18
+ "@alfe.ai/mcp-bundler": "^0.4.0"
19
19
  },
20
20
  "files": [
21
21
  "dist"