519 lines · 15.2 KB · TypeScript
RawDownload
import {  DateTime,  Duration,  Effect,  Equal,  Layer,  Option,  Result,  Schema,  Stream,  Types,} from "effect";import { ChildProcessSpawner } from "effect/unstable/process";import * as CodexClient from "effect-codex-app-server/client";import * as CodexSchema from "effect-codex-app-server/schema";import * as CodexErrors from "effect-codex-app-server/errors"; import type {  CodexSettings,  ServerProvider,  ServerProviderState,  ModelCapabilities,  ServerProviderModel,  ServerProviderSkill,} from "@t3tools/contracts";import { ServerSettingsError } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";import { buildServerProvider } from "../providerSnapshot.ts";import { CodexProvider } from "../Services/CodexProvider.ts";import { expandHomePath } from "../../pathExpansion.ts";import { ServerSettingsService } from "../../serverSettings.ts";import packageJson from "../../../package.json" with { type: "json" }; const PROVIDER = "codex" as const;const PROVIDER_PROBE_TIMEOUT_MS = 8_000;const CODEX_PRESENTATION = {  displayName: "Codex",  showInteractionModeToggle: true,} as const; export interface CodexAppServerProviderSnapshot {  readonly account: CodexSchema.V2GetAccountResponse;  readonly version: string | undefined;  readonly models: ReadonlyArray<ServerProviderModel>;  readonly skills: ReadonlyArray<ServerProviderSkill>;} const REASONING_EFFORT_LABELS: Record<CodexSchema.V2ModelListResponse__ReasoningEffort, string> = {  none: "None",  minimal: "Minimal",  low: "Low",  medium: "Medium",  high: "High",  xhigh: "Extra High",}; function codexAccountAuthLabel(account: CodexSchema.V2GetAccountResponse["account"]) {  if (!account) return undefined;  if (account.type === "apiKey") return "OpenAI API Key";   switch (account.planType) {    case "free":      return "ChatGPT Free Subscription";    case "go":      return "ChatGPT Go Subscription";    case "plus":      return "ChatGPT Plus Subscription";    case "pro":      return "ChatGPT Pro 20x Subscription";    case "prolite":      return "ChatGPT Pro 5x Subscription";    case "team":      return "ChatGPT Team Subscription";    case "self_serve_business_usage_based":    case "business":      return "ChatGPT Business Subscription";    case "enterprise_cbp_usage_based":    case "enterprise":      return "ChatGPT Enterprise Subscription";    case "edu":      return "ChatGPT Edu Subscription";    case "unknown":      return "ChatGPT Subscription";    default:      account.planType satisfies never;      return undefined;  }} function mapCodexModelCapabilities(  model: CodexSchema.V2ModelListResponse__Model,): ModelCapabilities {  const reasoningOptions = model.supportedReasoningEfforts.map(({ reasoningEffort }) =>    reasoningEffort === model.defaultReasoningEffort      ? {          id: reasoningEffort,          label: REASONING_EFFORT_LABELS[reasoningEffort],          isDefault: true,        }      : {          id: reasoningEffort,          label: REASONING_EFFORT_LABELS[reasoningEffort],        },  );  const defaultReasoning = reasoningOptions.find((option) => option.isDefault)?.id;  const supportsFastMode = (model.additionalSpeedTiers ?? []).includes("fast");  return createModelCapabilities({    optionDescriptors: [      ...(reasoningOptions.length > 0        ? [            {              id: "reasoningEffort",              label: "Reasoning",              type: "select" as const,              options: reasoningOptions,              ...(defaultReasoning ? { currentValue: defaultReasoning } : {}),            },          ]        : []),      ...(supportsFastMode        ? [            {              id: "fastMode",              label: "Fast Mode",              type: "boolean" as const,            },          ]        : []),    ],  });} const toDisplayName = (model: CodexSchema.V2ModelListResponse__Model): string => {  // Capitalize 'gpt' to 'GPT-' and capitalize any letter following a dash  return model.displayName    .replace(/^gpt/i, "GPT") // Handle start with 'gpt' or 'GPT'    .replace(/-([a-z])/g, (_, c) => "-" + c.toUpperCase());}; function parseCodexModelListResponse(  response: CodexSchema.V2ModelListResponse,): ReadonlyArray<ServerProviderModel> {  return response.data.map((model) => ({    slug: model.model,    name: toDisplayName(model),    isCustom: false,    capabilities: mapCodexModelCapabilities(model),  }));} function appendCustomCodexModels(  models: ReadonlyArray<ServerProviderModel>,  customModels: ReadonlyArray<string>,): ReadonlyArray<ServerProviderModel> {  if (customModels.length === 0) {    return models;  }   const seen = new Set(models.map((model) => model.slug));  const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null;  const customEntries: ServerProviderModel[] = [];  for (const rawModel of customModels) {    const slug = rawModel.trim();    if (!slug || seen.has(slug)) {      continue;    }    seen.add(slug);    customEntries.push({      slug,      name: slug,      isCustom: true,      capabilities: fallbackCapabilities,    });  }  return customEntries.length === 0 ? models : [...models, ...customEntries];} function parseCodexSkillsListResponse(  response: CodexSchema.V2SkillsListResponse,  cwd: string,): ReadonlyArray<ServerProviderSkill> {  const matchingEntry = response.data.find((entry) => entry.cwd === cwd);  const skills = matchingEntry    ? matchingEntry.skills    : response.data.flatMap((entry) => entry.skills);   return skills.map((skill) => {    const shortDescription =      skill.shortDescription ?? skill.interface?.shortDescription ?? undefined;     const parsedSkill: Types.Mutable<ServerProviderSkill> = {      name: skill.name,      path: skill.path,      enabled: skill.enabled,    };     if (skill.description) {      parsedSkill.description = skill.description;    }    if (skill.scope) {      parsedSkill.scope = skill.scope;    }    if (skill.interface?.displayName) {      parsedSkill.displayName = skill.interface.displayName;    }    if (shortDescription) {      parsedSkill.shortDescription = shortDescription;    }     return parsedSkill;  });} const requestAllCodexModels = Effect.fn("requestAllCodexModels")(function* (  client: CodexClient.CodexAppServerClientShape,) {  const models: ServerProviderModel[] = [];  let cursor: string | null | undefined = undefined;   do {    const response: CodexSchema.V2ModelListResponse = yield* client.request(      "model/list",      cursor ? { cursor } : {},    );    models.push(...parseCodexModelListResponse(response));    cursor = response.nextCursor;  } while (cursor);   return models;}); export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {  return {    clientInfo: {      name: "t3code_desktop",      title: "T3 Code Desktop",      version: packageJson.version,    },    capabilities: {      experimentalApi: true,    },  };} const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: {  readonly binaryPath: string;  readonly homePath?: string;  readonly cwd: string;  readonly customModels?: ReadonlyArray<string>;}) {  const clientContext = yield* Layer.build(    CodexClient.layerCommand({      command: input.binaryPath,      args: ["app-server"],      cwd: input.cwd,      ...(input.homePath ? { env: { CODEX_HOME: expandHomePath(input.homePath) } } : {}),    }),  );  const client = yield* Effect.service(CodexClient.CodexAppServerClient).pipe(    Effect.provide(clientContext),  );   const initialize = yield* client.request("initialize", {    clientInfo: {      name: "t3code_desktop",      title: "T3 Code Desktop",      version: "0.1.0",    },    capabilities: {      experimentalApi: true,    },  });  yield* client.notify("initialized", undefined);   // Extract the version string after the first '/' in userAgent, up to the next space or the end  const versionMatch = initialize.userAgent.match(/\/([^\s]+)/);  const version = versionMatch ? versionMatch[1] : undefined;   const accountResponse = yield* client.request("account/read", {});  if (!accountResponse.account && accountResponse.requiresOpenaiAuth) {    return {      account: accountResponse,      version,      models: appendCustomCodexModels([], input.customModels ?? []),      skills: [],    } satisfies CodexAppServerProviderSnapshot;  }   const [skillsResponse, models] = yield* Effect.all(    [      client.request("skills/list", {        cwds: [input.cwd],      }),      requestAllCodexModels(client),    ],    { concurrency: "unbounded" },  );   return {    account: accountResponse,    version,    models: appendCustomCodexModels(models, input.customModels ?? []),    skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),  } satisfies CodexAppServerProviderSnapshot;}, Effect.scoped); const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] =>  codexSettings.customModels    .map((model) => model.trim())    .filter((model, index, models) => model.length > 0 && models.indexOf(model) === index)    .map((model) => ({      slug: model,      name: model,      isCustom: true,      capabilities: null,    })); const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProvider => {  const checkedAt = new Date().toISOString();  const models = emptyCodexModelsFromSettings(codexSettings);   if (!codexSettings.enabled) {    return buildServerProvider({      provider: PROVIDER,      presentation: CODEX_PRESENTATION,      enabled: false,      checkedAt,      models,      skills: [],      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: "Codex is disabled in T3 Code settings.",      },    });  }   return buildServerProvider({    provider: PROVIDER,    presentation: CODEX_PRESENTATION,    enabled: true,    checkedAt,    models,    skills: [],    probe: {      installed: false,      version: null,      status: "warning",      auth: { status: "unknown" },      message: "Codex provider status has not been checked in this session yet.",    },  });}; function accountProbeStatus(account: CodexAppServerProviderSnapshot["account"]): {  readonly status: Exclude<ServerProviderState, "disabled">;  readonly auth: ServerProvider["auth"];  readonly message?: string;} {  const authLabel = codexAccountAuthLabel(account.account);  const auth = {    status: account.account ? ("authenticated" as const) : ("unknown" as const),    ...(account.account?.type ? { type: account.account?.type } : {}),    ...(authLabel ? { label: authLabel } : {}),  } satisfies ServerProvider["auth"];   if (account.account) {    return { status: "ready", auth };  }   if (account.requiresOpenaiAuth) {    return {      status: "error",      auth: { status: "unauthenticated" },      message: "Codex CLI is not authenticated. Run `codex login` and try again.",    };  }   return { status: "ready", auth };} export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(function* (  probe: (input: {    readonly binaryPath: string;    readonly homePath?: string;    readonly cwd: string;    readonly customModels: ReadonlyArray<string>;  }) => Effect.Effect<    CodexAppServerProviderSnapshot,    CodexErrors.CodexAppServerError,    ChildProcessSpawner.ChildProcessSpawner  > = probeCodexAppServerProvider,): Effect.fn.Return<  ServerProvider,  ServerSettingsError,  ServerSettingsService | ChildProcessSpawner.ChildProcessSpawner> {  const codexSettings = yield* Effect.service(ServerSettingsService).pipe(    Effect.flatMap((service) => service.getSettings),    Effect.map((settings) => settings.providers.codex),  );  const checkedAt = DateTime.formatIso(yield* DateTime.now);  const emptyModels = emptyCodexModelsFromSettings(codexSettings);   if (!codexSettings.enabled) {    return buildServerProvider({      provider: PROVIDER,      presentation: CODEX_PRESENTATION,      enabled: false,      checkedAt,      models: emptyModels,      skills: [],      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: "Codex is disabled in T3 Code settings.",      },    });  }   const probeResult = yield* probe({    binaryPath: codexSettings.binaryPath,    homePath: codexSettings.homePath,    cwd: process.cwd(),    customModels: codexSettings.customModels,  }).pipe(Effect.timeoutOption(Duration.millis(PROVIDER_PROBE_TIMEOUT_MS)), Effect.result);   if (Result.isFailure(probeResult)) {    const error = probeResult.failure;    const installed = !Schema.is(CodexErrors.CodexAppServerSpawnError)(error);    return buildServerProvider({      provider: PROVIDER,      presentation: CODEX_PRESENTATION,      enabled: codexSettings.enabled,      checkedAt,      models: emptyModels,      skills: [],      probe: {        installed,        version: null,        status: "error",        auth: { status: "unknown" },        message: installed          ? `Codex app-server provider probe failed: ${error.message}.`          : "Codex CLI (`codex`) is not installed or not on PATH.",      },    });  }   if (Option.isNone(probeResult.success)) {    return buildServerProvider({      provider: PROVIDER,      presentation: CODEX_PRESENTATION,      enabled: codexSettings.enabled,      checkedAt,      models: emptyModels,      skills: [],      probe: {        installed: true,        version: null,        status: "error",        auth: { status: "unknown" },        message: "Timed out while checking Codex app-server provider status.",      },    });  }   const snapshot = probeResult.success.value;  const accountStatus = accountProbeStatus(snapshot.account);   return buildServerProvider({    provider: PROVIDER,    presentation: CODEX_PRESENTATION,    enabled: codexSettings.enabled,    checkedAt,    models: snapshot.models,    skills: snapshot.skills,    probe: {      installed: true,      version: snapshot.version ?? null,      status: accountStatus.status,      auth: accountStatus.auth,      ...(accountStatus.message ? { message: accountStatus.message } : {}),    },  });}); export const CodexProviderLive = Layer.effect(  CodexProvider,  Effect.gen(function* () {    const serverSettings = yield* ServerSettingsService;    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;    const checkProvider = checkCodexProviderStatus().pipe(

Lines 1–500 of 519.