/** * @fileoverview Read or update fork-local @kody/ai provider defaults (provider, model, base URL, Cloudflare ids). * Writes default to dry-run; live persist requires `confirm: true`. Never stores API keys. * * @example * import aiSettings from 'kody:@kody/ai/settings' * const current = await aiSettings() */ import { packageStorage } from 'kody:runtime' import { PROVIDER_IDS, providerCatalog, providerSecretUrl } from './providers.ts' import { assertHttpsUrl, inputRecord, isMutationDryRun, optionalString, parseProviderId, type ProviderId, } from './validation.ts' export const SETTING_KEYS = { provider: 'provider', model: 'model', baseUrl: 'baseUrl', cloudflareAccountId: 'cloudflareAccountId', cloudflareAiGatewayId: 'cloudflareAiGatewayId', apiKeySecret: 'apiKeySecret', } as const export type AiSettings = { provider: ProviderId | null model: string | null baseUrl: string | null cloudflareAccountId: string | null cloudflareAiGatewayId: string | null apiKeySecret: string | null } const STORAGE_NOTE = "These defaults live in this package's packageStorage. Live @kody/ai storage is the platform bucket — fork or invoke your own copy before setting them." async function readString(key: string): Promise { try { const store = packageStorage() if (!store) return null const raw = await store.get(key) return typeof raw === 'string' && raw.trim() ? raw.trim() : null } catch { return null } } async function writeString(key: string, value: string) { const store = packageStorage() await store.set(key, value) } /** * Read or update fork-local @kody/ai provider defaults (provider, model, base URL, Cloudflare ids). * Writes default to dry-run; live persist requires `confirm: true`. Never stores API keys. * * @returns Current fork-local AI settings. * * @example * import aiSettings from 'kody:@kody/ai/settings' * const current = await aiSettings() */ export async function getSettings(): Promise { const providerRaw = await readString(SETTING_KEYS.provider) let provider: ProviderId | null = null if (providerRaw) { try { provider = parseProviderId(providerRaw) ?? null } catch { provider = null } } return { provider, model: await readString(SETTING_KEYS.model), baseUrl: await readString(SETTING_KEYS.baseUrl), cloudflareAccountId: await readString(SETTING_KEYS.cloudflareAccountId), cloudflareAiGatewayId: await readString(SETTING_KEYS.cloudflareAiGatewayId), apiKeySecret: await readString(SETTING_KEYS.apiKeySecret), } } export type SettingsInput = { provider?: string model?: string baseUrl?: string cloudflareAccountId?: string cloudflareAiGatewayId?: string apiKeySecret?: string dryRun?: boolean confirm?: boolean } /** * Read or update fork-local provider defaults. * Writes default to dry-run. A live persist requires `confirm: true`. * Never stores API keys — only provider, model, base URL, and Cloudflare ids. * * @param input - Optional fields to update plus `dryRun` / `confirm`. * @returns Current settings, or a dry-run / updated payload when writing. * * @example * import aiSettings from 'kody:@kody/ai/settings' * const current = await aiSettings() */ export default async function aiSettings(input: SettingsInput = {}) { const parsed = inputRecord(input ?? {}) const next: Partial = {} const provider = optionalString(parsed, 'provider') if (provider) next.provider = parseProviderId(provider) const model = optionalString(parsed, 'model') if (model) next.model = model const baseUrl = optionalString(parsed, 'baseUrl') if (baseUrl) next.baseUrl = assertHttpsUrl(baseUrl, 'baseUrl') const cloudflareAccountId = optionalString(parsed, 'cloudflareAccountId') if (cloudflareAccountId) next.cloudflareAccountId = cloudflareAccountId const cloudflareAiGatewayId = optionalString(parsed, 'cloudflareAiGatewayId') if (cloudflareAiGatewayId) next.cloudflareAiGatewayId = cloudflareAiGatewayId const apiKeySecret = optionalString(parsed, 'apiKeySecret') if (apiKeySecret) next.apiKeySecret = apiKeySecret const updates = Object.entries(next).filter(([, value]) => value != null) if (updates.length === 0) { return { settings: await getSettings(), note: STORAGE_NOTE, providers: PROVIDER_IDS.map((id) => { const entry = providerCatalog(id) return { id, secret: entry.secretPrefix, hosts: entry.hosts, secretUrl: providerSecretUrl(id), } }), } } if (isMutationDryRun(parsed)) { return { dryRun: true as const, wouldUpdate: Object.fromEntries(updates), settings: await getSettings(), note: 'Pass confirm: true to persist these defaults on this fork.', } } for (const [key, value] of updates) { await writeString(key, String(value)) } return { updated: updates.map(([key]) => key), settings: await getSettings(), note: STORAGE_NOTE, } } export { STORAGE_NOTE }