@alfe.ai/integrations 0.4.2 → 0.5.1

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) {
@@ -2098,7 +2098,7 @@ function partitionEntries(entries) {
2098
2098
  * is stored in a separate tracking file (config.json) for clean removal.
2099
2099
  */
2100
2100
  const execFileAsync$1 = promisify(execFile);
2101
- const log$3 = createLogger("OpenClawApplier");
2101
+ const log$5 = createLogger("OpenClawApplier");
2102
2102
  const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
2103
2103
  /**
2104
2104
  * Bundled OpenClaw plugins that ship inside the runtime (not installed as npm
@@ -2509,7 +2509,7 @@ var OpenClawApplier = class {
2509
2509
  return;
2510
2510
  } catch (err) {
2511
2511
  if (!isMalformedStateDbError(err)) throw err;
2512
- 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");
2513
2513
  if (!await this.healMalformedStateDb()) throw err;
2514
2514
  await execFileAsync$1("openclaw", args, opts);
2515
2515
  }
@@ -2535,7 +2535,7 @@ var OpenClawApplier = class {
2535
2535
  async healMalformedStateDb() {
2536
2536
  const now = Date.now();
2537
2537
  if (now - this.lastHealAt < HEAL_MIN_INTERVAL_MS) {
2538
- 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");
2539
2539
  return false;
2540
2540
  }
2541
2541
  this.lastHealAt = now;
@@ -2555,19 +2555,19 @@ var OpenClawApplier = class {
2555
2555
  quarantined.push(dest);
2556
2556
  }
2557
2557
  if (quarantined.length === 0) {
2558
- 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");
2559
2559
  this.lastHealAt = now - HEAL_MIN_INTERVAL_MS + FAILED_HEAL_COOLDOWN_MS;
2560
2560
  return false;
2561
2561
  }
2562
- log$3.warn({
2562
+ log$5.warn({
2563
2563
  stateDir,
2564
2564
  quarantined
2565
2565
  }, "quarantined malformed openclaw state DB — recreating via `openclaw plugins list` (upstream openclaw/openclaw#71689)");
2566
2566
  await execFileAsync$1("openclaw", ["plugins", "list"], { timeout: 9e4 });
2567
- log$3.warn({ stateDir }, "recreated openclaw state DB after quarantine");
2567
+ log$5.warn({ stateDir }, "recreated openclaw state DB after quarantine");
2568
2568
  return true;
2569
2569
  } catch (err) {
2570
- log$3.warn({
2570
+ log$5.warn({
2571
2571
  stateDir,
2572
2572
  quarantined,
2573
2573
  err: err instanceof Error ? err.message : String(err)
@@ -2584,7 +2584,7 @@ var OpenClawApplier = class {
2584
2584
  const pinnedVersion = pluginSpecVersion(spec);
2585
2585
  const claim = this.appliedPluginPins.get(pkg);
2586
2586
  if (pinnedVersion !== void 0 && claim !== void 0 && claim.version !== pinnedVersion && claim.integrationId !== opts?.integrationId) {
2587
- log$3.warn({
2587
+ log$5.warn({
2588
2588
  pkg,
2589
2589
  requestedVersion: pinnedVersion,
2590
2590
  keptVersion: claim.version,
@@ -2599,7 +2599,7 @@ var OpenClawApplier = class {
2599
2599
  const installedVersion = this.installedPluginVersion(pkg);
2600
2600
  const versionsComparable = pinnedVersion !== void 0 && installedVersion !== void 0;
2601
2601
  if (versionsComparable && installedVersion === pinnedVersion) {
2602
- if (opts?.force) log$3.info({
2602
+ if (opts?.force) log$5.info({
2603
2603
  pkg,
2604
2604
  spec,
2605
2605
  version: installedVersion
@@ -2611,7 +2611,7 @@ var OpenClawApplier = class {
2611
2611
  this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
2612
2612
  return;
2613
2613
  }
2614
- log$3.info({
2614
+ log$5.info({
2615
2615
  pkg,
2616
2616
  spec,
2617
2617
  installedVersion,
@@ -2621,7 +2621,7 @@ var OpenClawApplier = class {
2621
2621
  try {
2622
2622
  await this.removePluginUnlocked(pkg);
2623
2623
  } catch (err) {
2624
- log$3.warn({
2624
+ log$5.warn({
2625
2625
  pkg,
2626
2626
  spec,
2627
2627
  err: err instanceof Error ? err.message : String(err)
@@ -2643,7 +2643,7 @@ var OpenClawApplier = class {
2643
2643
  } catch (err) {
2644
2644
  const errText = err instanceof Error ? `${err.message}\n${err.stderr ?? ""}` : String(err);
2645
2645
  if (useUnsafeFlag && errText.includes("unknown option")) {
2646
- 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");
2647
2647
  await this.execOpenClawHealing(baseArgs, { timeout: 6e4 });
2648
2648
  } else throw err;
2649
2649
  }
@@ -2652,7 +2652,7 @@ var OpenClawApplier = class {
2652
2652
  setTimeout(r, 500);
2653
2653
  });
2654
2654
  if (!this.isPluginInstalled(pkg)) throw err;
2655
- log$3.warn({
2655
+ log$5.warn({
2656
2656
  pkg,
2657
2657
  spec,
2658
2658
  err: err instanceof Error ? err.message : String(err)
@@ -2715,7 +2715,7 @@ var OpenClawApplier = class {
2715
2715
  const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
2716
2716
  if (missing.length === 0) return;
2717
2717
  if (!readTrustworthy) {
2718
- log$3.warn({
2718
+ log$5.warn({
2719
2719
  pkgs: wanted,
2720
2720
  missing
2721
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");
@@ -2729,7 +2729,7 @@ var OpenClawApplier = class {
2729
2729
  "--replace"
2730
2730
  ]);
2731
2731
  } catch (err) {
2732
- log$3.warn({
2732
+ log$5.warn({
2733
2733
  err: err instanceof Error ? err.message : String(err),
2734
2734
  pkgs: wanted
2735
2735
  }, "Failed to set plugins.allow via openclaw config set");
@@ -2814,12 +2814,12 @@ var OpenClawApplier = class {
2814
2814
  recursive: true,
2815
2815
  force: true
2816
2816
  });
2817
- log$3.info({
2817
+ log$5.info({
2818
2818
  pkg,
2819
2819
  removed: fullPath
2820
2820
  }, "Removed untracked extensions/ install — will reinstall via npm path");
2821
2821
  } catch (err) {
2822
- log$3.warn({
2822
+ log$5.warn({
2823
2823
  pkg,
2824
2824
  removed: fullPath,
2825
2825
  err: err instanceof Error ? err.message : String(err)
@@ -2853,21 +2853,21 @@ var OpenClawApplier = class {
2853
2853
  }
2854
2854
  applyClawHubSkill(slug) {
2855
2855
  return this.cliLock.run(async () => {
2856
- log$3.info({ slug }, "Installing skill from ClawHub");
2856
+ log$5.info({ slug }, "Installing skill from ClawHub");
2857
2857
  try {
2858
2858
  await execFileAsync$1("openclaw", [
2859
2859
  "skills",
2860
2860
  "install",
2861
2861
  slug
2862
2862
  ], { timeout: 6e4 });
2863
- log$3.info({ slug }, "ClawHub skill installed");
2863
+ log$5.info({ slug }, "ClawHub skill installed");
2864
2864
  } catch (err) {
2865
2865
  const msg = err instanceof Error ? err.message : String(err);
2866
2866
  if (msg.includes("already exists") || msg.includes("Skill already exists")) {
2867
- log$3.info({ slug }, "ClawHub skill already installed (skipped)");
2867
+ log$5.info({ slug }, "ClawHub skill already installed (skipped)");
2868
2868
  return;
2869
2869
  }
2870
- log$3.error({
2870
+ log$5.error({
2871
2871
  slug,
2872
2872
  err: msg
2873
2873
  }, "ClawHub skill install failed");
@@ -2882,7 +2882,7 @@ var OpenClawApplier = class {
2882
2882
  recursive: true,
2883
2883
  force: true
2884
2884
  });
2885
- log$3.info({ slug }, "ClawHub skill removed");
2885
+ log$5.info({ slug }, "ClawHub skill removed");
2886
2886
  }
2887
2887
  return Promise.resolve();
2888
2888
  }
@@ -2937,10 +2937,10 @@ var OpenClawApplier = class {
2937
2937
  const after = await readParentObject(parentPath);
2938
2938
  return [...dottedKvs.entries()].every(([k, v]) => deepSubset(v, after[k]));
2939
2939
  })) {
2940
- 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");
2941
2941
  continue;
2942
2942
  }
2943
- log$3.error({
2943
+ log$5.error({
2944
2944
  err: err instanceof Error ? err.message : String(err),
2945
2945
  parentPath
2946
2946
  }, "Failed to set config subtree via openclaw config set");
@@ -2956,9 +2956,9 @@ var OpenClawApplier = class {
2956
2956
  } catch (err) {
2957
2957
  if (await this.verifyApplied(async () => {
2958
2958
  return (await mapWithConcurrency(leaves, VERIFY_GET_CONCURRENCY, (l) => this.configValueMatches(l.path, l.value))).every(Boolean);
2959
- })) 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");
2960
2960
  else {
2961
- log$3.error({
2961
+ log$5.error({
2962
2962
  err: err instanceof Error ? err.message : String(err),
2963
2963
  batch: leaves
2964
2964
  }, "Failed to set config via openclaw config set --batch-json");
@@ -3010,7 +3010,7 @@ var OpenClawApplier = class {
3010
3010
  "--replace"
3011
3011
  ]);
3012
3012
  } catch (err) {
3013
- log$3.warn({
3013
+ log$5.warn({
3014
3014
  err: err instanceof Error ? err.message : String(err),
3015
3015
  parentPath
3016
3016
  }, "Failed to update parent config during subtree key drop");
@@ -3048,19 +3048,19 @@ var OpenClawApplier = class {
3048
3048
  unsetParents.add(parentPath);
3049
3049
  try {
3050
3050
  await this.runConfigCommandUnlocked(["unset", parentPath]);
3051
- log$3.warn({
3051
+ log$5.warn({
3052
3052
  path,
3053
3053
  parentPath
3054
3054
  }, "Leaf unset rejected as an invalid partial custom provider — unset the parent subtree instead");
3055
3055
  } catch (parentErr) {
3056
- log$3.warn({
3056
+ log$5.warn({
3057
3057
  err: parentErr instanceof Error ? parentErr.message : String(parentErr),
3058
3058
  parentPath
3059
3059
  }, "Failed to unset parent subtree after an invalid-partial leaf unset");
3060
3060
  }
3061
3061
  return;
3062
3062
  }
3063
- log$3.warn({
3063
+ log$5.warn({
3064
3064
  err: err instanceof Error ? err.message : String(err),
3065
3065
  path
3066
3066
  }, "Failed to unset config via openclaw config unset");
@@ -3166,7 +3166,7 @@ var OpenClawApplier = class {
3166
3166
  * config + plugins only.
3167
3167
  */
3168
3168
  const execFileAsync = promisify(execFile);
3169
- const log$2 = createLogger("HermesApplier");
3169
+ const log$4 = createLogger("HermesApplier");
3170
3170
  const DEFAULT_HERMES_HOME$1 = join(homedir(), ".hermes");
3171
3171
  /**
3172
3172
  * Hermes config writes are serialized through one promise chain so concurrent
@@ -3184,7 +3184,7 @@ const delay = (ms) => new Promise((resolve) => {
3184
3184
  setTimeout(resolve, ms);
3185
3185
  });
3186
3186
  /** `@alfe.ai/openclaw-*` packages are OpenClaw-specific plugins — not Hermes. */
3187
- function isOpenClawPlugin(spec) {
3187
+ function isOpenClawPlugin$1(spec) {
3188
3188
  return stripPluginVersion(spec).startsWith("@alfe.ai/openclaw-");
3189
3189
  }
3190
3190
  /**
@@ -3256,7 +3256,7 @@ var HermesApplier = class {
3256
3256
  if (staleLeafPaths.length > 0) try {
3257
3257
  await this.deleteConfigKeys(staleLeafPaths);
3258
3258
  } catch (err) {
3259
- log$2.warn({
3259
+ log$4.warn({
3260
3260
  err: err instanceof Error ? err.message : String(err),
3261
3261
  integrationId
3262
3262
  }, "Failed to delete stale Hermes config keys during applyConfig diff");
@@ -3265,7 +3265,7 @@ var HermesApplier = class {
3265
3265
  const nextKvs = subtreesByParent.get(parent);
3266
3266
  return [...prevKvs.keys()].some((k) => !nextKvs?.has(k));
3267
3267
  }).map(([parent]) => parent);
3268
- if (staleSubtreeParents.length > 0) log$2.warn({
3268
+ if (staleSubtreeParents.length > 0) log$4.warn({
3269
3269
  integrationId,
3270
3270
  parents: staleSubtreeParents
3271
3271
  }, "Hermes applyConfig diff: skipping stale dotted-key subtree(s) — OpenClaw-plugin-shaped config does not apply to Hermes");
@@ -3273,14 +3273,14 @@ var HermesApplier = class {
3273
3273
  integrations[integrationId] = config;
3274
3274
  tracking._integrations = integrations;
3275
3275
  this.writeTracking(tracking);
3276
- if (subtreesByParent.size > 0) log$2.warn({
3276
+ if (subtreesByParent.size > 0) log$4.warn({
3277
3277
  integrationId,
3278
3278
  parents: [...subtreesByParent.keys()]
3279
3279
  }, "Hermes applyConfig: skipping dotted-key subtree(s) — OpenClaw-plugin-shaped config does not apply to Hermes");
3280
3280
  for (const { path, value } of leaves) try {
3281
3281
  await this.runConfigSet([path, stringifyConfigValue(value)]);
3282
3282
  } catch (err) {
3283
- log$2.error({
3283
+ log$4.error({
3284
3284
  err: err instanceof Error ? err.message : String(err),
3285
3285
  key: path
3286
3286
  }, "Failed to set config via hermes config set");
@@ -3303,7 +3303,7 @@ var HermesApplier = class {
3303
3303
  try {
3304
3304
  await this.deleteConfigKeys(leaves.map(({ path }) => path));
3305
3305
  } catch (err) {
3306
- log$2.warn({
3306
+ log$4.warn({
3307
3307
  err: err instanceof Error ? err.message : String(err),
3308
3308
  integrationId
3309
3309
  }, "Failed to delete Alfe config keys from ~/.hermes/config.yaml");
@@ -3367,11 +3367,11 @@ var HermesApplier = class {
3367
3367
  * one in Phase 1, so this path is a real attempt (not faked) but untrodden.
3368
3368
  */
3369
3369
  async applyPlugin(spec) {
3370
- if (isOpenClawPlugin(spec)) {
3371
- 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)");
3372
3372
  return;
3373
3373
  }
3374
- log$2.info({ spec }, "Hermes applyPlugin: installing native Hermes plugin");
3374
+ log$4.info({ spec }, "Hermes applyPlugin: installing native Hermes plugin");
3375
3375
  await execFileAsync("hermes", [
3376
3376
  "plugins",
3377
3377
  "install",
@@ -3389,11 +3389,11 @@ var HermesApplier = class {
3389
3389
  * Hermes plugin would be disabled via `hermes plugins`.
3390
3390
  */
3391
3391
  async removePlugin(spec) {
3392
- if (isOpenClawPlugin(spec)) {
3393
- 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)");
3394
3394
  return;
3395
3395
  }
3396
- log$2.info({ spec }, "Hermes removePlugin: disabling native Hermes plugin");
3396
+ log$4.info({ spec }, "Hermes removePlugin: disabling native Hermes plugin");
3397
3397
  await execFileAsync("hermes", [
3398
3398
  "plugins",
3399
3399
  "disable",
@@ -3401,19 +3401,19 @@ var HermesApplier = class {
3401
3401
  ], { timeout: 3e4 });
3402
3402
  }
3403
3403
  applySkill(name) {
3404
- 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)");
3405
3405
  return Promise.resolve();
3406
3406
  }
3407
3407
  removeSkill(name) {
3408
- 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)");
3409
3409
  return Promise.resolve();
3410
3410
  }
3411
3411
  applyClawHubSkill(slug) {
3412
- 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)");
3413
3413
  return Promise.resolve();
3414
3414
  }
3415
3415
  removeClawHubSkill(slug) {
3416
- 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)");
3417
3417
  return Promise.resolve();
3418
3418
  }
3419
3419
  isAvailable() {
@@ -3530,7 +3530,7 @@ var HermesApplier = class {
3530
3530
  * `warmup`) so the store is a pure ledger and the MCP children are spawned ONLY
3531
3531
  * by Hermes — avoiding a double-spawn of every server.
3532
3532
  */
3533
- const log$1 = createLogger("HermesMcpSync");
3533
+ const log$3 = createLogger("HermesMcpSync");
3534
3534
  const DEFAULT_HERMES_HOME = join(homedir(), ".hermes");
3535
3535
  /**
3536
3536
  * SEAM #1 — `${VAR}` interpolation from `~/.hermes/.env`. CONFIRMED (Hermes
@@ -3542,7 +3542,7 @@ const DEFAULT_HERMES_HOME = join(homedir(), ".hermes");
3542
3542
  * resolves at spawn time. The inline-literal fallback is therefore not needed.
3543
3543
  */
3544
3544
  const ALFE_API_KEY_ENV_REF = "${ALFE_API_KEY}";
3545
- const DEFAULT_DEBOUNCE_MS = 250;
3545
+ const DEFAULT_DEBOUNCE_MS$1 = 250;
3546
3546
  /**
3547
3547
  * Read-merge-write mirror of the MCP store into `~/.hermes/config.yaml`.
3548
3548
  *
@@ -3579,7 +3579,7 @@ var HermesMcpSync = class {
3579
3579
  this.configPath = opts.configPath ?? join(this.home, "config.yaml");
3580
3580
  this.envPath = opts.envPath ?? join(this.home, ".env");
3581
3581
  this.trackingPath = opts.trackingPath ?? join(this.home, ".alfe-mcp-synced.json");
3582
- this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3582
+ this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS$1;
3583
3583
  }
3584
3584
  /**
3585
3585
  * Begin mirroring: load the prior removal set, run one immediate sync (so
@@ -3591,12 +3591,12 @@ var HermesMcpSync = class {
3591
3591
  this.started = true;
3592
3592
  this.loadSyncedIds();
3593
3593
  this.syncOnce().catch((err) => {
3594
- 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");
3595
3595
  });
3596
3596
  this.unsubscribe = this.manager.onChange(() => {
3597
3597
  this.schedule();
3598
3598
  });
3599
- 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");
3600
3600
  }
3601
3601
  /** Stop subscribing and cancel any pending debounced sync. Idempotent. */
3602
3602
  stop() {
@@ -3627,7 +3627,7 @@ var HermesMcpSync = class {
3627
3627
  schedule() {
3628
3628
  if (this.debounceMs <= 0) {
3629
3629
  this.syncOnce().catch((err) => {
3630
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync failed");
3630
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync failed");
3631
3631
  });
3632
3632
  return;
3633
3633
  }
@@ -3635,7 +3635,7 @@ var HermesMcpSync = class {
3635
3635
  this.debounceTimer = setTimeout(() => {
3636
3636
  this.debounceTimer = void 0;
3637
3637
  this.syncOnce().catch((err) => {
3638
- log$1.warn({ err: errMsg$1(err) }, "Hermes MCP sync failed");
3638
+ log$3.warn({ err: errMsg$2(err) }, "Hermes MCP sync failed");
3639
3639
  });
3640
3640
  }, this.debounceMs);
3641
3641
  this.debounceTimer.unref();
@@ -3652,7 +3652,7 @@ var HermesMcpSync = class {
3652
3652
  if (changed) {
3653
3653
  mkdirSync(dirname(this.configPath), { recursive: true });
3654
3654
  writeFileSync(this.configPath, after, "utf-8");
3655
- log$1.info({
3655
+ log$3.info({
3656
3656
  added: [...desiredIds],
3657
3657
  removed: [...this.syncedIds].filter((id) => !desiredIds.has(id))
3658
3658
  }, "Hermes MCP sync: config.yaml mcp_servers updated");
@@ -3694,7 +3694,7 @@ var HermesMcpSync = class {
3694
3694
  /** Read-merge-write `~/.hermes/.env` to set ALFE_API_KEY without clobbering other keys. */
3695
3695
  ensureEnvApiKey() {
3696
3696
  if (!this.apiKey) {
3697
- 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");
3698
3698
  return false;
3699
3699
  }
3700
3700
  return upsertEnvVar(this.envPath, "ALFE_API_KEY", this.apiKey);
@@ -3711,7 +3711,7 @@ var HermesMcpSync = class {
3711
3711
  mkdirSync(dirname(this.trackingPath), { recursive: true });
3712
3712
  writeFileSync(this.trackingPath, JSON.stringify({ syncedIds: [...this.syncedIds] }, null, 2) + "\n", "utf-8");
3713
3713
  } catch (err) {
3714
- 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");
3715
3715
  }
3716
3716
  }
3717
3717
  };
@@ -3742,6 +3742,466 @@ function upsertEnvVar(envPath, key, value) {
3742
3742
  }
3743
3743
  return changed;
3744
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
+ };
3745
4205
  function errMsg$1(err) {
3746
4206
  return err instanceof Error ? err.message : String(err);
3747
4207
  }
@@ -3960,4 +4420,4 @@ var IntegrationManagerAdapter = class {
3960
4420
  }
3961
4421
  };
3962
4422
  //#endregion
3963
- 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.2",
3
+ "version": "0.5.1",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "@auriclabs/logger": "^0.1.1",
16
16
  "yaml": ">=2.8.3",
17
- "@alfe.ai/integration-manifest": "^0.3.1",
17
+ "@alfe.ai/integration-manifest": "^0.3.2",
18
18
  "@alfe.ai/mcp-bundler": "^0.3.1"
19
19
  },
20
20
  "files": [