import { type ClaudeSettings, type ModelCapabilities, type ModelSelection, ProviderDriverKind, type ServerProviderModel, type ServerProviderSlashCommand, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Result from "effect/Result"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { createModelCapabilities, getModelSelectionStringOptionValue, getProviderOptionCurrentValue, getProviderOptionDescriptors, } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { compareSemverVersions } from "@t3tools/shared/semver"; import { query as claudeQuery, type SlashCommand as ClaudeSlashCommand, type SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import { buildBooleanOptionDescriptor, buildSelectOptionDescriptor, buildServerProvider, DEFAULT_TIMEOUT_MS, isCommandMissingCause, parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, type ServerProviderDraft, } from "../providerSnapshot.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], }); const PROVIDER = ProviderDriverKind.make("claudeAgent"); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, } as const; const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169"; const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154"; const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111"; const BUILT_IN_MODELS: ReadonlyArray = [ { slug: "claude-fable-5", name: "Claude Fable 5", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, { value: "xhigh", label: "Extra High" }, { value: "max", label: "Max" }, { value: "ultracode", label: "Ultracode" }, { value: "ultrathink", label: "Ultrathink" }, ], promptInjectedValues: ["ultrathink"], }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", options: [ { value: "200k", label: "200k", isDefault: true }, { value: "1m", label: "1M" }, ], }), ], }), }, { slug: "claude-opus-4-8", name: "Claude Opus 4.8", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, { value: "xhigh", label: "Extra High" }, { value: "max", label: "Max" }, { value: "ultracode", label: "Ultracode" }, { value: "ultrathink", label: "Ultrathink" }, ], promptInjectedValues: ["ultrathink"], }), buildBooleanOptionDescriptor({ id: "fastMode", label: "Fast Mode", }), ], }), }, { slug: "claude-opus-4-7", name: "Claude Opus 4.7", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "xhigh", label: "Extra High", isDefault: true }, { value: "max", label: "Max" }, { value: "ultrathink", label: "Ultrathink" }, ], promptInjectedValues: ["ultrathink"], }), buildBooleanOptionDescriptor({ id: "fastMode", label: "Fast Mode", }), ], }), }, { slug: "claude-opus-4-6", name: "Claude Opus 4.6", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, { value: "max", label: "Max" }, { value: "ultrathink", label: "Ultrathink" }, ], promptInjectedValues: ["ultrathink"], }), buildBooleanOptionDescriptor({ id: "fastMode", label: "Fast Mode", }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", options: [ { value: "200k", label: "200k", isDefault: true }, { value: "1m", label: "1M" }, ], }), ], }), }, { slug: "claude-opus-4-5", name: "Claude Opus 4.5", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, { value: "max", label: "Max" }, ], }), buildBooleanOptionDescriptor({ id: "fastMode", label: "Fast Mode", }), ], }), }, { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildSelectOptionDescriptor({ id: "effort", label: "Reasoning", options: [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High", isDefault: true }, { value: "max", label: "Max" }, { value: "ultrathink", label: "Ultrathink" }, ], promptInjectedValues: ["ultrathink"], }), buildSelectOptionDescriptor({ id: "contextWindow", label: "Context Window", options: [ { value: "200k", label: "200k", isDefault: true }, { value: "1m", label: "1M" }, ], }), ], }), }, { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ buildBooleanOptionDescriptor({ id: "thinking", label: "Thinking", }), ], }), }, ]; function supportsClaudeFable5(version: string | null | undefined): boolean { return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_VERSION) >= 0 : false; } function supportsClaudeOpus48(version: string | null | undefined): boolean { return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_8_VERSION) >= 0 : false; } function supportsClaudeOpus47(version: string | null | undefined): boolean { return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_4_7_VERSION) >= 0 : false; } function getBuiltInClaudeModelsForVersion( version: string | null | undefined, ): ReadonlyArray { return BUILT_IN_MODELS.filter((model) => { if (model.slug === "claude-fable-5") { return supportsClaudeFable5(version); } if (model.slug === "claude-opus-4-8") { return supportsClaudeOpus48(version); } if (model.slug === "claude-opus-4-7") { return supportsClaudeOpus47(version); } return true; }); } function formatClaudeFable5UpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; return `Claude Code ${versionLabel} is too old for Claude Fable 5. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_VERSION} or newer to access it.`; } function formatClaudeOpus48UpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; return `Claude Code ${versionLabel} is too old for Claude Opus 4.8. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_8_VERSION} or newer to access it.`; } function formatClaudeOpus47UpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; return `Claude Code ${versionLabel} is too old for Claude Opus 4.7. Upgrade to v${MINIMUM_CLAUDE_OPUS_4_7_VERSION} or newer to access it.`; } export function getClaudeModelCapabilities(model: string | null | undefined): ModelCapabilities { const slug = model?.trim(); return ( BUILT_IN_MODELS.find((candidate) => candidate.slug === slug)?.capabilities ?? DEFAULT_CLAUDE_MODEL_CAPABILITIES ); } export function resolveClaudeEffort( caps: ModelCapabilities, raw: string | null | undefined, ): string | undefined { const descriptors = getProviderOptionDescriptors({ caps, ...(raw ? { selections: [{ id: "effort", value: raw }] } : {}), }); const effortDescriptor = descriptors.find((descriptor) => descriptor.id === "effort"); const value = getProviderOptionCurrentValue(effortDescriptor); return typeof value === "string" ? value : undefined; } /** * Normalize a resolved Claude effort value into one suitable for the Claude * CLI's `--effort` flag. * * Mirrors the mapping used when invoking the Claude Agent SDK * ({@link getEffectiveClaudeAgentEffort} in ClaudeAdapter): `ultracode` is a * Claude Code setting that pairs with `xhigh`, `ultrathink` is filtered out * because it is a prompt-prefix mode, and older model compatibility mappings * are preserved for current Claude Code behavior. */ export function normalizeClaudeCliEffort( effort: string | null | undefined, model: string | null | undefined, ): string | undefined { if (!effort || effort === "ultrathink") { return undefined; } if (effort === "ultracode") { return "xhigh"; } if (effort === "xhigh" && model !== "claude-fable-5" && model !== "claude-opus-4-8") { return "max"; } if (effort === "max" && model === "claude-sonnet-4-6") { return "high"; } return effort; } export function isClaudeUltracodeEffort(effort: string | null | undefined): boolean { return effort === "ultracode"; } export function resolveClaudeApiModelId(modelSelection: ModelSelection): string { switch (getModelSelectionStringOptionValue(modelSelection, "contextWindow")) { case "1m": return `${modelSelection.model}[1m]`; default: return modelSelection.model; } } function toTitleCaseWords(value: string): string { const parts: Array = []; for (const part of value.split(/[\s_-]+/g)) { if (part.length > 0) { parts.push(part[0]!.toUpperCase() + part.slice(1).toLowerCase()); } } return parts.join(" "); } function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined { const normalized = subscriptionType?.toLowerCase().replace(/[\s_-]+/g, ""); if (!normalized) return undefined; switch (normalized) { case "claudemaxsubscription": return "Max"; case "claudemax5xsubscription": return "Max 5x"; case "claudemax20xsubscription": return "Max 20x"; case "claudeenterprisesubscription": return "Enterprise"; case "claudeteamsubscription": return "Team"; case "claudeprosubscription": return "Pro"; case "claudefreesubscription": return "Free"; case "max": case "maxplan": return "Max"; case "max5": return "Max 5x"; case "max20": return "Max 20x"; case "enterprise": return "Enterprise"; case "team": return "Team"; case "pro": return "Pro"; case "free": return "Free"; default: return toTitleCaseWords(subscriptionType!); } } function normalizeClaudeAuthMethod(authMethod: string | undefined): string | undefined { const normalized = authMethod?.toLowerCase().replace(/[\s_-]+/g, ""); if (!normalized) return undefined; if ( normalized === "apikey" || normalized === "anthropicapikey" || normalized === "anthropicauthtoken" ) { return "apiKey"; } return undefined; } function formatClaudeSubscriptionAuthLabel(subscriptionType: string): string { const subscriptionLabel = claudeSubscriptionLabel(subscriptionType) ?? toTitleCaseWords(subscriptionType); const normalized = subscriptionLabel.toLowerCase().replace(/[\s_-]+/g, ""); if (normalized.startsWith("claude") && normalized.endsWith("subscription")) { return subscriptionLabel; } if (normalized.startsWith("claude")) { return `${subscriptionLabel} Subscription`; } if (normalized.endsWith("subscription")) { return `Claude ${subscriptionLabel}`; } return `Claude ${subscriptionLabel} Subscription`; } function claudeAuthMetadata(input: { readonly subscriptionType: string | undefined; readonly authMethod: string | undefined; }): { readonly type: string; readonly label: string } | undefined { if (normalizeClaudeAuthMethod(input.authMethod) === "apiKey") { return { type: "apiKey", label: "Claude API Key", }; } if (input.subscriptionType) { return { type: input.subscriptionType, label: formatClaudeSubscriptionAuthLabel(input.subscriptionType), }; } return undefined; } // ── SDK capability probe ──────────────────────────────────────────── const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000; function nonEmptyProbeString(value: string): string | undefined { const candidate = value.trim(); return candidate ? candidate : undefined; } type ClaudeCapabilitiesProbe = { readonly email: string | undefined; readonly subscriptionType: string | undefined; readonly tokenSource: string | undefined; readonly slashCommands: ReadonlyArray; }; function parseClaudeInitializationCommands( commands: ReadonlyArray | undefined, ): ReadonlyArray { return dedupeSlashCommands( (commands ?? []).flatMap((command) => { const name = nonEmptyProbeString(command.name); if (!name) { return []; } const description = nonEmptyProbeString(command.description); const argumentHint = nonEmptyProbeString(command.argumentHint); return [ { name, ...(description ? { description } : {}), ...(argumentHint ? { input: { hint: argumentHint } } : {}), } satisfies ServerProviderSlashCommand, ]; }), ); } function dedupeSlashCommands( commands: ReadonlyArray, ): ReadonlyArray { const commandsByName = new Map(); for (const command of commands) { const name = nonEmptyProbeString(command.name); if (!name) { continue; } const key = name.toLowerCase(); const existing = commandsByName.get(key); if (!existing) { commandsByName.set(key, { ...command, name, }); continue; } commandsByName.set(key, { ...existing, ...(existing.description ? {} : command.description ? { description: command.description } : {}), ...(existing.input?.hint ? {} : command.input?.hint ? { input: { hint: command.input.hint } } : {}), }); } return [...commandsByName.values()]; } function waitForAbortSignal(signal: AbortSignal): Promise { if (signal.aborted) { return Promise.resolve(); } return new Promise((resolve) => { signal.addEventListener("abort", () => resolve(), { once: true }); }); } /** * Probe account information by spawning a lightweight Claude Agent SDK * session and reading the initialization result. * * We pass a never-yielding AsyncIterable as the prompt so that no user * message is ever written to the subprocess stdin. This means the Claude * Code subprocess completes its local initialization IPC (returning * account info and slash commands) but never starts an API request to * Anthropic. We read the init data and then abort the subprocess. * * This is used as a fallback when `claude auth status` does not include * subscription type information. */ const probeClaudeCapabilities = ( claudeSettings: ClaudeSettings, environment?: NodeJS.ProcessEnv, ) => { const abort = new AbortController(); return Effect.gen(function* () { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); return yield* Effect.tryPromise(async () => { const q = claudeQuery({ // Never yield — we only need initialization data, not a conversation. // This prevents any prompt from reaching the Anthropic API. // oxlint-disable-next-line require-yield prompt: (async function* (): AsyncGenerator { await waitForAbortSignal(abort.signal); })(), options: { persistSession: false, pathToClaudeCodeExecutable: claudeSettings.binaryPath, abortController: abort, settingSources: ["user", "project", "local"], allowedTools: [], env: claudeEnvironment, stderr: () => {}, }, }); const init = await q.initializationResult(); const account = init.account as | { readonly email?: string; readonly subscriptionType?: string; readonly tokenSource?: string; } | undefined; return { email: account?.email, subscriptionType: account?.subscriptionType, tokenSource: account?.tokenSource, slashCommands: parseClaudeInitializationCommands(init.commands), } satisfies ClaudeCapabilitiesProbe; }); }).pipe( Effect.ensuring( Effect.sync(() => { if (!abort.signal.aborted) abort.abort(); }), ), Effect.timeoutOption(CAPABILITIES_PROBE_TIMEOUT_MS), Effect.result, Effect.map((result) => { if (Result.isFailure(result)) return undefined; return Option.isSome(result.success) ? result.success.value : undefined; }), ); }; const runClaudeCommand = Effect.fn("runClaudeCommand")(function* ( claudeSettings: ClaudeSettings, args: ReadonlyArray, environment?: NodeJS.ProcessEnv, ) { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, { env: claudeEnvironment, }); const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, { env: claudeEnvironment, shell: spawnCommand.shell, }); return yield* spawnAndCollect(claudeSettings.binaryPath, command); }); export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(function* ( claudeSettings: ClaudeSettings, resolveCapabilities?: ( claudeSettings: ClaudeSettings, ) => Effect.Effect, environment?: NodeJS.ProcessEnv, ): Effect.fn.Return< ServerProviderDraft, never, ChildProcessSpawner.ChildProcessSpawner | Path.Path > { const resolvedEnvironment = environment ?? process.env; const checkedAt = DateTime.formatIso(yield* DateTime.now); const allModels = providerModelsFromSettings( BUILT_IN_MODELS, PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); if (!claudeSettings.enabled) { return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: false, checkedAt, models: allModels, probe: { installed: false, version: null, status: "warning", auth: { status: "unknown" }, message: "Claude is disabled in T3 Code settings.", }, }); } const versionProbe = yield* runClaudeCommand( claudeSettings, ["--version"], resolvedEnvironment, ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; yield* Effect.logWarning("Claude Agent CLI health check failed.", { errorTag: error._tag, }); return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, checkedAt, models: allModels, probe: { installed: !isCommandMissingCause(error), version: null, status: "error", auth: { status: "unknown" }, message: isCommandMissingCause(error) ? "Claude Agent CLI (`claude`) is not installed or not on PATH." : "Failed to execute Claude Agent CLI health check.", }, }); } if (Option.isNone(versionProbe.success)) { return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, checkedAt, models: allModels, probe: { installed: true, version: null, status: "error", auth: { status: "unknown" }, message: "Claude Agent CLI is installed but failed to run. Timed out while running command.", }, }); } const version = versionProbe.success.value; const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`); if (version.code !== 0) { yield* Effect.logWarning("Claude Agent CLI version probe exited with a non-zero status.", { exitCode: version.code, stdoutLength: version.stdout.length, stderrLength: version.stderr.length, }); return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, checkedAt, models: allModels, probe: { installed: true, version: parsedVersion, status: "error", auth: { status: "unknown" }, message: "Claude Agent CLI is installed but failed to run.", }, }); } const models = providerModelsFromSettings( getBuiltInClaudeModelsForVersion(parsedVersion), PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); const versionUpgradeMessage = supportsClaudeFable5(parsedVersion) ? undefined : supportsClaudeOpus48(parsedVersion) ? formatClaudeFable5UpgradeMessage(parsedVersion) : supportsClaudeOpus47(parsedVersion) ? formatClaudeOpus48UpgradeMessage(parsedVersion) : formatClaudeOpus47UpgradeMessage(parsedVersion); const capabilities = resolveCapabilities ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined)) : undefined; const slashCommands = capabilities?.slashCommands ?? []; const dedupedSlashCommands = dedupeSlashCommands(slashCommands); if (!capabilities) { return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, checkedAt, models, slashCommands: dedupedSlashCommands, probe: { installed: true, version: parsedVersion, status: "warning", auth: { status: "unknown" }, message: "Could not verify Claude authentication status from initialization result.", }, }); } const authMetadata = claudeAuthMetadata({ subscriptionType: capabilities.subscriptionType, authMethod: capabilities.tokenSource, }); return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, checkedAt, models, slashCommands: dedupedSlashCommands, probe: { installed: true, version: parsedVersion, status: "ready", auth: { status: "authenticated", ...(capabilities.email ? { email: capabilities.email } : {}), ...(authMetadata ? authMetadata : {}), }, ...(versionUpgradeMessage ? { message: versionUpgradeMessage } : {}), }, }); }); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); export const makePendingClaudeProvider = ( claudeSettings: ClaudeSettings, ): Effect.Effect => Effect.gen(function* () { const checkedAt = yield* nowIso; const models = providerModelsFromSettings( BUILT_IN_MODELS, PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); if (!claudeSettings.enabled) { return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: false, checkedAt, models, probe: { installed: false, version: null, status: "warning", auth: { status: "unknown" }, message: "Claude is disabled in T3 Code settings.", }, }); } return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: true, checkedAt, models, probe: { installed: false, version: null, status: "warning", auth: { status: "unknown" }, message: "Claude provider status has not been checked in this session yet.", }, }); }); export { probeClaudeCapabilities };