@alfe.ai/integrations 0.4.2 → 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 +198 -1
- package/dist/index.js +523 -63
- package/package.json +1 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2567
|
+
log$5.warn({ stateDir }, "recreated openclaw state DB after quarantine");
|
|
2568
2568
|
return true;
|
|
2569
2569
|
} catch (err) {
|
|
2570
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2867
|
+
log$5.info({ slug }, "ClawHub skill already installed (skipped)");
|
|
2868
2868
|
return;
|
|
2869
2869
|
}
|
|
2870
|
-
log$
|
|
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$
|
|
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$
|
|
2940
|
+
log$5.warn({ parentPath }, "openclaw config set exited non-zero but config landed — continuing");
|
|
2941
2941
|
continue;
|
|
2942
2942
|
}
|
|
2943
|
-
log$
|
|
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$
|
|
2959
|
+
})) log$5.warn({ count: leaves.length }, "openclaw config set --batch-json exited non-zero but config landed — continuing");
|
|
2960
2960
|
else {
|
|
2961
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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