import { type ClaudeSettings, type ModelCapabilities, type ServerProviderSlashCommand,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Ref from "effect/Ref";import * as Result from "effect/Result";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import { createModelCapabilities } from "@t3tools/shared/model";import { resolveSpawnCommand } from "@t3tools/shared/shell";import { query as claudeQuery, type Options as ClaudeQueryOptions, type SlashCommand as ClaudeSlashCommand, type SDKControlGetUsageResponse, type SDKUserMessage, type SettingSource,} from "@anthropic-ai/claude-agent-sdk"; import { buildServerProvider, COMPACT_SLASH_COMMAND, DEFAULT_TIMEOUT_MS, isCommandMissingCause, parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, type ServerProviderDraft,} from "../providerSnapshot.ts";import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts";import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts";import { makeUnavailableUsageLimits } from "../providerUsageLimits.ts";import { type ClaudeScopedLimitNames, claudeUsageResponseToLimits, recordClaudeUsageResponse,} from "./claudeUsageLimits.ts";import { BUNDLED_CLAUDE_MODEL_CATALOG, type ClaudeModelCatalog, formatClaudeVersionUpgradeMessage, resolveClaudeModelsForVersion,} from "../ClaudeModelCatalog.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [],}); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, reportsContextWindow: true,} as const;function toTitleCaseWords(value: string): string { const parts: Array<string> = []; 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;} function apiProviderAuthMetadata( apiProvider: string | undefined,): { readonly type: string; readonly label: string } | undefined { return apiProvider === "bedrock" ? { type: "bedrock", label: "Amazon Bedrock" } : undefined;} // ── SDK capability probe ──────────────────────────────────────────── // Amazon Bedrock initializes far slower than first-party auth: the SDK boots the// Bedrock backend and runs the `awsAuthRefresh` credential hook before returning// account info. The previous 8s budget expired mid-init, so the probe returned// `undefined` and left the provider unverified and unselectable in the picker.const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; /** * Keep workspace-scoped command discovery intact while isolating the periodic * health check from configured MCP servers. */export const CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES = [ "user", "project", "local",] as const satisfies ReadonlyArray<SettingSource>; /** Build the exact SDK options used by the periodic Claude capability probe. */export function buildClaudeCapabilitiesProbeQueryOptions(input: { readonly executablePath: string; readonly abortController: AbortController; readonly environment: NodeJS.ProcessEnv; readonly cwd: string | undefined;}): ClaudeQueryOptions { return { persistSession: false, pathToClaudeCodeExecutable: input.executablePath, abortController: input.abortController, settingSources: [...CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES], // The probe keeps filesystem setting sources for slash-command discovery, // but must not run the user's hooks: it fires every few minutes, so // SessionStart hooks would run on every health check. settings: { disableAllHooks: true }, allowedTools: [], // Ignore MCP definitions from every filesystem setting source above. The // SDK combines this empty explicit map with --strict-mcp-config. mcpServers: {}, strictMcpConfig: true, env: { ...input.environment, // Connected claude.ai MCP servers are discovered outside filesystem // config; disable them independently for this health check. ENABLE_CLAUDEAI_MCP_SERVERS: "false", // This is a noninteractive health check, so IDE discovery cannot add any // useful capability data. Skipping it also avoids Claude spawning a // Windows `tasklist | findstr` process tree on every periodic refresh. FORCE_CODE_TERMINAL: undefined, CLAUDE_CODE_AUTO_CONNECT_IDE: "0", CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL: "1", }, ...(input.cwd ? { cwd: input.cwd } : {}), stderr: () => {}, };} 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; /** * Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth * login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`) * the subscription/token fields are absent and auth is external AWS creds. */ readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>; /** * Subscription windows from the SDK's `get_usage` control request, or * `undefined` when the request itself failed. Absent windows on an * otherwise successful response mean the account has none (API key). */ readonly usage?: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits">;}; function parseClaudeInitializationCommands( commands: ReadonlyArray<ClaudeSlashCommand> | undefined,): ReadonlyArray<ServerProviderSlashCommand> { 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<ServerProviderSlashCommand>,): ReadonlyArray<ServerProviderSlashCommand> { const commandsByName = new Map<string, ServerProviderSlashCommand>(); 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<void> { 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, cwd?: string,) => { const abort = new AbortController(); return Effect.gen(function* () { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); const executablePath = yield* resolveClaudeSdkExecutablePath( claudeSettings.binaryPath, claudeEnvironment, ); 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<SDKUserMessage> { await waitForAbortSignal(abort.signal); })(), options: buildClaudeCapabilitiesProbeQueryOptions({ executablePath, abortController: abort, environment: claudeEnvironment, cwd, }), }); const init = await q.initializationResult(); return { q, init }; }); }).pipe( Effect.timeout(CAPABILITIES_PROBE_TIMEOUT_MS), Effect.flatMap(({ q, init }) => Effect.gen(function* () { // Usage has its own deadline so a slow optional request cannot discard initialization. const usageResult = yield* Effect.tryPromise(() => q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(), ).pipe(Effect.timeout(DEFAULT_TIMEOUT_MS), Effect.result); const usage = Result.isSuccess(usageResult) ? { rate_limits_available: usageResult.success.rate_limits_available, rate_limits: usageResult.success.rate_limits, } : undefined; const account = init.account as | { readonly email?: string; readonly subscriptionType?: string; readonly tokenSource?: string; readonly apiProvider?: string; } | undefined; return { email: account?.email, subscriptionType: account?.subscriptionType, tokenSource: account?.tokenSource, apiProvider: account?.apiProvider, slashCommands: parseClaudeInitializationCommands(init.commands), ...(usage ? { usage } : {}), } satisfies ClaudeCapabilitiesProbe; }), ), Effect.ensuring( Effect.sync(() => { if (!abort.signal.aborted) abort.abort(); }), ), Effect.result, Effect.map((result) => (Result.isSuccess(result) ? result.success : undefined)), );}; const runClaudeCommand = Effect.fn("runClaudeCommand")(function* ( claudeSettings: ClaudeSettings, args: ReadonlyArray<string>, 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<ClaudeCapabilitiesProbe | undefined>, environment?: NodeJS.ProcessEnv, cwd?: string, modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG, /** Shared with the adapter so turn events reuse the scoped-bucket names this probe saw. */ scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>,): Effect.fn.Return< ServerProviderDraft, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path> { const resolvedEnvironment = environment ?? process.env; const checkedAt = DateTime.formatIso(yield* DateTime.now); const allModels = providerModelsFromSettings( modelCatalog.models.map((entry) => entry.model), 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`) was not found 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.", }, });Lines 1–500 of 638. Load more
import { type ClaudeSettings, type ModelCapabilities, type ServerProviderSlashCommand, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { createModelCapabilities } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { query as claudeQuery, type Options as ClaudeQueryOptions, type SlashCommand as ClaudeSlashCommand, type SDKControlGetUsageResponse, type SDKUserMessage, type SettingSource, } from "@anthropic-ai/claude-agent-sdk"; import { buildServerProvider, COMPACT_SLASH_COMMAND, DEFAULT_TIMEOUT_MS, isCommandMissingCause, parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, type ServerProviderDraft, } from "../providerSnapshot.ts"; import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts"; import { makeUnavailableUsageLimits } from "../providerUsageLimits.ts"; import { type ClaudeScopedLimitNames, claudeUsageResponseToLimits, recordClaudeUsageResponse, } from "./claudeUsageLimits.ts"; import { BUNDLED_CLAUDE_MODEL_CATALOG, type ClaudeModelCatalog, formatClaudeVersionUpgradeMessage, resolveClaudeModelsForVersion, } from "../ClaudeModelCatalog.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], }); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, reportsContextWindow: true, } as const; function toTitleCaseWords(value: string): string { const parts: Array<string> = []; 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; } function apiProviderAuthMetadata( apiProvider: string | undefined, ): { readonly type: string; readonly label: string } | undefined { return apiProvider === "bedrock" ? { type: "bedrock", label: "Amazon Bedrock" } : undefined; } // ── SDK capability probe ──────────────────────────────────────────── // Amazon Bedrock initializes far slower than first-party auth: the SDK boots the // Bedrock backend and runs the `awsAuthRefresh` credential hook before returning // account info. The previous 8s budget expired mid-init, so the probe returned // `undefined` and left the provider unverified and unselectable in the picker. const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; /** * Keep workspace-scoped command discovery intact while isolating the periodic * health check from configured MCP servers. */ export const CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES = [ "user", "project", "local", ] as const satisfies ReadonlyArray<SettingSource>; /** Build the exact SDK options used by the periodic Claude capability probe. */ export function buildClaudeCapabilitiesProbeQueryOptions(input: { readonly executablePath: string; readonly abortController: AbortController; readonly environment: NodeJS.ProcessEnv; readonly cwd: string | undefined; }): ClaudeQueryOptions { return { persistSession: false, pathToClaudeCodeExecutable: input.executablePath, abortController: input.abortController, settingSources: [...CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES], // The probe keeps filesystem setting sources for slash-command discovery, // but must not run the user's hooks: it fires every few minutes, so // SessionStart hooks would run on every health check. settings: { disableAllHooks: true }, allowedTools: [], // Ignore MCP definitions from every filesystem setting source above. The // SDK combines this empty explicit map with --strict-mcp-config. mcpServers: {}, strictMcpConfig: true, env: { ...input.environment, // Connected claude.ai MCP servers are discovered outside filesystem // config; disable them independently for this health check. ENABLE_CLAUDEAI_MCP_SERVERS: "false", // This is a noninteractive health check, so IDE discovery cannot add any // useful capability data. Skipping it also avoids Claude spawning a // Windows `tasklist | findstr` process tree on every periodic refresh. FORCE_CODE_TERMINAL: undefined, CLAUDE_CODE_AUTO_CONNECT_IDE: "0", CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL: "1", }, ...(input.cwd ? { cwd: input.cwd } : {}), stderr: () => {}, }; } 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; /** * Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth * login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`) * the subscription/token fields are absent and auth is external AWS creds. */ readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>; /** * Subscription windows from the SDK's `get_usage` control request, or * `undefined` when the request itself failed. Absent windows on an * otherwise successful response mean the account has none (API key). */ readonly usage?: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits">; }; function parseClaudeInitializationCommands( commands: ReadonlyArray<ClaudeSlashCommand> | undefined, ): ReadonlyArray<ServerProviderSlashCommand> { 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<ServerProviderSlashCommand>, ): ReadonlyArray<ServerProviderSlashCommand> { const commandsByName = new Map<string, ServerProviderSlashCommand>(); 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<void> { 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, cwd?: string, ) => { const abort = new AbortController(); return Effect.gen(function* () { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); const executablePath = yield* resolveClaudeSdkExecutablePath( claudeSettings.binaryPath, claudeEnvironment, ); 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<SDKUserMessage> { await waitForAbortSignal(abort.signal); })(), options: buildClaudeCapabilitiesProbeQueryOptions({ executablePath, abortController: abort, environment: claudeEnvironment, cwd, }), }); const init = await q.initializationResult(); return { q, init }; }); }).pipe( Effect.timeout(CAPABILITIES_PROBE_TIMEOUT_MS), Effect.flatMap(({ q, init }) => Effect.gen(function* () { // Usage has its own deadline so a slow optional request cannot discard initialization. const usageResult = yield* Effect.tryPromise(() => q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(), ).pipe(Effect.timeout(DEFAULT_TIMEOUT_MS), Effect.result); const usage = Result.isSuccess(usageResult) ? { rate_limits_available: usageResult.success.rate_limits_available, rate_limits: usageResult.success.rate_limits, } : undefined; const account = init.account as | { readonly email?: string; readonly subscriptionType?: string; readonly tokenSource?: string; readonly apiProvider?: string; } | undefined; return { email: account?.email, subscriptionType: account?.subscriptionType, tokenSource: account?.tokenSource, apiProvider: account?.apiProvider, slashCommands: parseClaudeInitializationCommands(init.commands), ...(usage ? { usage } : {}), } satisfies ClaudeCapabilitiesProbe; }), ), Effect.ensuring( Effect.sync(() => { if (!abort.signal.aborted) abort.abort(); }), ), Effect.result, Effect.map((result) => (Result.isSuccess(result) ? result.success : undefined)), ); }; const runClaudeCommand = Effect.fn("runClaudeCommand")(function* ( claudeSettings: ClaudeSettings, args: ReadonlyArray<string>, 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<ClaudeCapabilitiesProbe | undefined>, environment?: NodeJS.ProcessEnv, cwd?: string, modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG, /** Shared with the adapter so turn events reuse the scoped-bucket names this probe saw. */ scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>, ): Effect.fn.Return< ServerProviderDraft, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path > { const resolvedEnvironment = environment ?? process.env; const checkedAt = DateTime.formatIso(yield* DateTime.now); const allModels = providerModelsFromSettings( modelCatalog.models.map((entry) => entry.model), 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`) was not found 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.", }, });