522 lines · 15.6 KB · TypeScript
RawDownload
import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Result from "effect/Result";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Types from "effect/Types";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 {  AUTH_PROBE_TIMEOUT_MS,  buildServerProvider,  type ServerProviderDraft,} from "../providerSnapshot.ts";import { expandHomePath } from "../../pathExpansion.ts";import packageJson from "../../../package.json" with { type: "json" };const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError); 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";  if (account.type === "amazonBedrock") return "Amazon Bedrock";  if (account.type !== "chatgpt") return undefined;   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 codexAccountEmail(account: CodexSchema.V2GetAccountResponse["account"]) {  if (!account || account.type !== "chatgpt") return undefined;  return account.email;} 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>;  readonly environment?: NodeJS.ProcessEnv;}) {  // `~` is not shell-expanded when env vars are set via `child_process.spawn`,  // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip  // "CODEX_HOME points to '~/.codex_work', but that path does not exist".  // Expand here for parity with `CodexTextGeneration`/`CodexSessionRuntime`.  const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined;  const clientContext = yield* Layer.build(    CodexClient.layerCommand({      command: input.binaryPath,      args: ["app-server"],      cwd: input.cwd,      env: {        ...(input.environment ?? process.env),        ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),      },    }),  );  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;}); const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] => {  const models = new Set<string>();  for (const model of codexSettings.customModels) {    const trimmed = model.trim();    if (trimmed.length > 0) {      models.add(trimmed);    }  }  return Array.from(models, (model) => ({    slug: model,    name: model,    isCustom: true,    capabilities: null,  }));}; const makePendingCodexProvider = (  codexSettings: CodexSettings,): Effect.Effect<ServerProviderDraft> =>  Effect.gen(function* () {    const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);    const models = emptyCodexModelsFromSettings(codexSettings);     if (!codexSettings.enabled) {      return buildServerProvider({        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({      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 authEmail = codexAccountEmail(account.account);  const auth = {    status: account.account ? ("authenticated" as const) : ("unknown" as const),    ...(account.account?.type ? { type: account.account?.type } : {}),    ...(authLabel ? { label: authLabel } : {}),    ...(authEmail ? { email: authEmail } : {}),  } 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* (  codexSettings: CodexSettings,  probe: (input: {    readonly binaryPath: string;    readonly homePath?: string;    readonly cwd: string;    readonly customModels: ReadonlyArray<string>;    readonly environment?: NodeJS.ProcessEnv;  }) => Effect.Effect<    CodexAppServerProviderSnapshot,    CodexErrors.CodexAppServerError,    ChildProcessSpawner.ChildProcessSpawner | Scope.Scope  > = probeCodexAppServerProvider,  environment: NodeJS.ProcessEnv = process.env,): Effect.fn.Return<  ServerProviderDraft,  ServerSettingsError,  ChildProcessSpawner.ChildProcessSpawner> {  const checkedAt = DateTime.formatIso(yield* DateTime.now);  const emptyModels = emptyCodexModelsFromSettings(codexSettings);   if (!codexSettings.enabled) {    return buildServerProvider({      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,    environment,  }).pipe(    Effect.scoped,    Effect.timeoutOption(Duration.millis(AUTH_PROBE_TIMEOUT_MS)),    Effect.result,  );   if (Result.isFailure(probeResult)) {    const error = probeResult.failure;    const installed = !isCodexAppServerSpawnError(error);    return buildServerProvider({      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({      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({    presentation: CODEX_PRESENTATION,

Lines 1–500 of 522.