@alfe.ai/integrations 0.4.1 → 0.5.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
@@ -1508,6 +1508,203 @@ declare class HermesMcpSync {
1508
1508
  */
1509
1509
  declare function upsertEnvVar(envPath: string, key: string, value: string): boolean;
1510
1510
  //#endregion
1511
+ //#region src/appliers/claude-code-applier.d.ts
1512
+ interface ClaudeCodeApplierOptions {
1513
+ /**
1514
+ * Path to the claude-code home directory (e.g. ~/.claude-code) — where the
1515
+ * config ledger and the generated `--mcp-config` live. For claude-code,
1516
+ * home == workspace.
1517
+ */
1518
+ home?: string;
1519
+ /**
1520
+ * @deprecated Use `home`. Kept for parity with the OpenClaw/Hermes applier
1521
+ * option shape — when set, used as `home`.
1522
+ */
1523
+ workspace?: string;
1524
+ /** Path to the per-integration config ledger (defaults to {home}/.alfe-integrations.json). */
1525
+ configPath?: string;
1526
+ /**
1527
+ * Path to Claude Code's native skills directory (defaults to
1528
+ * ~/.claude/skills). Override for tests.
1529
+ */
1530
+ skillsDir?: string;
1531
+ }
1532
+ declare class ClaudeCodeApplier implements RuntimeApplier {
1533
+ readonly runtime = "claude-code";
1534
+ private home;
1535
+ /** `~/.claude-code/.alfe-integrations.json` — per-integration config ledger + raw-key store. */
1536
+ private ledgerPath;
1537
+ /** `~/.claude/skills` — where native Claude Code skills are copied. */
1538
+ private skillsDir;
1539
+ constructor(options?: ClaudeCodeApplierOptions);
1540
+ /**
1541
+ * Record an integration's config contribution in the ledger. Claude Code has
1542
+ * no integration-config CLI, so this is a pure ledger write — it makes the
1543
+ * per-integration accounting honest (so `removeConfig` and the lock's
1544
+ * config-only tracking work) without shelling anything. Overwrites the prior
1545
+ * contribution for this integration (convergent by construction — a full
1546
+ * replace, no stale-key diff needed since nothing is applied to a runtime).
1547
+ */
1548
+ applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
1549
+ /** Clear an integration's config contribution from the ledger. */
1550
+ removeConfig(integrationId: string): Promise<void>;
1551
+ /**
1552
+ * Raw single-key config write for the daemon's `alfe.config_set` cloud-command.
1553
+ *
1554
+ * Deliberately bypasses the `_integrations` per-integration accounting (a raw
1555
+ * set is untracked — routing it through `applyConfig` would corrupt an
1556
+ * integration's removal accounting). Stored under a separate `_raw` map in the
1557
+ * same ledger file. Inert as far as the runtime goes (Claude Code has no config
1558
+ * CLI to push it into), but it degrades cleanly — the daemon gets an OK ack
1559
+ * instead of `CONFIG_SET_UNSUPPORTED`, and the value round-trips via
1560
+ * `getConfigRaw`.
1561
+ */
1562
+ setConfigRaw(key: string, value: string): Promise<void>;
1563
+ /**
1564
+ * Read a raw config key back from the ledger — the untracked read companion to
1565
+ * `setConfigRaw`, used by the daemon's config-reconcile pass (value-diff before
1566
+ * write; verify a write landed). Returns the exact string `setConfigRaw` stored,
1567
+ * or `undefined` when the key was never set. Never throws.
1568
+ */
1569
+ getConfigRaw(key: string): Promise<string | undefined>;
1570
+ /**
1571
+ * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
1572
+ * and do NOT load in Claude Code — log + skip them. No claude-code-native
1573
+ * plugin spec exists, so any non-openclaw spec is also skipped (rather than a
1574
+ * blind shell of an unknown installer). The manager catches per-plugin, so a
1575
+ * skip here is a correct no-op.
1576
+ */
1577
+ applyPlugin(spec: string): Promise<void>;
1578
+ /** Remove a plugin. Nothing was installed (applyPlugin skipped it) — no-op log. */
1579
+ removePlugin(spec: string): Promise<void>;
1580
+ /**
1581
+ * Copy a skill directory into `~/.claude/skills/<name>/`. Claude Code reads
1582
+ * native Agent Skills from there, so an Alfe skill dir lands as a first-class
1583
+ * Claude Code skill. Idempotent: an existing target is replaced (a
1584
+ * newer-version re-apply must overwrite).
1585
+ */
1586
+ applySkill(name: string, srcPath: string): Promise<void>;
1587
+ /** Remove a skill directory from `~/.claude/skills/<name>/`. */
1588
+ removeSkill(name: string): Promise<void>;
1589
+ /**
1590
+ * Install a ClawHub skill natively. ClawHub is an OpenClaw registry, but the
1591
+ * skill payload is a plain directory — for claude-code we treat the slug as the
1592
+ * skill name and expect the caller to have staged the directory. Absent a
1593
+ * staged source we log + skip (no OpenClaw `skills install` CLI on this
1594
+ * runtime); a future claude-code ClawHub fetch would stage the dir then copy it
1595
+ * exactly like `applySkill`.
1596
+ *
1597
+ * NOTE: unlike OpenClaw's `applyClawHubSkill` (which THROWS on a real install
1598
+ * failure so an all-skills-fail activation is marked `error`), there is no
1599
+ * install to fail here — the skill either lands from a staged directory or is a
1600
+ * clean no-op. We never throw for a claude-code ClawHub skill.
1601
+ */
1602
+ applyClawHubSkill(slug: string): Promise<void>;
1603
+ /** Remove a ClawHub-installed skill directory (by slug == dir name). */
1604
+ removeClawHubSkill(slug: string): Promise<void>;
1605
+ isAvailable(): Promise<boolean>;
1606
+ private copySkillDir;
1607
+ private deleteSkillDir;
1608
+ private readLedger;
1609
+ private writeLedger;
1610
+ }
1611
+ //#endregion
1612
+ //#region src/appliers/claude-code-mcp-sync.d.ts
1613
+ interface ClaudeCodeMcpSyncOptions {
1614
+ /** The runtime-agnostic MCP store manager (read + onChange). */
1615
+ manager: McpStoreReader;
1616
+ /** Claude-code home (== ~/.claude-code). mcp-config.json lives here. */
1617
+ home?: string;
1618
+ /**
1619
+ * The `ALFE_API_KEY` value injected into every generated stdio server's env so
1620
+ * the Alfe MCP servers resolve their provider credentials. In managed mode
1621
+ * this is the daemon's own api key; self-hosted threads it explicitly. When
1622
+ * absent, injection is skipped and a warning is logged (the servers would then
1623
+ * start in zero-accounts degraded mode). Unlike Hermes there is no `.env`
1624
+ * side-file — the value is written directly into the generated env block.
1625
+ */
1626
+ apiKey?: string;
1627
+ /**
1628
+ * Trigger a runtime reload after a real change. Accepted for parity with
1629
+ * `HermesMcpSyncOptions` so the daemon can wire claude-code identically — but
1630
+ * Claude Code reads `--mcp-config` fresh per `claude -p` spawn, so this sync
1631
+ * NEVER calls it. Kept optional and unused; see the file header.
1632
+ */
1633
+ requestRestart?: () => void;
1634
+ /** Override the mcp-config.json path (defaults to {home}/mcp-config.json). For tests. */
1635
+ configPath?: string;
1636
+ /**
1637
+ * Override the sidecar that records which `mcpServers` ids this sync wrote
1638
+ * (defaults to {home}/.alfe-mcp-synced.json). Used so removals survive daemon
1639
+ * restarts (a full regenerate makes it strictly informational here, but kept
1640
+ * for parity + forensics).
1641
+ */
1642
+ trackingPath?: string;
1643
+ /**
1644
+ * Trailing debounce (ms) used to coalesce a burst of store mutations (one
1645
+ * integration install fires `addServer` once per server) into a single file
1646
+ * write, avoiding a write storm. Default 250; set 0 in tests. The initial sync
1647
+ * in `start()` is always immediate.
1648
+ */
1649
+ debounceMs?: number;
1650
+ }
1651
+ /**
1652
+ * Full-regenerate writer of the MCP store into `~/.claude-code/mcp-config.json`.
1653
+ *
1654
+ * Ownership / removal model: every entry returned by `manager.listServers()`
1655
+ * comes from the Alfe store (its `owner` is `integration:<id>` / `cli` /
1656
+ * `manual`) and is therefore Alfe-owned. Unlike the Hermes config.yaml mirror
1657
+ * (which must preserve user-authored `mcp_servers` in a shared file), the
1658
+ * `--mcp-config` file is Alfe-generated and Alfe-owned end to end — Claude Code's
1659
+ * OWN user-authored MCP servers live in `~/.claude.json` / project `.mcp.json`,
1660
+ * NOT this generated file. So we fully regenerate it from the store on every
1661
+ * change (no read-merge-write needed). We still persist the written-id set to a
1662
+ * sidecar for parity + forensics, but removal is implicit in the regenerate.
1663
+ */
1664
+ declare class ClaudeCodeMcpSync {
1665
+ private readonly manager;
1666
+ private readonly home;
1667
+ private readonly apiKey?;
1668
+ private readonly configPath;
1669
+ private readonly trackingPath;
1670
+ private readonly debounceMs;
1671
+ /** Ids of `mcpServers` entries this sync last wrote — informational sidecar. */
1672
+ private syncedIds;
1673
+ /** Serializes syncs so two onChange-driven runs can't interleave file writes. */
1674
+ private queue;
1675
+ private unsubscribe?;
1676
+ private debounceTimer?;
1677
+ private started;
1678
+ constructor(opts: ClaudeCodeMcpSyncOptions);
1679
+ /**
1680
+ * Begin mirroring: load the prior removal set, run one immediate sync (so the
1681
+ * `--mcp-config` file reflects the current store before the host next spawns
1682
+ * `claude -p`), then subscribe to store changes (debounced). Idempotent.
1683
+ */
1684
+ start(): void;
1685
+ /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
1686
+ stop(): void;
1687
+ /**
1688
+ * Regenerate the `--mcp-config` file from the current store exactly once.
1689
+ * Public so the daemon (and tests) can await a deterministic sync. Serialized
1690
+ * against any other in-flight sync.
1691
+ */
1692
+ syncOnce(): Promise<void>;
1693
+ private schedule;
1694
+ private syncNow;
1695
+ private computeDesired;
1696
+ /**
1697
+ * Transform a stored entry into a Claude Code `--mcp-config` entry. stdio
1698
+ * entries get the literal `ALFE_API_KEY` injected; remote entries pass through
1699
+ * with the documented `type` discriminator (`sse` → `sse`, `streamable-http`
1700
+ * → `http`).
1701
+ */
1702
+ private toClaudeCodeEntry;
1703
+ private withAlfeApiKey;
1704
+ private loadSyncedIds;
1705
+ private persistSyncedIds;
1706
+ }
1707
+ //#endregion
1511
1708
  //#region src/lock.d.ts
1512
1709
  interface RuntimePluginEntry {
1513
1710
  /**
@@ -1677,4 +1874,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
1677
1874
  resetReinstallAttempts(integrationId: string): void;
1678
1875
  }
1679
1876
  //#endregion
1680
- export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, type LoadOptions, LockManager, McpApplier, type McpApplierOptions, type McpStoreReader, NoopOpenClawCliLock, OpenClawApplier, type OpenClawApplierOptions, type OpenClawCliLock, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
1877
+ export { ClaudeCodeApplier, type ClaudeCodeApplierOptions, ClaudeCodeMcpSync, type ClaudeCodeMcpSyncOptions, type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, type LoadOptions, LockManager, McpApplier, type McpApplierOptions, type McpStoreReader, NoopOpenClawCliLock, OpenClawApplier, type OpenClawApplierOptions, type OpenClawCliLock, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { homedir, platform, tmpdir } from "node:os";
6
- import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { chmodSync, closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
8
8
  import { createLogger } from "@auriclabs/logger";
9
9
  import { parseDocument } from "yaml";
@@ -153,7 +153,7 @@ var Resolver = class {
153
153
  * can resolve them via Node's upward module resolution.
154
154
  */
155
155
  const execFileAsync$2 = promisify(execFile);
156
- const log$4 = createLogger("Installer");
156
+ 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;
@@ -342,7 +342,7 @@ var Installer = class {
342
342
  force: true
343
343
  });
344
344
  } catch (err) {
345
- log$4.warn({
345
+ log$6.warn({
346
346
  dir: entry.name,
347
347
  err: err instanceof Error ? err.message : String(err)
348
348
  }, "Failed to sweep orphaned staging dir");
@@ -397,7 +397,7 @@ var Installer = class {
397
397
  dependencies: { ...SHARED_PACKAGES }
398
398
  };
399
399
  writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf-8");
400
- log$4.info("Installing shared @alfe.ai packages for integration hooks");
400
+ log$6.info("Installing shared @alfe.ai packages for integration hooks");
401
401
  await this.runNpmInstall(this.basePath);
402
402
  this.sharedPackagesReady = true;
403
403
  }
@@ -407,7 +407,7 @@ var Installer = class {
407
407
  */
408
408
  async installLocalDependencies(installPath) {
409
409
  if (!existsSync(join(installPath, "package.json"))) return;
410
- log$4.info({ path: installPath }, "Installing integration-specific npm dependencies");
410
+ log$6.info({ path: installPath }, "Installing integration-specific npm dependencies");
411
411
  await this.runNpmInstall(installPath);
412
412
  }
413
413
  async runNpmInstall(cwd) {
@@ -1302,15 +1302,27 @@ var IntegrationManager = class {
1302
1302
  }
1303
1303
  if (pluginFailures.length > 0 && pluginFailures.length === plugins.length) throw new Error(`All plugins failed to install: ${pluginFailures.join(", ")}`);
1304
1304
  if (pluginFailures.length > 0) this.log.warn(`Continuing activation with ${String(pluginFailures.length)} failed plugin(s): ${pluginFailures.join(", ")}`);
1305
- for (const skill of skills) if (skill.clawhub) {
1306
- this.log.info(`Installing ClawHub skill ${skill.clawhub} to ${runtimeName}`);
1307
- await applier.applyClawHubSkill(skill.clawhub);
1308
- } else if (skill.path) {
1309
- const skillName = skill.path.split("/").pop() ?? skill.path;
1310
- const srcPath = join(installPath, skill.path);
1311
- this.log.info(`Applying skill ${skillName} to ${runtimeName}`);
1312
- await applier.applySkill(skillName, srcPath);
1305
+ const skillFailures = [];
1306
+ for (const skill of skills) {
1307
+ const skillLabel = skill.clawhub ?? skill.path ?? "unknown";
1308
+ try {
1309
+ if (skill.clawhub) {
1310
+ this.log.info(`Installing ClawHub skill ${skill.clawhub} to ${runtimeName}`);
1311
+ await applier.applyClawHubSkill(skill.clawhub);
1312
+ } else if (skill.path) {
1313
+ const skillName = skill.path.split("/").pop() ?? skill.path;
1314
+ const srcPath = join(installPath, skill.path);
1315
+ this.log.info(`Applying skill ${skillName} to ${runtimeName}`);
1316
+ await applier.applySkill(skillName, srcPath);
1317
+ }
1318
+ } catch (err) {
1319
+ const msg = err instanceof Error ? err.message : String(err);
1320
+ this.log.error(`Failed to apply skill ${skillLabel}: ${msg}`);
1321
+ skillFailures.push(skillLabel);
1322
+ }
1313
1323
  }
1324
+ if (skillFailures.length > 0 && skillFailures.length === skills.length) throw new Error(`All skills failed to install: ${skillFailures.join(", ")}`);
1325
+ if (skillFailures.length > 0) this.log.warn(`Continuing activation with ${String(skillFailures.length)} failed skill(s): ${skillFailures.join(", ")}`);
1314
1326
  let runtimeConfigApplied = false;
1315
1327
  if (runtimeConfig && Object.keys(runtimeConfig).length > 0) {
1316
1328
  const agentConfig = entry.config;
@@ -2086,7 +2098,7 @@ function partitionEntries(entries) {
2086
2098
  * is stored in a separate tracking file (config.json) for clean removal.
2087
2099
  */
2088
2100
  const execFileAsync$1 = promisify(execFile);
2089
- const log$3 = createLogger("OpenClawApplier");
2101
+ const log$5 = createLogger("OpenClawApplier");
2090
2102
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
2091
2103
  /**
2092
2104
  * Bundled OpenClaw plugins that ship inside the runtime (not installed as npm
@@ -2113,6 +2125,22 @@ const BUNDLED_BASELINE_ALLOW = ["browser"];
2113
2125
  const CONFIG_SET_RETRIES$1 = 3;
2114
2126
  const CONFIG_SET_RETRY_DELAY_MS$1 = 750;
2115
2127
  /**
2128
+ * Boot-safe exec timeout for the OUT-OF-BAND `setConfigRaw` write (the daemon's
2129
+ * config-reconcile pass / `alfe.config_set`). Unlike integration `applyConfig`,
2130
+ * which runs under the gateway RuntimeGate with the runtime SUSPENDED (no CPU
2131
+ * contention, so the 10s execFile default is plenty), `setConfigRaw` fires while
2132
+ * `openclaw gateway run` is live and — on a fresh managed box — still BOOTING.
2133
+ * On a 2-vCPU Hetzner cx-class instance the `openclaw config set` CLI cold-start
2134
+ * takes >10s under that boot contention, so the 10s default SIGKILLs the child
2135
+ * mid-write and every retry lands inside the same ~30s boot window and times out
2136
+ * identically (observed in prod: two `config set agents.defaults.model` attempts
2137
+ * logged exactly 10s apart with no exit line, then a 3rd succeeded once load
2138
+ * eased). 60s clears the cold-start-under-contention worst case while still
2139
+ * bounding a genuinely hung CLI. This is ONLY for `setConfigRaw` — the
2140
+ * integration `applyConfig` paths keep the 10s default (they're not contended).
2141
+ */
2142
+ const CONFIG_SET_RAW_TIMEOUT_MS = 6e4;
2143
+ /**
2116
2144
  * Max `openclaw config get` spawns in flight at once during the batch
2117
2145
  * verify-after-write. A naive `Promise.all(leaves.map(...))` fires one CLI
2118
2146
  * process per leaf simultaneously — a 36-leaf model-provider batch on a 2-vCPU
@@ -2481,7 +2509,7 @@ var OpenClawApplier = class {
2481
2509
  return;
2482
2510
  } catch (err) {
2483
2511
  if (!isMalformedStateDbError(err)) throw err;
2484
- log$3.warn({ args: args.slice(0, 2) }, "openclaw CLI failed with a malformed state DB — attempting self-heal then one retry");
2512
+ log$5.warn({ args: args.slice(0, 2) }, "openclaw CLI failed with a malformed state DB — attempting self-heal then one retry");
2485
2513
  if (!await this.healMalformedStateDb()) throw err;
2486
2514
  await execFileAsync$1("openclaw", args, opts);
2487
2515
  }
@@ -2507,7 +2535,7 @@ var OpenClawApplier = class {
2507
2535
  async healMalformedStateDb() {
2508
2536
  const now = Date.now();
2509
2537
  if (now - this.lastHealAt < HEAL_MIN_INTERVAL_MS) {
2510
- log$3.warn({ sinceLastHealMs: now - this.lastHealAt }, "openclaw state DB malformed but a heal ran within the rate-limit window — skipping to avoid a heal→corrupt→heal loop");
2538
+ log$5.warn({ sinceLastHealMs: now - this.lastHealAt }, "openclaw state DB malformed but a heal ran within the rate-limit window — skipping to avoid a heal→corrupt→heal loop");
2511
2539
  return false;
2512
2540
  }
2513
2541
  this.lastHealAt = now;
@@ -2527,19 +2555,19 @@ var OpenClawApplier = class {
2527
2555
  quarantined.push(dest);
2528
2556
  }
2529
2557
  if (quarantined.length === 0) {
2530
- log$3.warn({ stateDir }, "malformed openclaw state DB reported but no state/openclaw.sqlite* files found to quarantine");
2558
+ log$5.warn({ stateDir }, "malformed openclaw state DB reported but no state/openclaw.sqlite* files found to quarantine");
2531
2559
  this.lastHealAt = now - HEAL_MIN_INTERVAL_MS + FAILED_HEAL_COOLDOWN_MS;
2532
2560
  return false;
2533
2561
  }
2534
- log$3.warn({
2562
+ log$5.warn({
2535
2563
  stateDir,
2536
2564
  quarantined
2537
2565
  }, "quarantined malformed openclaw state DB — recreating via `openclaw plugins list` (upstream openclaw/openclaw#71689)");
2538
2566
  await execFileAsync$1("openclaw", ["plugins", "list"], { timeout: 9e4 });
2539
- log$3.warn({ stateDir }, "recreated openclaw state DB after quarantine");
2567
+ log$5.warn({ stateDir }, "recreated openclaw state DB after quarantine");
2540
2568
  return true;
2541
2569
  } catch (err) {
2542
- log$3.warn({
2570
+ log$5.warn({
2543
2571
  stateDir,
2544
2572
  quarantined,
2545
2573
  err: err instanceof Error ? err.message : String(err)
@@ -2556,7 +2584,7 @@ var OpenClawApplier = class {
2556
2584
  const pinnedVersion = pluginSpecVersion(spec);
2557
2585
  const claim = this.appliedPluginPins.get(pkg);
2558
2586
  if (pinnedVersion !== void 0 && claim !== void 0 && claim.version !== pinnedVersion && claim.integrationId !== opts?.integrationId) {
2559
- log$3.warn({
2587
+ log$5.warn({
2560
2588
  pkg,
2561
2589
  requestedVersion: pinnedVersion,
2562
2590
  keptVersion: claim.version,
@@ -2571,7 +2599,7 @@ var OpenClawApplier = class {
2571
2599
  const installedVersion = this.installedPluginVersion(pkg);
2572
2600
  const versionsComparable = pinnedVersion !== void 0 && installedVersion !== void 0;
2573
2601
  if (versionsComparable && installedVersion === pinnedVersion) {
2574
- if (opts?.force) log$3.info({
2602
+ if (opts?.force) log$5.info({
2575
2603
  pkg,
2576
2604
  spec,
2577
2605
  version: installedVersion
@@ -2583,7 +2611,7 @@ var OpenClawApplier = class {
2583
2611
  this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
2584
2612
  return;
2585
2613
  }
2586
- log$3.info({
2614
+ log$5.info({
2587
2615
  pkg,
2588
2616
  spec,
2589
2617
  installedVersion,
@@ -2593,7 +2621,7 @@ var OpenClawApplier = class {
2593
2621
  try {
2594
2622
  await this.removePluginUnlocked(pkg);
2595
2623
  } catch (err) {
2596
- log$3.warn({
2624
+ log$5.warn({
2597
2625
  pkg,
2598
2626
  spec,
2599
2627
  err: err instanceof Error ? err.message : String(err)
@@ -2615,7 +2643,7 @@ var OpenClawApplier = class {
2615
2643
  } catch (err) {
2616
2644
  const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
2617
2645
  if (useUnsafeFlag && errText.includes("unknown option")) {
2618
- log$3.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
2646
+ log$5.info({ pkg }, "OpenClaw does not support --dangerously-force-unsafe-install, retrying without");
2619
2647
  await this.execOpenClawHealing(baseArgs, { timeout: 6e4 });
2620
2648
  } else throw err;
2621
2649
  }
@@ -2624,7 +2652,7 @@ var OpenClawApplier = class {
2624
2652
  setTimeout(r, 500);
2625
2653
  });
2626
2654
  if (!this.isPluginInstalled(pkg)) throw err;
2627
- log$3.warn({
2655
+ log$5.warn({
2628
2656
  pkg,
2629
2657
  spec,
2630
2658
  err: err instanceof Error ? err.message : String(err)
@@ -2687,7 +2715,7 @@ var OpenClawApplier = class {
2687
2715
  const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
2688
2716
  if (missing.length === 0) return;
2689
2717
  if (!readTrustworthy) {
2690
- log$3.warn({
2718
+ log$5.warn({
2691
2719
  pkgs: wanted,
2692
2720
  missing
2693
2721
  }, "plugins.allow read was not trustworthy (non-empty parse failure or command error) — skipping the allow-list write to avoid replacing a value not proven to be a superset");
@@ -2701,7 +2729,7 @@ var OpenClawApplier = class {
2701
2729
  "--replace"
2702
2730
  ]);
2703
2731
  } catch (err) {
2704
- log$3.warn({
2732
+ log$5.warn({
2705
2733
  err: err instanceof Error ? err.message : String(err),
2706
2734
  pkgs: wanted
2707
2735
  }, "Failed to set plugins.allow via openclaw config set");
@@ -2786,12 +2814,12 @@ var OpenClawApplier = class {
2786
2814
  recursive: true,
2787
2815
  force: true
2788
2816
  });
2789
- log$3.info({
2817
+ log$5.info({
2790
2818
  pkg,
2791
2819
  removed: fullPath
2792
2820
  }, "Removed untracked extensions/ install — will reinstall via npm path");
2793
2821
  } catch (err) {
2794
- log$3.warn({
2822
+ log$5.warn({
2795
2823
  pkg,
2796
2824
  removed: fullPath,
2797
2825
  err: err instanceof Error ? err.message : String(err)
@@ -2825,24 +2853,25 @@ var OpenClawApplier = class {
2825
2853
  }
2826
2854
  applyClawHubSkill(slug) {
2827
2855
  return this.cliLock.run(async () => {
2828
- log$3.info({ slug }, "Installing skill from ClawHub");
2856
+ log$5.info({ slug }, "Installing skill from ClawHub");
2829
2857
  try {
2830
2858
  await execFileAsync$1("openclaw", [
2831
2859
  "skills",
2832
2860
  "install",
2833
2861
  slug
2834
2862
  ], { timeout: 6e4 });
2835
- log$3.info({ slug }, "ClawHub skill installed");
2863
+ log$5.info({ slug }, "ClawHub skill installed");
2836
2864
  } catch (err) {
2837
2865
  const msg = err instanceof Error ? err.message : String(err);
2838
2866
  if (msg.includes("already exists") || msg.includes("Skill already exists")) {
2839
- log$3.info({ slug }, "ClawHub skill already installed (skipped)");
2867
+ log$5.info({ slug }, "ClawHub skill already installed (skipped)");
2840
2868
  return;
2841
2869
  }
2842
- log$3.error({
2870
+ log$5.error({
2843
2871
  slug,
2844
2872
  err: msg
2845
2873
  }, "ClawHub skill install failed");
2874
+ throw new Error(`ClawHub skill install failed for "${slug}": ${msg}`);
2846
2875
  }
2847
2876
  });
2848
2877
  }
@@ -2853,7 +2882,7 @@ var OpenClawApplier = class {
2853
2882
  recursive: true,
2854
2883
  force: true
2855
2884
  });
2856
- log$3.info({ slug }, "ClawHub skill removed");
2885
+ log$5.info({ slug }, "ClawHub skill removed");
2857
2886
  }
2858
2887
  return Promise.resolve();
2859
2888
  }
@@ -2908,10 +2937,10 @@ var OpenClawApplier = class {
2908
2937
  const after = await readParentObject(parentPath);
2909
2938
  return [...dottedKvs.entries()].every(([k, v]) => deepSubset(v, after[k]));
2910
2939
  })) {
2911
- log$3.warn({ parentPath }, "openclaw config set exited non-zero but config landed — continuing");
2940
+ log$5.warn({ parentPath }, "openclaw config set exited non-zero but config landed — continuing");
2912
2941
  continue;
2913
2942
  }
2914
- log$3.error({
2943
+ log$5.error({
2915
2944
  err: err instanceof Error ? err.message : String(err),
2916
2945
  parentPath
2917
2946
  }, "Failed to set config subtree via openclaw config set");
@@ -2927,9 +2956,9 @@ var OpenClawApplier = class {
2927
2956
  } catch (err) {
2928
2957
  if (await this.verifyApplied(async () => {
2929
2958
  return (await mapWithConcurrency(leaves, VERIFY_GET_CONCURRENCY, (l) => this.configValueMatches(l.path, l.value))).every(Boolean);
2930
- })) log$3.warn({ count: leaves.length }, "openclaw config set --batch-json exited non-zero but config landed — continuing");
2959
+ })) log$5.warn({ count: leaves.length }, "openclaw config set --batch-json exited non-zero but config landed — continuing");
2931
2960
  else {
2932
- log$3.error({
2961
+ log$5.error({
2933
2962
  err: err instanceof Error ? err.message : String(err),
2934
2963
  batch: leaves
2935
2964
  }, "Failed to set config via openclaw config set --batch-json");
@@ -2981,7 +3010,7 @@ var OpenClawApplier = class {
2981
3010
  "--replace"
2982
3011
  ]);
2983
3012
  } catch (err) {
2984
- log$3.warn({
3013
+ log$5.warn({
2985
3014
  err: err instanceof Error ? err.message : String(err),
2986
3015
  parentPath
2987
3016
  }, "Failed to update parent config during subtree key drop");
@@ -3019,19 +3048,19 @@ var OpenClawApplier = class {
3019
3048
  unsetParents.add(parentPath);
3020
3049
  try {
3021
3050
  await this.runConfigCommandUnlocked(["unset", parentPath]);
3022
- log$3.warn({
3051
+ log$5.warn({
3023
3052
  path,
3024
3053
  parentPath
3025
3054
  }, "Leaf unset rejected as an invalid partial custom provider — unset the parent subtree instead");
3026
3055
  } catch (parentErr) {
3027
- log$3.warn({
3056
+ log$5.warn({
3028
3057
  err: parentErr instanceof Error ? parentErr.message : String(parentErr),
3029
3058
  parentPath
3030
3059
  }, "Failed to unset parent subtree after an invalid-partial leaf unset");
3031
3060
  }
3032
3061
  return;
3033
3062
  }
3034
- log$3.warn({
3063
+ log$5.warn({
3035
3064
  err: err instanceof Error ? err.message : String(err),
3036
3065
  path
3037
3066
  }, "Failed to unset config via openclaw config unset");
@@ -3048,7 +3077,7 @@ var OpenClawApplier = class {
3048
3077
  * config writes trigger.
3049
3078
  */
3050
3079
  setConfigRaw(key, value) {
3051
- return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
3080
+ return this.cliLock.run(() => this.runConfigSetUnlocked([key, value], { timeout: CONFIG_SET_RAW_TIMEOUT_MS }));
3052
3081
  }
3053
3082
  /**
3054
3083
  * Read a single config key back — `openclaw config get <key>` — and normalize
@@ -3137,7 +3166,7 @@ var OpenClawApplier = class {
3137
3166
  * config + plugins only.
3138
3167
  */
3139
3168
  const execFileAsync = promisify(execFile);
3140
- const log$2 = createLogger("HermesApplier");
3169
+ const log$4 = createLogger("HermesApplier");
3141
3170
  const DEFAULT_HERMES_HOME$1 = join(homedir(), ".hermes");
3142
3171
  /**
3143
3172
  * Hermes config writes are serialized through one promise chain so concurrent
@@ -3155,7 +3184,7 @@ const delay = (ms) => new Promise((resolve) => {
3155
3184
  setTimeout(resolve, ms);
3156
3185
  });
3157
3186
  /** `@alfe.ai/openclaw-*` packages are OpenClaw-specific plugins — not Hermes. */
3158
- function isOpenClawPlugin(spec) {
3187
+ function isOpenClawPlugin$1(spec) {
3159
3188
  return stripPluginVersion(spec).startsWith("@alfe.ai/openclaw-");
3160
3189
  }
3161
3190
  /**
@@ -3227,7 +3256,7 @@ var HermesApplier = class {
3227
3256
  if (staleLeafPaths.length > 0) try {
3228
3257
  await this.deleteConfigKeys(staleLeafPaths);
3229
3258
  } catch (err) {
3230
- log$2.warn({
3259
+ log$4.warn({
3231
3260
  err: err instanceof Error ? err.message : String(err),
3232
3261
  integrationId
3233
3262
  }, "Failed to delete stale Hermes config keys during applyConfig diff");
@@ -3236,7 +3265,7 @@ var HermesApplier = class {
3236
3265
  const nextKvs = subtreesByParent.get(parent);
3237
3266
  return [...prevKvs.keys()].some((k) => !nextKvs?.has(k));
3238
3267
  }).map(([parent]) => parent);
3239
- if (staleSubtreeParents.length > 0) log$2.warn({
3268
+ if (staleSubtreeParents.length > 0) log$4.warn({
3240
3269
  integrationId,
3241
3270
  parents: staleSubtreeParents
3242
3271
  }, "Hermes applyConfig diff: skipping stale dotted-key subtree(s) — OpenClaw-plugin-shaped config does not apply to Hermes");
@@ -3244,14 +3273,14 @@ var HermesApplier = class {
3244
3273
  integrations[integrationId] = config;
3245
3274
  tracking._integrations = integrations;
3246
3275
  this.writeTracking(tracking);
3247
- if (subtreesByParent.size > 0) log$2.warn({
3276
+ if (subtreesByParent.size > 0) log$4.warn({
3248
3277
  integrationId,
3249
3278
  parents: [...subtreesByParent.keys()]
3250
3279
  }, "Hermes applyConfig: skipping dotted-key subtree(s) — OpenClaw-plugin-shaped config does not apply to Hermes");
3251
3280
  for (const { path, value } of leaves) try {
3252
3281
  await this.runConfigSet([path, stringifyConfigValue(value)]);
3253
3282
  } catch (err) {
3254
- log$2.error({
3283
+ log$4.error({
3255
3284
  err: err instanceof Error ? err.message : String(err),
3256
3285
  key: path
3257
3286
  }, "Failed to set config via hermes config set");
@@ -3274,7 +3303,7 @@ var HermesApplier = class {
3274
3303
  try {
3275
3304
  await this.deleteConfigKeys(leaves.map(({ path }) => path));
3276
3305
  } catch (err) {
3277
- log$2.warn({
3306
+ log$4.warn({
3278
3307
  err: err instanceof Error ? err.message : String(err),
3279
3308
  integrationId
3280
3309
  }, "Failed to delete Alfe config keys from ~/.hermes/config.yaml");
@@ -3338,11 +3367,11 @@ var HermesApplier = class {
3338
3367
  * one in Phase 1, so this path is a real attempt (not faked) but untrodden.
3339
3368
  */
3340
3369
  async applyPlugin(spec) {
3341
- if (isOpenClawPlugin(spec)) {
3342
- log$2.info({ spec }, "Hermes applyPlugin: skipping OpenClaw-specific npm plugin (not a Hermes plugin)");
3370
+ if (isOpenClawPlugin$1(spec)) {
3371
+ log$4.info({ spec }, "Hermes applyPlugin: skipping OpenClaw-specific npm plugin (not a Hermes plugin)");
3343
3372
  return;
3344
3373
  }
3345
- log$2.info({ spec }, "Hermes applyPlugin: installing native Hermes plugin");
3374
+ log$4.info({ spec }, "Hermes applyPlugin: installing native Hermes plugin");
3346
3375
  await execFileAsync("hermes", [
3347
3376
  "plugins",
3348
3377
  "install",
@@ -3360,11 +3389,11 @@ var HermesApplier = class {
3360
3389
  * Hermes plugin would be disabled via `hermes plugins`.
3361
3390
  */
3362
3391
  async removePlugin(spec) {
3363
- if (isOpenClawPlugin(spec)) {
3364
- log$2.info({ spec }, "Hermes removePlugin: skipping OpenClaw-specific npm plugin (was never installed on Hermes)");
3392
+ if (isOpenClawPlugin$1(spec)) {
3393
+ log$4.info({ spec }, "Hermes removePlugin: skipping OpenClaw-specific npm plugin (was never installed on Hermes)");
3365
3394
  return;
3366
3395
  }
3367
- log$2.info({ spec }, "Hermes removePlugin: disabling native Hermes plugin");
3396
+ log$4.info({ spec }, "Hermes removePlugin: disabling native Hermes plugin");
3368
3397
  await execFileAsync("hermes", [
3369
3398
  "plugins",
3370
3399
  "disable",
@@ -3372,19 +3401,19 @@ var HermesApplier = class {
3372
3401
  ], { timeout: 3e4 });
3373
3402
  }
3374
3403
  applySkill(name) {
3375
- log$2.info({ name }, "Hermes applySkill: no-op (Hermes built-in skills; deferred)");
3404
+ log$4.info({ name }, "Hermes applySkill: no-op (Hermes built-in skills; deferred)");
3376
3405
  return Promise.resolve();
3377
3406
  }
3378
3407
  removeSkill(name) {
3379
- log$2.info({ name }, "Hermes removeSkill: no-op (Hermes built-in skills; deferred)");
3408
+ log$4.info({ name }, "Hermes removeSkill: no-op (Hermes built-in skills; deferred)");
3380
3409
  return Promise.resolve();
3381
3410
  }
3382
3411
  applyClawHubSkill(slug) {
3383
- log$2.info({ slug }, "Hermes applyClawHubSkill: no-op (ClawHub is OpenClaw-only)");
3412
+ log$4.info({ slug }, "Hermes applyClawHubSkill: no-op (ClawHub is OpenClaw-only)");
3384
3413
  return Promise.resolve();
3385
3414
  }
3386
3415
  removeClawHubSkill(slug) {
3387
- log$2.info({ slug }, "Hermes removeClawHubSkill: no-op (ClawHub is OpenClaw-only)");
3416
+ log$4.info({ slug }, "Hermes removeClawHubSkill: no-op (ClawHub is OpenClaw-only)");
3388
3417
  return Promise.resolve();
3389
3418
  }
3390
3419
  isAvailable() {
@@ -3501,7 +3530,7 @@ var HermesApplier = class {
3501
3530
  * `warmup`) so the store is a pure ledger and the MCP children are spawned ONLY
3502
3531
  * by Hermes — avoiding a double-spawn of every server.
3503
3532
  */
3504
- const log$1 = createLogger("HermesMcpSync");
3533
+ const log$3 = createLogger("HermesMcpSync");
3505
3534
  const DEFAULT_HERMES_HOME = join(homedir(), ".hermes");
3506
3535
  /**
3507
3536
  * SEAM #1 — `${VAR}` interpolation from `~/.hermes/.env`. CONFIRMED (Hermes
@@ -3513,7 +3542,7 @@ const DEFAULT_HERMES_HOME = join(homedir(), ".hermes");
3513
3542
  * resolves at spawn time. The inline-literal fallback is therefore not needed.
3514
3543
  */
3515
3544
  const ALFE_API_KEY_ENV_REF = "${ALFE_API_KEY}";
3516
- const DEFAULT_DEBOUNCE_MS = 250;
3545
+ const DEFAULT_DEBOUNCE_MS$1 = 250;
3517
3546
  /**
3518
3547
  * Read-merge-write mirror of the MCP store into `~/.hermes/config.yaml`.
3519
3548
  *
@@ -3550,7 +3579,7 @@ var HermesMcpSync = class {
3550
3579
  this.configPath = opts.configPath ?? join(this.home, "config.yaml");
3551
3580
  this.envPath = opts.envPath ?? join(this.home, ".env");
3552
3581
  this.trackingPath = opts.trackingPath ?? join(this.home, ".alfe-mcp-synced.json");
3553
- this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3582
+ this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS$1;
3554
3583
  }
3555
3584
  /**
3556
3585
  * Begin mirroring: load the prior removal set, run one immediate sync (so
@@ -3562,12 +3591,12 @@ var HermesMcpSync = class {
3562
3591
  this.started = true;
3563
3592
  this.loadSyncedIds();
3564
3593
  this.syncOnce().catch((err) => {
3565
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync: initial sync failed");
3594
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync: initial sync failed");
3566
3595
  });
3567
3596
  this.unsubscribe = this.manager.onChange(() => {
3568
3597
  this.schedule();
3569
3598
  });
3570
- log$1.info({ configPath: this.configPath }, "Hermes MCP sync started — mirroring store into config.yaml");
3599
+ log$3.info({ configPath: this.configPath }, "Hermes MCP sync started — mirroring store into config.yaml");
3571
3600
  }
3572
3601
  /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
3573
3602
  stop() {
@@ -3598,7 +3627,7 @@ var HermesMcpSync = class {
3598
3627
  schedule() {
3599
3628
  if (this.debounceMs <= 0) {
3600
3629
  this.syncOnce().catch((err) => {
3601
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync failed");
3630
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync failed");
3602
3631
  });
3603
3632
  return;
3604
3633
  }
@@ -3606,7 +3635,7 @@ var HermesMcpSync = class {
3606
3635
  this.debounceTimer = setTimeout(() => {
3607
3636
  this.debounceTimer = void 0;
3608
3637
  this.syncOnce().catch((err) => {
3609
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync failed");
3638
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync failed");
3610
3639
  });
3611
3640
  }, this.debounceMs);
3612
3641
  this.debounceTimer.unref();
@@ -3623,7 +3652,7 @@ var HermesMcpSync = class {
3623
3652
  if (changed) {
3624
3653
  mkdirSync(dirname(this.configPath), { recursive: true });
3625
3654
  writeFileSync(this.configPath, after, "utf-8");
3626
- log$1.info({
3655
+ log$3.info({
3627
3656
  added: [...desiredIds],
3628
3657
  removed: [...this.syncedIds].filter((id) => !desiredIds.has(id))
3629
3658
  }, "Hermes MCP sync: config.yaml mcp_servers updated");
@@ -3665,7 +3694,7 @@ var HermesMcpSync = class {
3665
3694
  /** Read-merge-write `~/.hermes/.env` to set ALFE_API_KEY without clobbering other keys. */
3666
3695
  ensureEnvApiKey() {
3667
3696
  if (!this.apiKey) {
3668
- log$1.warn("Hermes MCP sync: no ALFE_API_KEY available — mirrored MCP servers will start in zero-accounts degraded mode");
3697
+ log$3.warn("Hermes MCP sync: no ALFE_API_KEY available — mirrored MCP servers will start in zero-accounts degraded mode");
3669
3698
  return false;
3670
3699
  }
3671
3700
  return upsertEnvVar(this.envPath, "ALFE_API_KEY", this.apiKey);
@@ -3682,7 +3711,7 @@ var HermesMcpSync = class {
3682
3711
  mkdirSync(dirname(this.trackingPath), { recursive: true });
3683
3712
  writeFileSync(this.trackingPath, JSON.stringify({ syncedIds: [...this.syncedIds] }, null, 2) + "\n", "utf-8");
3684
3713
  } catch (err) {
3685
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync: failed to persist synced-id sidecar");
3714
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync: failed to persist synced-id sidecar");
3686
3715
  }
3687
3716
  }
3688
3717
  };
@@ -3713,6 +3742,466 @@ function upsertEnvVar(envPath, key, value) {
3713
3742
  }
3714
3743
  return changed;
3715
3744
  }
3745
+ function errMsg$2(err) {
3746
+ return err instanceof Error ? err.message : String(err);
3747
+ }
3748
+ //#endregion
3749
+ //#region src/appliers/claude-code-applier.ts
3750
+ /**
3751
+ * ClaudeCodeApplier — applies integration capability to the claude-code runtime
3752
+ * (a self-hosted Claude Code agent driven by the `alfe-claude-host`).
3753
+ *
3754
+ * The claude-code runtime is deliberately thin on the applier surface. Chat,
3755
+ * memory, and voice reach a claude-code agent via the host + MCP servers (see
3756
+ * `ClaudeCodeMcpSync`), NOT via applied plugins — so the core `alfe` integration
3757
+ * is effectively a no-op here. What DOES land:
3758
+ *
3759
+ * - **Plugins**: the `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
3760
+ * (they carry the `openclaw` extension / plugin-SDK peer dep) and cannot load
3761
+ * inside Claude Code — `applyPlugin` / `removePlugin` log-and-skip them, the
3762
+ * same as `HermesApplier`. No claude-code-native plugin spec exists.
3763
+ * - **Skills**: a REAL win vs Hermes — Claude Code has native Agent Skills read
3764
+ * from `~/.claude/skills/<name>/`. `applySkill` copies an Alfe skill directory
3765
+ * there; `applyClawHubSkill` fetches the ClawHub skill and stages it the same
3766
+ * way; the `remove*` variants delete the directory.
3767
+ * - **Config**: Claude Code has no integration-config CLI analogous to
3768
+ * `openclaw config set` / `hermes config set`, so config is a mostly-inert
3769
+ * surface. We track per-integration contributions in a ledger
3770
+ * (`~/.claude-code/.alfe-integrations.json`) so the lifecycle accounting stays
3771
+ * honest (a config-only integration is still lock-tracked and torn down on
3772
+ * removal), but there is no runtime CLI to shell. `setConfigRaw` /
3773
+ * `getConfigRaw` are ledger-backed raw key writes for the daemon's
3774
+ * `alfe.config_set` cloud-command — they MUST exist (the daemon hard-fails
3775
+ * `alfe.config_set` and the config reconciler no-ops when the applier omits
3776
+ * them), and they degrade cleanly rather than error.
3777
+ *
3778
+ * Constructor shape mirrors `HermesApplier` (`{ home, workspace?, configPath? }`)
3779
+ * so the daemon constructs it identically. `home` is the `~/.claude-code`
3780
+ * workspace; skills go to `~/.claude/skills` (Claude Code's global skills dir,
3781
+ * derived from the OS home, NOT the claude-code workspace).
3782
+ */
3783
+ const log$2 = createLogger("ClaudeCodeApplier");
3784
+ const DEFAULT_CLAUDE_CODE_HOME$1 = join(homedir(), ".claude-code");
3785
+ /** Claude Code reads native Agent Skills from `~/.claude/skills/<name>/`. */
3786
+ const DEFAULT_CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
3787
+ /** `@alfe.ai/openclaw-*` packages are OpenClaw-specific plugins — not Claude Code. */
3788
+ function isOpenClawPlugin(spec) {
3789
+ return stripPluginVersion(spec).startsWith("@alfe.ai/openclaw-");
3790
+ }
3791
+ var ClaudeCodeApplier = class {
3792
+ runtime = "claude-code";
3793
+ home;
3794
+ /** `~/.claude-code/.alfe-integrations.json` — per-integration config ledger + raw-key store. */
3795
+ ledgerPath;
3796
+ /** `~/.claude/skills` — where native Claude Code skills are copied. */
3797
+ skillsDir;
3798
+ constructor(options = {}) {
3799
+ this.home = options.home ?? options.workspace ?? DEFAULT_CLAUDE_CODE_HOME$1;
3800
+ this.ledgerPath = options.configPath ?? join(this.home, ".alfe-integrations.json");
3801
+ this.skillsDir = options.skillsDir ?? DEFAULT_CLAUDE_SKILLS_DIR;
3802
+ }
3803
+ /**
3804
+ * Record an integration's config contribution in the ledger. Claude Code has
3805
+ * no integration-config CLI, so this is a pure ledger write — it makes the
3806
+ * per-integration accounting honest (so `removeConfig` and the lock's
3807
+ * config-only tracking work) without shelling anything. Overwrites the prior
3808
+ * contribution for this integration (convergent by construction — a full
3809
+ * replace, no stale-key diff needed since nothing is applied to a runtime).
3810
+ */
3811
+ applyConfig(integrationId, config) {
3812
+ const ledger = this.readLedger();
3813
+ const integrations = ledger._integrations ?? {};
3814
+ integrations[integrationId] = config;
3815
+ ledger._integrations = integrations;
3816
+ this.writeLedger(ledger);
3817
+ log$2.info({ integrationId }, "Claude Code applyConfig: recorded config in ledger (no runtime CLI to apply)");
3818
+ return Promise.resolve();
3819
+ }
3820
+ /** Clear an integration's config contribution from the ledger. */
3821
+ removeConfig(integrationId) {
3822
+ const ledger = this.readLedger();
3823
+ const integrations = ledger._integrations ?? {};
3824
+ if (!(integrationId in integrations)) return Promise.resolve();
3825
+ ledger._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
3826
+ this.writeLedger(ledger);
3827
+ return Promise.resolve();
3828
+ }
3829
+ /**
3830
+ * Raw single-key config write for the daemon's `alfe.config_set` cloud-command.
3831
+ *
3832
+ * Deliberately bypasses the `_integrations` per-integration accounting (a raw
3833
+ * set is untracked — routing it through `applyConfig` would corrupt an
3834
+ * integration's removal accounting). Stored under a separate `_raw` map in the
3835
+ * same ledger file. Inert as far as the runtime goes (Claude Code has no config
3836
+ * CLI to push it into), but it degrades cleanly — the daemon gets an OK ack
3837
+ * instead of `CONFIG_SET_UNSUPPORTED`, and the value round-trips via
3838
+ * `getConfigRaw`.
3839
+ */
3840
+ setConfigRaw(key, value) {
3841
+ const ledger = this.readLedger();
3842
+ const raw = ledger._raw ?? {};
3843
+ raw[key] = value;
3844
+ ledger._raw = raw;
3845
+ this.writeLedger(ledger);
3846
+ return Promise.resolve();
3847
+ }
3848
+ /**
3849
+ * Read a raw config key back from the ledger — the untracked read companion to
3850
+ * `setConfigRaw`, used by the daemon's config-reconcile pass (value-diff before
3851
+ * write; verify a write landed). Returns the exact string `setConfigRaw` stored,
3852
+ * or `undefined` when the key was never set. Never throws.
3853
+ */
3854
+ getConfigRaw(key) {
3855
+ try {
3856
+ const value = (this.readLedger()._raw ?? {})[key];
3857
+ return Promise.resolve(typeof value === "string" ? value : void 0);
3858
+ } catch {
3859
+ return Promise.resolve(void 0);
3860
+ }
3861
+ }
3862
+ /**
3863
+ * Apply a plugin. The `@alfe.ai/openclaw-*` npm plugins are OpenClaw-specific
3864
+ * and do NOT load in Claude Code — log + skip them. No claude-code-native
3865
+ * plugin spec exists, so any non-openclaw spec is also skipped (rather than a
3866
+ * blind shell of an unknown installer). The manager catches per-plugin, so a
3867
+ * skip here is a correct no-op.
3868
+ */
3869
+ applyPlugin(spec) {
3870
+ if (isOpenClawPlugin(spec)) {
3871
+ log$2.info({ spec }, "Claude Code applyPlugin: skipping OpenClaw-specific npm plugin (not a Claude Code plugin)");
3872
+ return Promise.resolve();
3873
+ }
3874
+ log$2.info({ spec }, "Claude Code applyPlugin: no claude-code-native plugin mechanism — skipping");
3875
+ return Promise.resolve();
3876
+ }
3877
+ /** Remove a plugin. Nothing was installed (applyPlugin skipped it) — no-op log. */
3878
+ removePlugin(spec) {
3879
+ log$2.info({ spec }, "Claude Code removePlugin: no-op (no plugin was installed on Claude Code)");
3880
+ return Promise.resolve();
3881
+ }
3882
+ /**
3883
+ * Copy a skill directory into `~/.claude/skills/<name>/`. Claude Code reads
3884
+ * native Agent Skills from there, so an Alfe skill dir lands as a first-class
3885
+ * Claude Code skill. Idempotent: an existing target is replaced (a
3886
+ * newer-version re-apply must overwrite).
3887
+ */
3888
+ applySkill(name, srcPath) {
3889
+ return this.copySkillDir(name, srcPath);
3890
+ }
3891
+ /** Remove a skill directory from `~/.claude/skills/<name>/`. */
3892
+ removeSkill(name) {
3893
+ return this.deleteSkillDir(name);
3894
+ }
3895
+ /**
3896
+ * Install a ClawHub skill natively. ClawHub is an OpenClaw registry, but the
3897
+ * skill payload is a plain directory — for claude-code we treat the slug as the
3898
+ * skill name and expect the caller to have staged the directory. Absent a
3899
+ * staged source we log + skip (no OpenClaw `skills install` CLI on this
3900
+ * runtime); a future claude-code ClawHub fetch would stage the dir then copy it
3901
+ * exactly like `applySkill`.
3902
+ *
3903
+ * NOTE: unlike OpenClaw's `applyClawHubSkill` (which THROWS on a real install
3904
+ * failure so an all-skills-fail activation is marked `error`), there is no
3905
+ * install to fail here — the skill either lands from a staged directory or is a
3906
+ * clean no-op. We never throw for a claude-code ClawHub skill.
3907
+ */
3908
+ applyClawHubSkill(slug) {
3909
+ log$2.info({ slug }, "Claude Code applyClawHubSkill: no claude-code ClawHub fetch — skipping (native skill dirs land via applySkill)");
3910
+ return Promise.resolve();
3911
+ }
3912
+ /** Remove a ClawHub-installed skill directory (by slug == dir name). */
3913
+ removeClawHubSkill(slug) {
3914
+ return this.deleteSkillDir(slug);
3915
+ }
3916
+ isAvailable() {
3917
+ return Promise.resolve(existsSync(this.home));
3918
+ }
3919
+ copySkillDir(name, srcPath) {
3920
+ if (!existsSync(srcPath)) {
3921
+ log$2.warn({
3922
+ name,
3923
+ srcPath
3924
+ }, "Claude Code applySkill: source directory missing — skipping");
3925
+ return Promise.resolve();
3926
+ }
3927
+ const dest = join(this.skillsDir, name);
3928
+ try {
3929
+ mkdirSync(this.skillsDir, { recursive: true });
3930
+ rmSync(dest, {
3931
+ recursive: true,
3932
+ force: true
3933
+ });
3934
+ cpSync(srcPath, dest, { recursive: true });
3935
+ log$2.info({
3936
+ name,
3937
+ dest
3938
+ }, "Claude Code applySkill: copied skill into ~/.claude/skills");
3939
+ } catch (err) {
3940
+ const message = err instanceof Error ? err.message : String(err);
3941
+ log$2.error({
3942
+ name,
3943
+ dest,
3944
+ err: message
3945
+ }, "Claude Code applySkill: failed to copy skill");
3946
+ throw new Error(`Failed to copy skill ${name} into ${dest}: ${message}`);
3947
+ }
3948
+ return Promise.resolve();
3949
+ }
3950
+ deleteSkillDir(name) {
3951
+ const dest = join(this.skillsDir, name);
3952
+ try {
3953
+ rmSync(dest, {
3954
+ recursive: true,
3955
+ force: true
3956
+ });
3957
+ log$2.info({
3958
+ name,
3959
+ dest
3960
+ }, "Claude Code removeSkill: removed skill from ~/.claude/skills");
3961
+ } catch (err) {
3962
+ log$2.warn({
3963
+ name,
3964
+ dest,
3965
+ err: err instanceof Error ? err.message : String(err)
3966
+ }, "Claude Code removeSkill: failed to remove skill directory");
3967
+ }
3968
+ return Promise.resolve();
3969
+ }
3970
+ readLedger() {
3971
+ if (!existsSync(this.ledgerPath)) return {};
3972
+ try {
3973
+ return JSON.parse(readFileSync(this.ledgerPath, "utf-8"));
3974
+ } catch {
3975
+ return {};
3976
+ }
3977
+ }
3978
+ writeLedger(ledger) {
3979
+ mkdirSync(join(this.ledgerPath, ".."), { recursive: true });
3980
+ writeFileSync(this.ledgerPath, JSON.stringify(ledger, null, 2) + "\n", "utf-8");
3981
+ }
3982
+ };
3983
+ //#endregion
3984
+ //#region src/appliers/claude-code-mcp-sync.ts
3985
+ /**
3986
+ * ClaudeCodeMcpSync — claude-code-only CONSUMER of the runtime-agnostic MCP
3987
+ * store (parallel to `HermesMcpSync`, Approach B: a generated config file the
3988
+ * runtime reads).
3989
+ *
3990
+ * Background: `McpApplier` writes resolved MCP servers (command/args/env, with
3991
+ * `{{config}}`/`{{credentials}}` already interpolated at apply time) into the
3992
+ * runtime-agnostic bundler store at `~/.alfe/mcp/servers.json`. OpenClaw consumes
3993
+ * that store over IPC (the daemon hosts the bundler children and the openclaw
3994
+ * plugin reaches them). The claude-code runtime cannot consume over IPC — the
3995
+ * `alfe-claude-host` spawns `claude -p --mcp-config <file>` per turn and Claude
3996
+ * Code itself spawns the MCP children. So claude-code needs its own consumer that
3997
+ * materialises the store into the `--mcp-config` JSON file the host passes.
3998
+ *
3999
+ * This class is the parallel to OpenClaw's IPC consumption: it subscribes to
4000
+ * `manager.onChange()` and, **only when the active runtime is claude-code** (the
4001
+ * daemon only constructs it for claude-code agents), writes the store's servers
4002
+ * into `~/.claude-code/mcp-config.json` in Claude Code's `--mcp-config` shape:
4003
+ *
4004
+ * { "mcpServers": { "<id>": { "command", "args", "env" } | { "url", ... } } }
4005
+ *
4006
+ * This is Anthropic's documented `.mcp.json` / `--mcp-config` schema — `command`
4007
+ * / `args` / `env` for stdio servers and `type: "sse"|"http"` + `url` + `headers`
4008
+ * for remote servers, under a top-level `mcpServers` map keyed by server id.
4009
+ *
4010
+ * Two cross-cutting responsibilities make the generated servers actually work:
4011
+ *
4012
+ * 1. `ALFE_API_KEY` injection. The Alfe MCP servers (`@alfe.ai/<provider>-mcp`)
4013
+ * fetch their real provider credentials from the Alfe API at startup using
4014
+ * `ALFE_API_KEY`. For OpenClaw the daemon spawns the children, so they
4015
+ * inherit `ALFE_API_KEY` from the daemon's own `process.env`. Claude Code
4016
+ * spawns the children itself in a separate process tree (per turn), so they
4017
+ * would NOT inherit it — without it they start in a silent zero-accounts
4018
+ * degraded mode. We therefore inject the actual `ALFE_API_KEY` value into
4019
+ * every generated stdio server's env. Unlike Hermes (which supports
4020
+ * `${VAR}` interpolation from `~/.hermes/.env`), Claude Code's `--mcp-config`
4021
+ * env block is a plain map with NO documented `${VAR}` expansion, so we write
4022
+ * the literal secret into the generated file. The file lives under
4023
+ * `~/.claude-code/` (0600) alongside other agent state and is regenerated,
4024
+ * never checked in.
4025
+ *
4026
+ * 2. Restart. Claude Code reads `--mcp-config` fresh on every `claude -p` spawn
4027
+ * (the host spawns per turn), so a regenerated file is picked up on the NEXT
4028
+ * turn with no runtime restart. Unlike Hermes (which reads config.yaml at
4029
+ * boot and needs a reload), this sync does NOT trigger a runtime restart —
4030
+ * the per-turn spawn model makes it unnecessary. This is the deliberate
4031
+ * divergence from `HermesMcpSync` (which owns a `requestRestart` callback);
4032
+ * we keep the same options shape for daemon-wiring parity but never restart.
4033
+ */
4034
+ const log$1 = createLogger("ClaudeCodeMcpSync");
4035
+ const DEFAULT_CLAUDE_CODE_HOME = join(homedir(), ".claude-code");
4036
+ const DEFAULT_DEBOUNCE_MS = 250;
4037
+ /**
4038
+ * Full-regenerate writer of the MCP store into `~/.claude-code/mcp-config.json`.
4039
+ *
4040
+ * Ownership / removal model: every entry returned by `manager.listServers()`
4041
+ * comes from the Alfe store (its `owner` is `integration:<id>` / `cli` /
4042
+ * `manual`) and is therefore Alfe-owned. Unlike the Hermes config.yaml mirror
4043
+ * (which must preserve user-authored `mcp_servers` in a shared file), the
4044
+ * `--mcp-config` file is Alfe-generated and Alfe-owned end to end — Claude Code's
4045
+ * OWN user-authored MCP servers live in `~/.claude.json` / project `.mcp.json`,
4046
+ * NOT this generated file. So we fully regenerate it from the store on every
4047
+ * change (no read-merge-write needed). We still persist the written-id set to a
4048
+ * sidecar for parity + forensics, but removal is implicit in the regenerate.
4049
+ */
4050
+ var ClaudeCodeMcpSync = class {
4051
+ manager;
4052
+ home;
4053
+ apiKey;
4054
+ configPath;
4055
+ trackingPath;
4056
+ debounceMs;
4057
+ /** Ids of `mcpServers` entries this sync last wrote — informational sidecar. */
4058
+ syncedIds = /* @__PURE__ */ new Set();
4059
+ /** Serializes syncs so two onChange-driven runs can't interleave file writes. */
4060
+ queue = Promise.resolve();
4061
+ unsubscribe;
4062
+ debounceTimer;
4063
+ started = false;
4064
+ constructor(opts) {
4065
+ this.manager = opts.manager;
4066
+ this.home = opts.home ?? DEFAULT_CLAUDE_CODE_HOME;
4067
+ this.apiKey = opts.apiKey;
4068
+ this.configPath = opts.configPath ?? join(this.home, "mcp-config.json");
4069
+ this.trackingPath = opts.trackingPath ?? join(this.home, ".alfe-mcp-synced.json");
4070
+ this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
4071
+ }
4072
+ /**
4073
+ * Begin mirroring: load the prior removal set, run one immediate sync (so the
4074
+ * `--mcp-config` file reflects the current store before the host next spawns
4075
+ * `claude -p`), then subscribe to store changes (debounced). Idempotent.
4076
+ */
4077
+ start() {
4078
+ if (this.started) return;
4079
+ this.started = true;
4080
+ this.loadSyncedIds();
4081
+ this.syncOnce().catch((err) => {
4082
+ log$1.warn({ err: errMsg$1(err) }, "Claude Code MCP sync: initial sync failed");
4083
+ });
4084
+ this.unsubscribe = this.manager.onChange(() => {
4085
+ this.schedule();
4086
+ });
4087
+ log$1.info({ configPath: this.configPath }, "Claude Code MCP sync started — mirroring store into mcp-config.json");
4088
+ }
4089
+ /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
4090
+ stop() {
4091
+ if (this.unsubscribe) {
4092
+ this.unsubscribe();
4093
+ this.unsubscribe = void 0;
4094
+ }
4095
+ if (this.debounceTimer) {
4096
+ clearTimeout(this.debounceTimer);
4097
+ this.debounceTimer = void 0;
4098
+ }
4099
+ this.started = false;
4100
+ }
4101
+ /**
4102
+ * Regenerate the `--mcp-config` file from the current store exactly once.
4103
+ * Public so the daemon (and tests) can await a deterministic sync. Serialized
4104
+ * against any other in-flight sync.
4105
+ */
4106
+ syncOnce() {
4107
+ const run = () => {
4108
+ this.syncNow();
4109
+ return Promise.resolve();
4110
+ };
4111
+ const result = this.queue.then(run, run);
4112
+ this.queue = result.catch(() => void 0);
4113
+ return result;
4114
+ }
4115
+ schedule() {
4116
+ if (this.debounceMs <= 0) {
4117
+ this.syncOnce().catch((err) => {
4118
+ log$1.warn({ err: errMsg$1(err) }, "Claude Code MCP sync failed");
4119
+ });
4120
+ return;
4121
+ }
4122
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
4123
+ this.debounceTimer = setTimeout(() => {
4124
+ this.debounceTimer = void 0;
4125
+ this.syncOnce().catch((err) => {
4126
+ log$1.warn({ err: errMsg$1(err) }, "Claude Code MCP sync failed");
4127
+ });
4128
+ }, this.debounceMs);
4129
+ this.debounceTimer.unref();
4130
+ }
4131
+ syncNow() {
4132
+ const desired = this.computeDesired();
4133
+ const desiredIds = new Set(desired.keys());
4134
+ if (desiredIds.size === 0 && this.syncedIds.size === 0 && !existsSync(this.configPath)) return;
4135
+ const file = { mcpServers: Object.fromEntries(desired) };
4136
+ const next = JSON.stringify(file, null, 2) + "\n";
4137
+ if ((existsSync(this.configPath) ? readFileSync(this.configPath, "utf-8") : "") !== next) {
4138
+ mkdirSync(dirname(this.configPath), { recursive: true });
4139
+ writeFileSync(this.configPath, next, {
4140
+ encoding: "utf-8",
4141
+ mode: 384
4142
+ });
4143
+ chmodSync(this.configPath, 384);
4144
+ log$1.info({
4145
+ added: [...desiredIds],
4146
+ removed: [...this.syncedIds].filter((id) => !desiredIds.has(id))
4147
+ }, "Claude Code MCP sync: mcp-config.json regenerated");
4148
+ }
4149
+ this.syncedIds = desiredIds;
4150
+ this.persistSyncedIds();
4151
+ }
4152
+ computeDesired() {
4153
+ const desired = /* @__PURE__ */ new Map();
4154
+ for (const { id, entry } of this.manager.listServers()) desired.set(id, this.toClaudeCodeEntry(entry));
4155
+ return desired;
4156
+ }
4157
+ /**
4158
+ * Transform a stored entry into a Claude Code `--mcp-config` entry. stdio
4159
+ * entries get the literal `ALFE_API_KEY` injected; remote entries pass through
4160
+ * with the documented `type` discriminator (`sse` → `sse`, `streamable-http`
4161
+ * → `http`).
4162
+ */
4163
+ toClaudeCodeEntry(entry) {
4164
+ const cfg = toServerConfig(entry);
4165
+ if ("command" in cfg) {
4166
+ const out = { command: cfg.command };
4167
+ if (cfg.args && cfg.args.length > 0) out.args = cfg.args;
4168
+ const env = this.withAlfeApiKey(cfg.env);
4169
+ if (Object.keys(env).length > 0) out.env = env;
4170
+ return out;
4171
+ }
4172
+ const out = {
4173
+ type: cfg.transport === "sse" ? "sse" : "http",
4174
+ url: cfg.url
4175
+ };
4176
+ if (cfg.headers) out.headers = cfg.headers;
4177
+ return out;
4178
+ }
4179
+ withAlfeApiKey(env) {
4180
+ const merged = { ...env ?? {} };
4181
+ if ("ALFE_API_KEY" in merged) return merged;
4182
+ if (!this.apiKey) {
4183
+ log$1.warn("Claude Code MCP sync: no ALFE_API_KEY available — generated MCP servers will start in zero-accounts degraded mode");
4184
+ return merged;
4185
+ }
4186
+ merged.ALFE_API_KEY = this.apiKey;
4187
+ return merged;
4188
+ }
4189
+ loadSyncedIds() {
4190
+ if (!existsSync(this.trackingPath)) return;
4191
+ try {
4192
+ const data = JSON.parse(readFileSync(this.trackingPath, "utf-8"));
4193
+ if (Array.isArray(data.syncedIds)) this.syncedIds = new Set(data.syncedIds.filter((x) => typeof x === "string"));
4194
+ } catch {}
4195
+ }
4196
+ persistSyncedIds() {
4197
+ try {
4198
+ mkdirSync(dirname(this.trackingPath), { recursive: true });
4199
+ writeFileSync(this.trackingPath, JSON.stringify({ syncedIds: [...this.syncedIds] }, null, 2) + "\n", "utf-8");
4200
+ } catch (err) {
4201
+ log$1.warn({ err: errMsg$1(err) }, "Claude Code MCP sync: failed to persist synced-id sidecar");
4202
+ }
4203
+ }
4204
+ };
3716
4205
  function errMsg$1(err) {
3717
4206
  return err instanceof Error ? err.message : String(err);
3718
4207
  }
@@ -3931,4 +4420,4 @@ var IntegrationManagerAdapter = class {
3931
4420
  }
3932
4421
  };
3933
4422
  //#endregion
3934
- export { DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, NoopOpenClawCliLock, OpenClawApplier, Registry, RegistryResolveError, Resolver, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
4423
+ export { ClaudeCodeApplier, ClaudeCodeMcpSync, DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, NoopOpenClawCliLock, OpenClawApplier, Registry, RegistryResolveError, Resolver, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "@auriclabs/logger": "^0.1.1",
16
16
  "yaml": ">=2.8.3",
17
17
  "@alfe.ai/integration-manifest": "^0.3.1",
18
- "@alfe.ai/mcp-bundler": "^0.3.0"
18
+ "@alfe.ai/mcp-bundler": "^0.3.1"
19
19
  },
20
20
  "files": [
21
21
  "dist"