/** * 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, type ServerProvider } 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 { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts"; import { checkClaudeProviderStatus, makePendingClaudeProvider, probeClaudeCapabilities, } from "../Layers/ClaudeProvider.ts"; import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import * as ModelManifest from "../ModelManifest.ts"; import { defaultProviderContinuationIdentity, type ProviderDriver, type ProviderInstance, } from "../ProviderDriver.ts"; import type { ServerProviderDraft } from "../providerSnapshot.ts"; import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; import { enrichProviderSnapshotWithVersionAdvisory, makePackageManagedProviderMaintenanceResolver, normalizeCommandPath, resolveProviderMaintenanceCapabilitiesEffect, } from "../providerMaintenance.ts"; import { haveProviderSnapshotSettingsChanged, makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings, } from "../providerUpdateSettings.ts"; import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.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", homebrewFormula: "claude-code", nativeUpdate: { executable: "claude", args: ["update"], lockKey: "claude-native", isCommandPath: isClaudeNativeCommandPath, }, }); export type ClaudeDriverEnv = | BackgroundPolicy.BackgroundPolicy | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | ModelManifest.ModelManifest | Path.Path | ProviderEventLoggers | ServerConfig | ServerSettingsService; const withInstanceIdentity = (input: { readonly instanceId: ProviderInstance["instanceId"]; readonly displayName: string | undefined; readonly accentColor: string | undefined; readonly continuationGroupKey: string; }) => (snapshot: ServerProviderDraft): ServerProvider => ({ ...snapshot, instanceId: input.instanceId, driver: DRIVER_KIND, ...(input.displayName ? { displayName: input.displayName } : {}), ...(input.accentColor ? { accentColor: input.accentColor } : {}), continuation: { groupKey: input.continuationGroupKey }, }); export const ClaudeDriver: ProviderDriver = { 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 processEnv = mergeProviderInstanceEnvironment(environment); const fallbackContinuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER_KIND, instanceId, }); const effectiveConfig = { ...config, enabled } satisfies ClaudeSettings; const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, }); const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); const stampIdentity = withInstanceIdentity({ instanceId, displayName, accentColor, continuationGroupKey, }); const adapterOptions = { instanceId, environment: processEnv, ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), }; const adapter = yield* makeClaudeAdapter(effectiveConfig, adapterOptions); const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv); // 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); // Kick the TTL-gated manifest refresh in the background and classify // with the in-memory manifest, so a slow or hung fetch never delays the // provider check. A refresh that lands mid-probe applies on the next one. const checkProvider = modelManifest.refreshInBackground.pipe( Effect.andThen( Effect.zipWith( checkClaudeProviderStatus( effectiveConfig, () => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey), processEnv, cwd, ), modelManifest.current, (draft, manifest) => stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)), { concurrent: true }, ), ), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), ); const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings); const snapshot = yield* makeManagedServerProvider>({ maintenanceCapabilities, getSettings: snapshotSettings.getSettings, streamSettings: snapshotSettings.streamSettings, haveSettingsChanged: haveProviderSnapshotSettingsChanged, initialSnapshot: (settings) => Effect.zipWith( makePendingClaudeProvider(settings.provider), modelManifest.current, (draft, manifest) => stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)), ), checkProvider, enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { enableProviderUpdateChecks: settings.enableProviderUpdateChecks, }).pipe( 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, }), ), ); return { instanceId, driverKind: DRIVER_KIND, continuationIdentity: { ...fallbackContinuationIdentity, continuationKey: continuationGroupKey, }, displayName, accentColor, enabled, snapshot, adapter, textGeneration, } satisfies ProviderInstance; }), };