1import {
2 type ClaudeSettings,
3 type ModelCapabilities,
4 type ServerProvider,
5 type ServerProviderSlashCommand,
6 type ServerProviderResetCredits,
7} from "@t3tools/contracts";
8import * as DateTime from "effect/DateTime";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Option from "effect/Option";
12import * as Path from "effect/Path";
13import * as Ref from "effect/Ref";
14import * as Result from "effect/Result";
15import { ChildProcess, ChildProcessSpawner } from "effect/process";
16import { createModelCapabilities } from "@t3tools/shared/model";
17import { resolveSpawnCommand } from "@t3tools/shared/shell";
18import {
19 query as claudeQuery,
20 type Options as ClaudeQueryOptions,
21 type SlashCommand as ClaudeSlashCommand,
22 type SDKControlGetUsageResponse,
23 type SDKUserMessage,
24 type SettingSource,
25} from "@anthropic-ai/claude-agent-sdk";
26
27import {
28 buildServerProvider,
29 COMPACT_SLASH_COMMAND,
30 DEFAULT_TIMEOUT_MS,
31 isCommandMissingCause,
32 parseGenericCliVersion,
33 providerModelsFromSettings,
34 spawnAndCollect,
35 type ServerProviderDraft,
36} from "@t3tools/provider-core/server/snapshotProbe";
37import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts";
38import { hasClaudeManagedMcpConfig, makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts";
39import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts";
40import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver";
41import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";
42import {
43 type ClaudeScopedLimitNames,
44 claudeUsageResponseToLimits,
45 recordClaudeUsageResponse,
46} from "./claudeUsageLimits.ts";
47import {
48 BUNDLED_CLAUDE_MODEL_CATALOG,
49 type ClaudeModelCatalog,
50 formatClaudeVersionUpgradeMessage,
51 resolveClaudeModelsForVersion,
52 resolveClaudeUpdateRequiredModels,
53} from "./ClaudeModelCatalog.ts";
54
55const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({
56 optionDescriptors: [],
57});
58
59const CLAUDE_PRESENTATION = {
60 displayName: "Claude",
61 showInteractionModeToggle: true,
62 reportsContextWindow: true,
63} as const;
64function toTitleCaseWords(value: string): string {
65 const parts: Array<string> = [];
66 for (const part of value.split(/[\s_-]+/g)) {
67 if (part.length > 0) {
68 parts.push(part[0]!.toUpperCase() + part.slice(1).toLowerCase());
69 }
70 }
71 return parts.join(" ");
72}
73
74function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined {
75 const normalized = subscriptionType?.toLowerCase().replace(/[\s_-]+/g, "");
76 if (!normalized) return undefined;
77
78 switch (normalized) {
79 case "claudemaxsubscription":
80 return "Max";
81 case "claudemax5xsubscription":
82 return "Max 5x";
83 case "claudemax20xsubscription":
84 return "Max 20x";
85 case "claudeenterprisesubscription":
86 return "Enterprise";
87 case "claudeteamsubscription":
88 return "Team";
89 case "claudeprosubscription":
90 return "Pro";
91 case "claudefreesubscription":
92 return "Free";
93 case "max":
94 case "maxplan":
95 return "Max";
96 case "max5":
97 return "Max 5x";
98 case "max20":
99 return "Max 20x";
100 case "enterprise":
101 return "Enterprise";
102 case "team":
103 return "Team";
104 case "pro":
105 return "Pro";
106 case "free":
107 return "Free";
108 default:
109 return toTitleCaseWords(subscriptionType!);
110 }
111}
112
113function normalizeClaudeAuthMethod(authMethod: string | undefined): string | undefined {
114 const normalized = authMethod?.toLowerCase().replace(/[\s_-]+/g, "");
115 if (!normalized) return undefined;
116 if (
117 normalized === "apikey" ||
118 normalized === "anthropicapikey" ||
119 normalized === "anthropicauthtoken"
120 ) {
121 return "apiKey";
122 }
123 return undefined;
124}
125
126function formatClaudeSubscriptionAuthLabel(subscriptionType: string): string {
127 const subscriptionLabel =
128 claudeSubscriptionLabel(subscriptionType) ?? toTitleCaseWords(subscriptionType);
129 const normalized = subscriptionLabel.toLowerCase().replace(/[\s_-]+/g, "");
130
131 if (normalized.startsWith("claude") && normalized.endsWith("subscription")) {
132 return subscriptionLabel;
133 }
134 if (normalized.startsWith("claude")) {
135 return `${subscriptionLabel} Subscription`;
136 }
137 if (normalized.endsWith("subscription")) {
138 return `Claude ${subscriptionLabel}`;
139 }
140 return `Claude ${subscriptionLabel} Subscription`;
141}
142
143function claudeAuthMetadata(input: {
144 readonly subscriptionType: string | undefined;
145 readonly authMethod: string | undefined;
146}): { readonly type: string; readonly label: string } | undefined {
147 if (normalizeClaudeAuthMethod(input.authMethod) === "apiKey") {
148 return {
149 type: "apiKey",
150 label: "Claude API Key",
151 };
152 }
153
154 if (input.subscriptionType) {
155 return {
156 type: input.subscriptionType,
157 label: formatClaudeSubscriptionAuthLabel(input.subscriptionType),
158 };
159 }
160
161 return undefined;
162}
163
164function apiProviderAuthMetadata(
165 apiProvider: string | undefined,
166): { readonly type: string; readonly label: string } | undefined {
167 return apiProvider === "bedrock" ? { type: "bedrock", label: "Amazon Bedrock" } : undefined;
168}
169
170/**
171 * Whether the SDK's account payload evidences a credential the CLI can use.
172 *
173 * The capability probe resolves for a logged-out CLI, so a completed probe only
174 * proves Claude Code started. `tokenSource: "none"` is the CLI reporting it
175 * found no token at all, and is the one shape that disproves authentication.
176 * Everything else either names a credential or, on a third-party backend, omits
177 * these fields by design because auth lives with AWS or gcloud instead.
178 *
179 * Silence is deliberately not disproof. Profile-authenticated installs report no
180 * token source, and a CLI too old to send an account payload reports nothing at
181 * all; treating either as logged out would sign working setups out of Settings.
182 * `apiKeySource: "none"` means no API key is in use, so it is no evidence either.
183 */
184function claudeAuthStatus(
185 capabilities: Pick<
186 ClaudeCapabilitiesProbe,
187 "email" | "subscriptionType" | "tokenSource" | "apiKeySource" | "apiProvider"
188 >,
189): "authenticated" | "unauthenticated" {
190 if (capabilities.apiProvider !== undefined && capabilities.apiProvider !== "firstParty") {
191 return "authenticated";
192 }
193 if (capabilities.tokenSource !== "none") return "authenticated";
194 // An `ANTHROPIC_API_KEY` install reports no token source but is authenticated
195 // all the same, so the key and account fields still get a say.
196 const hasApiKey = Boolean(capabilities.apiKeySource) && capabilities.apiKeySource !== "none";
197 return hasApiKey || capabilities.email || capabilities.subscriptionType
198 ? "authenticated"
199 : "unauthenticated";
200}
201
202// ── SDK capability probe ────────────────────────────────────────────
203
204// Amazon Bedrock initializes far slower than first-party auth: the SDK boots the
205// Bedrock backend and runs the `awsAuthRefresh` credential hook before returning
206// account info. The previous 8s budget expired mid-init, so the probe returned
207// `undefined` and left the provider unverified and unselectable in the picker.
208const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000;
209
210/**
211 * Keep workspace-scoped command discovery intact while isolating the periodic
212 * health check from configured MCP servers.
213 */
214export const CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES = [
215 "user",
216 "project",
217 "local",
218] as const satisfies ReadonlyArray<SettingSource>;
219
220/** Build the exact SDK options used by the periodic Claude capability probe. */
221export function buildClaudeCapabilitiesProbeQueryOptions(input: {
222 readonly executablePath: string;
223 readonly abortController: AbortController;
224 readonly environment: NodeJS.ProcessEnv;
225 readonly cwd: string | undefined;
226 /** An administrator's `managed-mcp.json` is installed; see `hasClaudeManagedMcpConfig`. */
227 readonly managedMcpConfig: boolean;
228}): ClaudeQueryOptions {
229 return {
230 persistSession: false,
231 pathToClaudeCodeExecutable: input.executablePath,
232 abortController: input.abortController,
233 settingSources: [...CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES],
234 // The probe keeps filesystem setting sources for slash-command discovery,
235 // but must not run the user's hooks: it fires every few minutes, so
236 // SessionStart hooks would run on every health check.
237 settings: { disableAllHooks: true },
238 allowedTools: [],
239 // Ignore MCP definitions from every filesystem setting source above. The
240 // SDK combines this empty explicit map with --strict-mcp-config, which
241 // Claude refuses next to a managed-mcp.json that already excludes them.
242 mcpServers: {},
243 ...(input.managedMcpConfig ? {} : { strictMcpConfig: true }),
244 env: {
245 ...input.environment,
246 // Connected claude.ai MCP servers are discovered outside filesystem
247 // config; disable them independently for this health check.
248 ENABLE_CLAUDEAI_MCP_SERVERS: "false",
249 // This is a noninteractive health check, so IDE discovery cannot add any
250 // useful capability data. Skipping it also avoids Claude spawning a
251 // Windows `tasklist | findstr` process tree on every periodic refresh.
252 FORCE_CODE_TERMINAL: undefined,
253 CLAUDE_CODE_AUTO_CONNECT_IDE: "0",
254 CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL: "1",
255 },
256 ...(input.cwd ? { cwd: input.cwd } : {}),
257 stderr: () => {},
258 };
259}
260
261function nonEmptyProbeString(value: string): string | undefined {
262 const candidate = value.trim();
263 return candidate ? candidate : undefined;
264}
265
266type ClaudeCapabilitiesProbe = {
267 readonly email: string | undefined;
268 /**
269 * The organization UUID from the login's `.claude.json`. The driver reads it
270 * in the same cached probe, so it describes the same login as the rest.
271 */
272 readonly workspaceId?: string | undefined;
273 readonly subscriptionType: string | undefined;
274 readonly tokenSource: string | undefined;
275 /** Where the CLI found an API key, when it authenticates with one. */
276 readonly apiKeySource: string | undefined;
277 /**
278 * Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth
279 * login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`)
280 * the subscription/token fields are absent and auth is external AWS creds.
281 */
282 readonly apiProvider: string | undefined;
283 readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
284 /**
285 * Subscription windows from the SDK's `get_usage` control request, or
286 * `undefined` when the request itself failed. Absent windows on an
287 * otherwise successful response mean the account has none (API key).
288 */
289 readonly usage?: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits">;
290};
291
292function parseClaudeInitializationCommands(
293 commands: ReadonlyArray<ClaudeSlashCommand> | undefined,
294): ReadonlyArray<ServerProviderSlashCommand> {
295 return dedupeSlashCommands(
296 (commands ?? []).flatMap((command) => {
297 const name = nonEmptyProbeString(command.name);
298 if (!name) {
299 return [];
300 }
301
302 const description = nonEmptyProbeString(command.description);
303 const argumentHint = nonEmptyProbeString(command.argumentHint);
304
305 return [
306 {
307 name,
308 ...(description ? { description } : {}),
309 ...(argumentHint ? { input: { hint: argumentHint } } : {}),
310 } satisfies ServerProviderSlashCommand,
311 ];
312 }),
313 );
314}
315
316function dedupeSlashCommands(
317 commands: ReadonlyArray<ServerProviderSlashCommand>,
318): ReadonlyArray<ServerProviderSlashCommand> {
319 const commandsByName = new Map<string, ServerProviderSlashCommand>();
320
321 for (const command of commands) {
322 const name = nonEmptyProbeString(command.name);
323 if (!name) {
324 continue;
325 }
326
327 const key = name.toLowerCase();
328 const existing = commandsByName.get(key);
329 if (!existing) {
330 commandsByName.set(key, {
331 ...command,
332 name,
333 });
334 continue;
335 }
336
337 commandsByName.set(key, {
338 ...existing,
339 ...(existing.description
340 ? {}
341 : command.description
342 ? { description: command.description }
343 : {}),
344 ...(existing.input?.hint
345 ? {}
346 : command.input?.hint
347 ? { input: { hint: command.input.hint } }
348 : {}),
349 });
350 }
351
352 return [...commandsByName.values()];
353}
354
355function waitForAbortSignal(signal: AbortSignal): Promise<void> {
356 if (signal.aborted) {
357 return Promise.resolve();
358 }
359 return new Promise((resolve) => {
360 signal.addEventListener("abort", () => resolve(), { once: true });
361 });
362}
363
364/**
365 * Probe account information by spawning a lightweight Claude Agent SDK
366 * session and reading the initialization result.
367 *
368 * We pass a never-yielding AsyncIterable as the prompt so that no user
369 * message is ever written to the subprocess stdin. This means the Claude
370 * Code subprocess completes its local initialization IPC (returning
371 * account info and slash commands) but never starts an API request to
372 * Anthropic. We read the init data and then abort the subprocess.
373 *
374 * This is used as a fallback when `claude auth status` does not include
375 * subscription type information.
376 */
377const probeClaudeCapabilities = (
378 claudeSettings: ClaudeSettings,
379 environment?: NodeJS.ProcessEnv,
380 cwd?: string,
381 includeUsage = true,
382) => {
383 const abort = new AbortController();
384 return Effect.gen(function* () {
385 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);
386 const executablePath = yield* resolveClaudeSdkExecutablePath(
387 claudeSettings.binaryPath,
388 claudeEnvironment,
389 );
390 const managedMcpConfig = yield* hasClaudeManagedMcpConfig();
391 return yield* Effect.tryPromise(async () => {
392 const q = claudeQuery({
393 // Never yield — we only need initialization data, not a conversation.
394 // This prevents any prompt from reaching the Anthropic API.
395 // oxlint-disable-next-line require-yield
396 prompt: (async function* (): AsyncGenerator<SDKUserMessage> {
397 await waitForAbortSignal(abort.signal);
398 })(),
399 options: buildClaudeCapabilitiesProbeQueryOptions({
400 executablePath,
401 abortController: abort,
402 environment: claudeEnvironment,
403 cwd,
404 managedMcpConfig,
405 }),
406 });
407 const init = await q.initializationResult();
408 return { q, init };
409 });
410 }).pipe(
411 Effect.timeout(CAPABILITIES_PROBE_TIMEOUT_MS),
412 Effect.flatMap(({ q, init }) =>
413 Effect.gen(function* () {
414 // Usage has its own deadline so a slow optional request cannot discard initialization.
415 // Only the rate limits are read, so skip the local transcript scan that fills
416 // `behaviors`: with a few GB of transcripts it outlasts the deadline.
417 const usageResult = includeUsage
418 ? yield* Effect.tryPromise(() =>
419 q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true }),
420 ).pipe(Effect.timeout(DEFAULT_TIMEOUT_MS), Effect.result)
421 : undefined;
422 const usage =
423 usageResult && Result.isSuccess(usageResult)
424 ? {
425 rate_limits_available: usageResult.success.rate_limits_available,
426 rate_limits: usageResult.success.rate_limits,
427 }
428 : undefined;
429 const account = init.account as
430 | {
431 readonly email?: string;
432 readonly subscriptionType?: string;
433 readonly tokenSource?: string;
434 readonly apiKeySource?: string;
435 readonly apiProvider?: string;
436 }
437 | undefined;
438 return {
439 email: account?.email,
440 subscriptionType: account?.subscriptionType,
441 tokenSource: account?.tokenSource,
442 apiKeySource: account?.apiKeySource,
443 apiProvider: account?.apiProvider,
444 slashCommands: parseClaudeInitializationCommands(init.commands),
445 ...(usage ? { usage } : {}),
446 } satisfies ClaudeCapabilitiesProbe;
447 }),
448 ),
449 Effect.ensuring(
450 Effect.sync(() => {
451 if (!abort.signal.aborted) abort.abort();
452 }),
453 ),
454 Effect.result,
455 Effect.map((result) => (Result.isSuccess(result) ? result.success : undefined)),
456 );
457};
458
459const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (
460 claudeSettings: ClaudeSettings,
461 args: ReadonlyArray<string>,
462 environment?: NodeJS.ProcessEnv,
463 cwd?: string,
464) {
465 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment);
466 const spawnCommand = yield* resolveSpawnCommand(claudeSettings.binaryPath, args, {
467 env: claudeEnvironment,
468 });
469 const command = ChildProcess.make(spawnCommand.command, spawnCommand.args, {
470 env: claudeEnvironment,
471 shell: spawnCommand.shell,
472 ...(cwd ? { cwd } : {}),
473 });
474 return yield* spawnAndCollect(claudeSettings.binaryPath, command);
475});
476
477/** Read commands from the same cwd Claude uses for a workspace session. */
478export const probeClaudeWorkspaceSnapshot = Effect.fn("probeClaudeWorkspaceSnapshot")(function* (
479 claudeSettings: ClaudeSettings,
480 machineSnapshot: ServerProvider,
481 cwd: string,
482 environment?: NodeJS.ProcessEnv,
483): Effect.fn.Return<ProviderWorkspaceSnapshot, never, FileSystem.FileSystem | Path.Path> {
484 if (!claudeSettings.enabled) return machineSnapshot;
485 const skills = yield* discoverClaudeSkills(claudeSettings, cwd, environment);
486 const capabilities = yield* probeClaudeCapabilities(claudeSettings, environment, cwd, false);
487 return {
488 ...machineSnapshot,
489 skills,
490 slashCommands: dedupeSlashCommands([
491 COMPACT_SLASH_COMMAND,
492 ...(capabilities?.slashCommands ?? []),
493 ]),
494 slashCommandsPending: !capabilities,
495 };
496});
497
498export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(function* (
499 claudeSettings: ClaudeSettings,
500 resolveCapabilities?: (
501 claudeSettings: ClaudeSettings,
502 ) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>,
503 environment?: NodeJS.ProcessEnv,
504 cwd?: string,
505 modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG,
506 /** Shared with the adapter so turn events reuse the scoped-bucket names this probe saw. */
507 scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>,
508 /** Banked resets for a subscription login, given the CLI version for the user agent. */
509 resolveResetCredits?: (version: string) => Effect.Effect<ServerProviderResetCredits | undefined>,
510): Effect.fn.Return<
511 ServerProviderDraft,
512 never,
513 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
514> {
515 const resolvedEnvironment = environment ?? process.env;
516 const checkedAt = DateTime.formatIso(yield* DateTime.now);
517 const allModels = providerModelsFromSettings(
518 modelCatalog.models.map((entry) => entry.model),
519 claudeSettings.customModels,
520 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
521 );
522
523 if (!claudeSettings.enabled) {
524 return buildServerProvider({
525 presentation: CLAUDE_PRESENTATION,
526 enabled: false,
527 checkedAt,
528 models: allModels,
529 probe: {
530 installed: false,
531 version: null,
532 status: "warning",
533 auth: { status: "unknown" },
534 message: "Claude is disabled in T3 Code settings.",
535 },
536 });
537 }
538
539 const versionProbe = yield* runClaudeCommand(
540 claudeSettings,
541 ["--version"],
542 resolvedEnvironment,
543 cwd,
544 ).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
545
546 if (Result.isFailure(versionProbe)) {
547 const error = versionProbe.failure;
548 yield* Effect.logWarning("Claude Agent CLI health check failed.", {
549 errorTag: error._tag,
550 });
551 return buildServerProvider({
552 presentation: CLAUDE_PRESENTATION,
553 enabled: claudeSettings.enabled,
554 checkedAt,
555 models: allModels,
556 probe: {
557 installed: !isCommandMissingCause(error),
558 version: null,
559 status: "error",
560 auth: { status: "unknown" },
561 message: isCommandMissingCause(error)
562 ? "Claude Agent CLI (`claude`) was not found on PATH."
563 : "Failed to execute Claude Agent CLI health check.",
564 },
565 });
566 }
567
568 if (Option.isNone(versionProbe.success)) {
569 return buildServerProvider({
570 presentation: CLAUDE_PRESENTATION,
571 enabled: claudeSettings.enabled,
572 checkedAt,
573 models: allModels,
574 probe: {
575 installed: true,
576 version: null,
577 status: "error",
578 auth: { status: "unknown" },
579 message:
580 "Claude Agent CLI is installed but failed to run. Timed out while running command.",
581 },
582 });
583 }
584
585 const version = versionProbe.success.value;
586 const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`);
587 if (version.code !== 0) {
588 yield* Effect.logWarning("Claude Agent CLI version probe exited with a non-zero status.", {
589 exitCode: version.code,
590 stdoutLength: version.stdout.length,
591 stderrLength: version.stderr.length,
592 });
593 return buildServerProvider({
594 presentation: CLAUDE_PRESENTATION,
595 enabled: claudeSettings.enabled,
596 checkedAt,
597 models: allModels,
598 probe: {
599 installed: true,
600 version: parsedVersion,
601 status: "error",
602 auth: { status: "unknown" },
603 message: "Claude Agent CLI is installed but failed to run.",
604 },
605 });
606 }
607
608 const models = providerModelsFromSettings(
609 resolveClaudeModelsForVersion(modelCatalog, parsedVersion),
610 claudeSettings.customModels,
611 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
612 );
613 const updateRequiredModels = resolveClaudeUpdateRequiredModels(modelCatalog, parsedVersion);
614 const versionUpgradeMessage = formatClaudeVersionUpgradeMessage(modelCatalog, parsedVersion);
615
616 const capabilities = resolveCapabilities
617 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
618 : undefined;
619 const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment);
620 const slashCommands = [COMPACT_SLASH_COMMAND, ...(capabilities?.slashCommands ?? [])];
621 const dedupedSlashCommands = dedupeSlashCommands(slashCommands);
622
623 if (!capabilities) {
624 return buildServerProvider({
625 presentation: CLAUDE_PRESENTATION,
626 enabled: claudeSettings.enabled,
627 checkedAt,
628 models,
629 updateRequiredModels,
630 slashCommands: dedupedSlashCommands,
631 skills,
632 probe: {
633 installed: true,
634 version: parsedVersion,
635 status: "warning",
636 auth: { status: "unknown" },
637 message: "Could not verify Claude authentication status from initialization result.",
638 },
639 });
640 }
641
642 if (claudeAuthStatus(capabilities) === "unauthenticated") {
643 return buildServerProvider({
644 presentation: CLAUDE_PRESENTATION,
645 enabled: claudeSettings.enabled,
646 checkedAt,
647 models,
648 slashCommands: dedupedSlashCommands,
649 skills,
650 probe: {
651 installed: true,
652 version: parsedVersion,
653 status: "error",
654 auth: { status: "unauthenticated" },
655 message: "Claude Code is not authenticated. Run `claude auth login` and try again.",
656 },
657 });
658 }
659
660 const authMetadata =
661 claudeAuthMetadata({
662 subscriptionType: capabilities.subscriptionType,
663 authMethod: capabilities.tokenSource,
664 }) ?? apiProviderAuthMetadata(capabilities.apiProvider);
665 const usageLimits = !capabilities.usage
666 ? makeUnavailableUsageLimits({ checkedAt, reason: "probeFailed" })
667 : scopedLimitNames
668 ? yield* recordClaudeUsageResponse(scopedLimitNames, {
669 response: capabilities.usage,
670 checkedAt,
671 })
672 : claudeUsageResponseToLimits({ response: capabilities.usage, checkedAt }).limits;
673 const resetCredits =
674 resolveResetCredits &&
675 capabilities.subscriptionType &&
676 !usageLimits.unavailable &&
677 parsedVersion
678 ? yield* resolveResetCredits(parsedVersion)
679 : undefined;
680 return buildServerProvider({
681 presentation: CLAUDE_PRESENTATION,
682 enabled: claudeSettings.enabled,
683 checkedAt,
684 models,
685 updateRequiredModels,
686 slashCommands: dedupedSlashCommands,
687 skills,
688 probe: {
689 installed: true,
690 version: parsedVersion,
691 status: "ready",
692 auth: {
693 status: "authenticated",
694 ...(capabilities.email ? { email: capabilities.email } : {}),
695 ...(capabilities.workspaceId ? { workspaceId: capabilities.workspaceId } : {}),
696 ...(authMetadata ? authMetadata : {}),
697 },
698 ...(versionUpgradeMessage ? { message: versionUpgradeMessage } : {}),
699 usageLimits: resetCredits ? { ...usageLimits, resetCredits } : usageLimits,
700 },
701 });
702});
703
704const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
705
706export const makePendingClaudeProvider = (
707 claudeSettings: ClaudeSettings,
708 modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG,
709): Effect.Effect<ServerProviderDraft> =>
710 Effect.gen(function* () {
711 const checkedAt = yield* nowIso;
712 const models = providerModelsFromSettings(
713 modelCatalog.models.map((entry) => entry.model),
714 claudeSettings.customModels,
715 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
716 );
717
718 if (!claudeSettings.enabled) {
719 return buildServerProvider({
720 presentation: CLAUDE_PRESENTATION,
721 enabled: false,
722 checkedAt,
723 models,
724 probe: {
725 installed: false,
726 version: null,
727 status: "warning",
728 auth: { status: "unknown" },
729 message: "Claude is disabled in T3 Code settings.",
730 },
731 });
732 }
733
734 return buildServerProvider({
735 presentation: CLAUDE_PRESENTATION,
736 enabled: true,
737 checkedAt,
738 models,
739 probe: {
740 installed: false,
741 version: null,
742 status: "warning",
743 auth: { status: "unknown" },
744 message: "Claude provider status has not been checked in this session yet.",
745 },
746 });
747 });
748
749export { probeClaudeCapabilities };