716 lines · 21.4 KB · TypeScript
RawDownload
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 { 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,  detailFromResult,  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_OPUS_4_7_VERSION = "2.1.111";const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [  {    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"],        }),        buildSelectOptionDescriptor({          id: "contextWindow",          label: "Context Window",          options: [            { value: "200k", label: "200k", isDefault: true },            { value: "1m", label: "1M" },          ],        }),      ],    }),  },  {    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: "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 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<ServerProviderModel> {  if (supportsClaudeOpus47(version)) {    return BUILT_IN_MODELS;  }  return BUILT_IN_MODELS.filter((model) => model.slug !== "claude-opus-4-7");} 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): the Opus 4.7 * capability `"xhigh"` is rewritten to the accepted CLI value `"max"`, and * `"ultrathink"` is filtered out because it is a prompt-prefix mode rather * than a CLI-effort value. Returns `undefined` when no flag should be passed. */export function normalizeClaudeCliEffort(effort: string | null | undefined): string | undefined {  if (!effort || effort === "ultrathink") {    return undefined;  }  if (effort === "xhigh") {    return "max";  }  return effort;} 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 {  return value    .split(/[\s_-]+/g)    .filter(Boolean)    .map((part) => part[0]!.toUpperCase() + part.slice(1).toLowerCase())    .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<ServerProviderSlashCommand>;}; 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 = process.env,) => {  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<SDKUserMessage> {          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;    }),

Lines 1–500 of 716.