/** * ClaudeDriver — `ProviderDriver` for the Claude Agent SDK runtime. * * Mirrors `CodexDriver`: a plain value whose `create()` returns one * `ProviderInstance` bundling `snapshot` / `adapter` / `textGeneration` * closures captured over the per-instance `ClaudeSettings`. * * Unlike Codex, the Claude snapshot probe may invoke a secondary probe * (`probeClaudeCapabilities`) to read Anthropic account + slash-command * metadata. That probe is per-instance and keyed by binary + resolved HOME so * two concurrent Claude instances don't cross-contaminate account metadata. * * @module provider/Drivers/ClaudeDriver */import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts";import * as Cache from "effect/Cache";import * as Duration from "effect/Duration";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import { HttpClient } from "effect/unstable/http";import { ChildProcessSpawner } from "effect/unstable/process"; import { makeClaudeTextGeneration } from "../../textGeneration/ClaudeTextGeneration.ts";import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";import { ServerConfig } from "../../config.ts";import { expandHomePath } from "../../pathExpansion.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { ProviderDriverError } from "../Errors.ts";import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts";import { makeClaudeScopedLimitNames } from "../Layers/claudeUsageLimits.ts";import { checkClaudeProviderStatus, makePendingClaudeProvider, probeClaudeCapabilities,} from "../Layers/ClaudeProvider.ts";import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";import { resolveClaudeModelCatalog } from "../ClaudeModelCatalog.ts";import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";import * as ModelManifest from "../ModelManifest.ts";import { defaultProviderContinuationIdentity, type ProviderDriver, type ProviderInstance,} from "../ProviderDriver.ts";import { withInstanceIdentity } from "./instanceIdentity.ts";import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";import { enrichProviderSnapshotWithVersionAdvisory, makeCachedProviderMaintenanceResolution, makePackageManagedProviderMaintenanceResolver, normalizeCommandPath, resolveProviderMaintenanceCapabilitiesEffect,} from "../providerMaintenance.ts";import { haveProviderSnapshotSettingsChanged, makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings,} from "../providerUpdateSettings.ts";import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";import { discoverClaudeSkills } from "./ClaudeSkills.ts";const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");const CAPABILITIES_PROBE_TTL = Duration.minutes(5); function isClaudeNativeCommandPath(commandPath: string): boolean { const normalized = normalizeCommandPath(commandPath); return ( normalized.endsWith("/.local/bin/claude") || normalized.endsWith("/.local/bin/claude.exe") || normalized.includes("/.local/share/claude/") );} const UPDATE = makePackageManagedProviderMaintenanceResolver({ provider: DRIVER_KIND, npmPackageName: "@anthropic-ai/claude-code", nativeUpdate: { args: ["update"], isCommandPath: isClaudeNativeCommandPath, },}); export type ClaudeDriverEnv = | BackgroundPolicy.BackgroundPolicy | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | ModelManifest.ModelManifest | Path.Path | ProviderEventLoggers | ServerConfig | ServerSettingsService; export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { driverKind: DRIVER_KIND, metadata: { displayName: "Claude", supportsMultipleInstances: true, }, configSchema: ClaudeSettings, defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}), create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const { cwd } = yield* ServerConfig; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; const eventLoggers = yield* ProviderEventLoggers; const modelManifest = yield* ModelManifest.ModelManifest; const modelCatalog = modelManifest.current.pipe(Effect.map(resolveClaudeModelCatalog)); const processEnv = mergeProviderInstanceEnvironment(environment); const fallbackContinuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, }); const effectiveConfig = { ...config, enabled, binaryPath: expandHomePath(config.binaryPath), } satisfies ClaudeSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ), ); const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); const stampIdentity = withInstanceIdentity({ instanceId, driverKind: DRIVER_KIND, displayName, accentColor, continuationGroupKey, }); // One per instance: the status probe writes the model-scoped bucket // names it saw, the adapter reads them to place turn-driven events. const scopedLimitNames = yield* makeClaudeScopedLimitNames; const adapterOptions = { instanceId, environment: processEnv, modelCatalog, scopedLimitNames, ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), }; const adapter = yield* makeClaudeAdapter(effectiveConfig, adapterOptions); const textGeneration = yield* makeClaudeTextGeneration( effectiveConfig, processEnv, modelCatalog, ); // Per-instance capabilities cache: keyed on binary + resolved HOME so // account-specific probes never share auth metadata across instances. const capabilitiesProbeCache = yield* Cache.make({ capacity: 1, timeToLive: CAPABILITIES_PROBE_TTL, lookup: () => probeClaudeCapabilities(effectiveConfig, processEnv, cwd).pipe( Effect.provideService(Path.Path, path), ), }); const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd); // Start the TTL-gated refresh without delaying provider readiness. The // next check observes a remote manifest after the background fetch lands. const checkProvider = modelManifest.refreshInBackground.pipe( Effect.andThen( modelManifest.current.pipe( Effect.flatMap((manifest) => checkClaudeProviderStatus( effectiveConfig, () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), processEnv, cwd, resolveClaudeModelCatalog(manifest), scopedLimitNames, ), ), Effect.map(stampIdentity), ), ), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings); const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<ClaudeSettings>>({ resolveMaintenance, getSettings: snapshotSettings.getSettings, streamSettings: snapshotSettings.streamSettings, haveSettingsChanged: haveProviderSnapshotSettingsChanged, initialSnapshot: (settings) => modelManifest.current.pipe( Effect.flatMap((manifest) => makePendingClaudeProvider(settings.provider, resolveClaudeModelCatalog(manifest)), ), Effect.map(stampIdentity), ), checkProvider, enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => resolveMaintenance().pipe( Effect.flatMap((maintenanceCapabilities) => enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { enableProviderUpdateChecks: settings.enableProviderUpdateChecks, }), ), Effect.provideService(HttpClient.HttpClient, httpClient), Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), ), }).pipe( Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: `Failed to build Claude snapshot: ${cause.message ?? String(cause)}`, cause, }), ), ); const snapshotForCwd = (cwd: string) => !effectiveConfig.enabled ? snapshot.getSnapshot : Effect.all([ snapshot.getSnapshot, discoverClaudeSkills(effectiveConfig, cwd, processEnv), ]).pipe( Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills })), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); return { instanceId, driverKind: DRIVER_KIND, continuationIdentity: { ...fallbackContinuationIdentity, continuationKey: continuationGroupKey, }, displayName, accentColor, enabled, snapshot, snapshotForCwd, adapter, textGeneration, } satisfies ProviderInstance; }),};
/**
* ClaudeDriver — `ProviderDriver` for the Claude Agent SDK runtime.
*
* Mirrors `CodexDriver`: a plain value whose `create()` returns one
* `ProviderInstance` bundling `snapshot` / `adapter` / `textGeneration`
* closures captured over the per-instance `ClaudeSettings`.
*
* Unlike Codex, the Claude snapshot probe may invoke a secondary probe
* (`probeClaudeCapabilities`) to read Anthropic account + slash-command
* metadata. That probe is per-instance and keyed by binary + resolved HOME so
* two concurrent Claude instances don't cross-contaminate account metadata.
*
* @module provider/Drivers/ClaudeDriver
*/
import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts";
import * as Cache from "effect/Cache";
import * as Duration from "effect/Duration";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";
import { makeClaudeTextGeneration } from "../../textGeneration/ClaudeTextGeneration.ts";
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { expandHomePath } from "../../pathExpansion.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts";
import { makeClaudeScopedLimitNames } from "../Layers/claudeUsageLimits.ts";
import {
checkClaudeProviderStatus,
makePendingClaudeProvider,
probeClaudeCapabilities,
} from "../Layers/ClaudeProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { resolveClaudeModelCatalog } from "../ClaudeModelCatalog.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import * as ModelManifest from "../ModelManifest.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import { withInstanceIdentity } from "./instanceIdentity.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makeCachedProviderMaintenanceResolution,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
import { discoverClaudeSkills } from "./ClaudeSkills.ts";
const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
const CAPABILITIES_PROBE_TTL = Duration.minutes(5);
function isClaudeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.endsWith("/.local/bin/claude") ||
normalized.endsWith("/.local/bin/claude.exe") ||
normalized.includes("/.local/share/claude/")
);
}
const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@anthropic-ai/claude-code",
nativeUpdate: {
args: ["update"],
isCommandPath: isClaudeNativeCommandPath,
},
});
export type ClaudeDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| ModelManifest.ModelManifest
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;
export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Claude",
supportsMultipleInstances: true,
},
configSchema: ClaudeSettings,
defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { cwd } = yield* ServerConfig;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const modelManifest = yield* ModelManifest.ModelManifest;
const modelCatalog = modelManifest.current.pipe(Effect.map(resolveClaudeModelCatalog));
const processEnv = mergeProviderInstanceEnvironment(environment);
const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const effectiveConfig = {
...config,
enabled,
binaryPath: expandHomePath(config.binaryPath),
} satisfies ClaudeSettings;
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
),
);
const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
const stampIdentity = withInstanceIdentity({
instanceId,
driverKind: DRIVER_KIND,
displayName,
accentColor,
continuationGroupKey,
});
// One per instance: the status probe writes the model-scoped bucket
// names it saw, the adapter reads them to place turn-driven events.
const scopedLimitNames = yield* makeClaudeScopedLimitNames;
const adapterOptions = {
instanceId,
environment: processEnv,
modelCatalog,
scopedLimitNames,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
};
const adapter = yield* makeClaudeAdapter(effectiveConfig, adapterOptions);
const textGeneration = yield* makeClaudeTextGeneration(
effectiveConfig,
processEnv,
modelCatalog,
);
// Per-instance capabilities cache: keyed on binary + resolved HOME so
// account-specific probes never share auth metadata across instances.
const capabilitiesProbeCache = yield* Cache.make({
capacity: 1,
timeToLive: CAPABILITIES_PROBE_TTL,
lookup: () =>
probeClaudeCapabilities(effectiveConfig, processEnv, cwd).pipe(
Effect.provideService(Path.Path, path),
),
});
const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd);
// Start the TTL-gated refresh without delaying provider readiness. The
// next check observes a remote manifest after the background fetch lands.
const checkProvider = modelManifest.refreshInBackground.pipe(
Effect.andThen(
modelManifest.current.pipe(
Effect.flatMap((manifest) =>
checkClaudeProviderStatus(
effectiveConfig,
() => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
processEnv,
cwd,
resolveClaudeModelCatalog(manifest),
scopedLimitNames,
),
),
Effect.map(stampIdentity),
),
),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);
const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<ClaudeSettings>>({
resolveMaintenance,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
modelManifest.current.pipe(
Effect.flatMap((manifest) =>
makePendingClaudeProvider(settings.provider, resolveClaudeModelCatalog(manifest)),
),
Effect.map(stampIdentity),
),
checkProvider,
enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
resolveMaintenance().pipe(
Effect.flatMap((maintenanceCapabilities) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
}),
),
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Claude snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);
const snapshotForCwd = (cwd: string) =>
!effectiveConfig.enabled
? snapshot.getSnapshot
: Effect.all([
snapshot.getSnapshot,
discoverClaudeSkills(effectiveConfig, cwd, processEnv),
]).pipe(
Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills })),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);
return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity: {
...fallbackContinuationIdentity,
continuationKey: continuationGroupKey,
},
displayName,
accentColor,
enabled,
snapshot,
snapshotForCwd,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};