import type { ProviderDriverKind, ModelCapabilities, ServerProvider, ServerProviderAuth, ServerProviderSkill, ServerProviderSlashCommand, ServerProviderModel, ServerProviderState, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Data from "effect/Data"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { normalizeModelSlug } from "@t3tools/shared/model"; import { isWindowsCommandNotFound } from "../processRunner.ts"; import { createProviderVersionAdvisory } from "./providerMaintenance.ts"; import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; export const DEFAULT_TIMEOUT_MS = 4_000; // Auth status checks involve disk/network lookups and can be slow on first run (especially Windows) export const AUTH_PROBE_TIMEOUT_MS = 10_000; export interface CommandResult { readonly stdout: string; readonly stderr: string; readonly code: number; } export class ProviderCommandExecutionError extends Data.TaggedError( "ProviderCommandExecutionError", )<{ readonly message: string; }> {} export interface ProviderProbeResult { readonly installed: boolean; readonly version: string | null; readonly status: Exclude; readonly auth: ServerProviderAuth; readonly message?: string; } export interface ServerProviderPresentation { readonly displayName: string; readonly badgeLabel?: string; readonly showInteractionModeToggle?: boolean; readonly requiresNewThreadForModelChange?: boolean; } export type ServerProviderDraft = Omit; export function nonEmptyTrimmed(value: string | undefined): string | undefined { if (!value) return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } export function isCommandMissingCause(error: { readonly message: string }): boolean { const lower = error.message.toLowerCase(); return lower.includes("enoent") || lower.includes("notfound"); } export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Command) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner.spawn(command); const [stdout, stderr, exitCode] = yield* Effect.all( [ collectStreamAsString(child.stdout), collectStreamAsString(child.stderr), child.exitCode.pipe(Effect.map(Number)), ], { concurrency: "unbounded" }, ); const result: CommandResult = { stdout, stderr, code: exitCode }; if (yield* isWindowsCommandNotFound(exitCode, stderr)) { return yield* new ProviderCommandExecutionError({ message: `spawn ${binaryPath} ENOENT` }); } return result; }).pipe(Effect.scoped); export function detailFromResult( result: CommandResult & { readonly timedOut?: boolean }, ): string | undefined { if (result.timedOut) return "Timed out while running command."; const stderr = nonEmptyTrimmed(result.stderr); if (stderr) return stderr; const stdout = nonEmptyTrimmed(result.stdout); if (stdout) return stdout; if (result.code !== 0) { return `Command exited with code ${result.code}.`; } return undefined; } export function extractAuthBoolean(value: unknown): boolean | undefined { if (globalThis.Array.isArray(value)) { for (const entry of value) { const nested = extractAuthBoolean(entry); if (nested !== undefined) return nested; } return undefined; } if (!value || typeof value !== "object") return undefined; const record = value as Record; for (const key of ["authenticated", "isAuthenticated", "loggedIn", "isLoggedIn"] as const) { if (typeof record[key] === "boolean") return record[key]; } for (const key of ["auth", "status", "session", "account"] as const) { const nested = extractAuthBoolean(record[key]); if (nested !== undefined) return nested; } return undefined; } export function parseGenericCliVersion(output: string): string | null { const match = output.match(/\b(\d+\.\d+\.\d+)\b/); return match?.[1] ?? null; } export function providerModelsFromSettings( builtInModels: ReadonlyArray, provider: ProviderDriverKind, customModels: ReadonlyArray, customModelCapabilities: ModelCapabilities, ): ReadonlyArray { const resolvedBuiltInModels = [...builtInModels]; const seen = new Set(resolvedBuiltInModels.map((model) => model.slug)); const customEntries: ServerProviderModel[] = []; for (const candidate of customModels) { const normalized = normalizeModelSlug(candidate, provider); if (!normalized || seen.has(normalized)) { continue; } seen.add(normalized); customEntries.push({ slug: normalized, name: normalized, isCustom: true, capabilities: customModelCapabilities, }); } return [...resolvedBuiltInModels, ...customEntries]; } export function buildSelectOptionDescriptor(input: { readonly id: string; readonly label: string; readonly options: | ReadonlyArray<{ value: string; label: string; isDefault?: boolean | undefined }> | undefined; readonly description?: string; readonly promptInjectedValues?: ReadonlyArray; }) { const options = (input.options ?? []).map((option) => option.isDefault ? { id: option.value, label: option.label, isDefault: true } : { id: option.value, label: option.label }, ); const currentValue = options.find((option) => option.isDefault)?.id; return { id: input.id, label: input.label, type: "select" as const, options, ...(currentValue ? { currentValue } : {}), ...(input.description ? { description: input.description } : {}), ...(input.promptInjectedValues && input.promptInjectedValues.length > 0 ? { promptInjectedValues: [...input.promptInjectedValues] } : {}), }; } export function buildBooleanOptionDescriptor(input: { readonly id: string; readonly label: string; readonly currentValue?: boolean; readonly description?: string; }) { return { id: input.id, label: input.label, type: "boolean" as const, ...(input.description ? { description: input.description } : {}), ...(typeof input.currentValue === "boolean" ? { currentValue: input.currentValue } : {}), }; } export function buildServerProvider(input: { driver?: ProviderDriverKind; presentation: ServerProviderPresentation; enabled: boolean; checkedAt: string; models: ReadonlyArray; slashCommands?: ReadonlyArray; skills?: ReadonlyArray; probe: ProviderProbeResult; }): ServerProviderDraft { const versionAdvisory = input.driver ? createProviderVersionAdvisory({ driver: input.driver, currentVersion: input.probe.version, checkedAt: input.checkedAt, }) : undefined; return { displayName: input.presentation.displayName, ...(input.presentation.badgeLabel ? { badgeLabel: input.presentation.badgeLabel } : {}), ...(typeof input.presentation.showInteractionModeToggle === "boolean" ? { showInteractionModeToggle: input.presentation.showInteractionModeToggle } : {}), ...(typeof input.presentation.requiresNewThreadForModelChange === "boolean" ? { requiresNewThreadForModelChange: input.presentation.requiresNewThreadForModelChange } : {}), enabled: input.enabled, installed: input.probe.installed, version: input.probe.version, status: input.enabled ? input.probe.status : "disabled", auth: input.probe.auth, checkedAt: input.checkedAt, ...(input.probe.message ? { message: input.probe.message } : {}), models: input.models, slashCommands: [...(input.slashCommands ?? [])], skills: [...(input.skills ?? [])], ...(versionAdvisory ? { versionAdvisory } : {}), }; } export const collectStreamAsString = ( stream: Stream.Stream, ): Effect.Effect => collectUint8StreamText({ stream }).pipe(Effect.map((collected) => collected.text));