Spaces:
Paused
Paused
| import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts"; | |
| import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js"; | |
| import type { | |
| ProviderAuthMethod, | |
| ProviderAuthResult, | |
| ProviderPlugin, | |
| } from "../../plugins/types.js"; | |
| import type { RuntimeEnv } from "../../runtime.js"; | |
| import { | |
| resolveAgentDir, | |
| resolveAgentWorkspaceDir, | |
| resolveDefaultAgentId, | |
| } from "../../agents/agent-scope.js"; | |
| import { upsertAuthProfile } from "../../agents/auth-profiles.js"; | |
| import { normalizeProviderId } from "../../agents/model-selection.js"; | |
| import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; | |
| import { formatCliCommand } from "../../cli/command-format.js"; | |
| import { parseDurationMs } from "../../cli/parse-duration.js"; | |
| import { readConfigFileSnapshot, type OpenClawConfig } from "../../config/config.js"; | |
| import { logConfigUpdated } from "../../config/logging.js"; | |
| import { resolvePluginProviders } from "../../plugins/providers.js"; | |
| import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js"; | |
| import { createClackPrompter } from "../../wizard/clack-prompter.js"; | |
| import { validateAnthropicSetupToken } from "../auth-token.js"; | |
| import { isRemoteEnvironment } from "../oauth-env.js"; | |
| import { createVpsAwareOAuthHandlers } from "../oauth-flow.js"; | |
| import { applyAuthProfileConfig } from "../onboard-auth.js"; | |
| import { openUrl } from "../onboard-helpers.js"; | |
| import { updateConfig } from "./shared.js"; | |
| const confirm = (params: Parameters<typeof clackConfirm>[0]) => | |
| clackConfirm({ | |
| ...params, | |
| message: stylePromptMessage(params.message), | |
| }); | |
| const text = (params: Parameters<typeof clackText>[0]) => | |
| clackText({ | |
| ...params, | |
| message: stylePromptMessage(params.message), | |
| }); | |
| const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) => | |
| clackSelect({ | |
| ...params, | |
| message: stylePromptMessage(params.message), | |
| options: params.options.map((opt) => | |
| opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) }, | |
| ), | |
| }); | |
| type TokenProvider = "anthropic"; | |
| function resolveTokenProvider(raw?: string): TokenProvider | "custom" | null { | |
| const trimmed = raw?.trim(); | |
| if (!trimmed) { | |
| return null; | |
| } | |
| const normalized = normalizeProviderId(trimmed); | |
| if (normalized === "anthropic") { | |
| return "anthropic"; | |
| } | |
| return "custom"; | |
| } | |
| function resolveDefaultTokenProfileId(provider: string): string { | |
| return `${normalizeProviderId(provider)}:manual`; | |
| } | |
| export async function modelsAuthSetupTokenCommand( | |
| opts: { provider?: string; yes?: boolean }, | |
| runtime: RuntimeEnv, | |
| ) { | |
| const provider = resolveTokenProvider(opts.provider ?? "anthropic"); | |
| if (provider !== "anthropic") { | |
| throw new Error("Only --provider anthropic is supported for setup-token."); | |
| } | |
| if (!process.stdin.isTTY) { | |
| throw new Error("setup-token requires an interactive TTY."); | |
| } | |
| if (!opts.yes) { | |
| const proceed = await confirm({ | |
| message: "Have you run `claude setup-token` and copied the token?", | |
| initialValue: true, | |
| }); | |
| if (!proceed) { | |
| return; | |
| } | |
| } | |
| const tokenInput = await text({ | |
| message: "Paste Anthropic setup-token", | |
| validate: (value) => validateAnthropicSetupToken(String(value ?? "")), | |
| }); | |
| const token = String(tokenInput).trim(); | |
| const profileId = resolveDefaultTokenProfileId(provider); | |
| upsertAuthProfile({ | |
| profileId, | |
| credential: { | |
| type: "token", | |
| provider, | |
| token, | |
| }, | |
| }); | |
| await updateConfig((cfg) => | |
| applyAuthProfileConfig(cfg, { | |
| profileId, | |
| provider, | |
| mode: "token", | |
| }), | |
| ); | |
| logConfigUpdated(runtime); | |
| runtime.log(`Auth profile: ${profileId} (${provider}/token)`); | |
| } | |
| export async function modelsAuthPasteTokenCommand( | |
| opts: { | |
| provider?: string; | |
| profileId?: string; | |
| expiresIn?: string; | |
| }, | |
| runtime: RuntimeEnv, | |
| ) { | |
| const rawProvider = opts.provider?.trim(); | |
| if (!rawProvider) { | |
| throw new Error("Missing --provider."); | |
| } | |
| const provider = normalizeProviderId(rawProvider); | |
| const profileId = opts.profileId?.trim() || resolveDefaultTokenProfileId(provider); | |
| const tokenInput = await text({ | |
| message: `Paste token for ${provider}`, | |
| validate: (value) => (value?.trim() ? undefined : "Required"), | |
| }); | |
| const token = String(tokenInput).trim(); | |
| const expires = | |
| opts.expiresIn?.trim() && opts.expiresIn.trim().length > 0 | |
| ? Date.now() + parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" }) | |
| : undefined; | |
| upsertAuthProfile({ | |
| profileId, | |
| credential: { | |
| type: "token", | |
| provider, | |
| token, | |
| ...(expires ? { expires } : {}), | |
| }, | |
| }); | |
| await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" })); | |
| logConfigUpdated(runtime); | |
| runtime.log(`Auth profile: ${profileId} (${provider}/token)`); | |
| } | |
| export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) { | |
| const provider = (await select({ | |
| message: "Token provider", | |
| options: [ | |
| { value: "anthropic", label: "anthropic" }, | |
| { value: "custom", label: "custom (type provider id)" }, | |
| ], | |
| })) as TokenProvider | "custom"; | |
| const providerId = | |
| provider === "custom" | |
| ? normalizeProviderId( | |
| String( | |
| await text({ | |
| message: "Provider id", | |
| validate: (value) => (value?.trim() ? undefined : "Required"), | |
| }), | |
| ), | |
| ) | |
| : provider; | |
| const method = (await select({ | |
| message: "Token method", | |
| options: [ | |
| ...(providerId === "anthropic" | |
| ? [ | |
| { | |
| value: "setup-token", | |
| label: "setup-token (claude)", | |
| hint: "Paste a setup-token from `claude setup-token`", | |
| }, | |
| ] | |
| : []), | |
| { value: "paste", label: "paste token" }, | |
| ], | |
| })) as "setup-token" | "paste"; | |
| if (method === "setup-token") { | |
| await modelsAuthSetupTokenCommand({ provider: providerId }, runtime); | |
| return; | |
| } | |
| const profileIdDefault = resolveDefaultTokenProfileId(providerId); | |
| const profileId = String( | |
| await text({ | |
| message: "Profile id", | |
| initialValue: profileIdDefault, | |
| validate: (value) => (value?.trim() ? undefined : "Required"), | |
| }), | |
| ).trim(); | |
| const wantsExpiry = await confirm({ | |
| message: "Does this token expire?", | |
| initialValue: false, | |
| }); | |
| const expiresIn = wantsExpiry | |
| ? String( | |
| await text({ | |
| message: "Expires in (duration)", | |
| initialValue: "365d", | |
| validate: (value) => { | |
| try { | |
| parseDurationMs(String(value ?? ""), { defaultUnit: "d" }); | |
| return undefined; | |
| } catch { | |
| return "Invalid duration (e.g. 365d, 12h, 30m)"; | |
| } | |
| }, | |
| }), | |
| ).trim() | |
| : undefined; | |
| await modelsAuthPasteTokenCommand({ provider: providerId, profileId, expiresIn }, runtime); | |
| } | |
| type LoginOptions = { | |
| provider?: string; | |
| method?: string; | |
| setDefault?: boolean; | |
| }; | |
| function resolveProviderMatch( | |
| providers: ProviderPlugin[], | |
| rawProvider?: string, | |
| ): ProviderPlugin | null { | |
| const raw = rawProvider?.trim(); | |
| if (!raw) { | |
| return null; | |
| } | |
| const normalized = normalizeProviderId(raw); | |
| return ( | |
| providers.find((provider) => normalizeProviderId(provider.id) === normalized) ?? | |
| providers.find( | |
| (provider) => | |
| provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false, | |
| ) ?? | |
| null | |
| ); | |
| } | |
| function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null { | |
| const raw = rawMethod?.trim(); | |
| if (!raw) { | |
| return null; | |
| } | |
| const normalized = raw.toLowerCase(); | |
| return ( | |
| provider.auth.find((method) => method.id.toLowerCase() === normalized) ?? | |
| provider.auth.find((method) => method.label.toLowerCase() === normalized) ?? | |
| null | |
| ); | |
| } | |
| function isPlainRecord(value: unknown): value is Record<string, unknown> { | |
| return Boolean(value && typeof value === "object" && !Array.isArray(value)); | |
| } | |
| function mergeConfigPatch<T>(base: T, patch: unknown): T { | |
| if (!isPlainRecord(base) || !isPlainRecord(patch)) { | |
| return patch as T; | |
| } | |
| const next: Record<string, unknown> = { ...base }; | |
| for (const [key, value] of Object.entries(patch)) { | |
| const existing = next[key]; | |
| if (isPlainRecord(existing) && isPlainRecord(value)) { | |
| next[key] = mergeConfigPatch(existing, value); | |
| } else { | |
| next[key] = value; | |
| } | |
| } | |
| return next as T; | |
| } | |
| function applyDefaultModel(cfg: OpenClawConfig, model: string): OpenClawConfig { | |
| const models = { ...cfg.agents?.defaults?.models }; | |
| models[model] = models[model] ?? {}; | |
| const existingModel = cfg.agents?.defaults?.model; | |
| return { | |
| ...cfg, | |
| agents: { | |
| ...cfg.agents, | |
| defaults: { | |
| ...cfg.agents?.defaults, | |
| models, | |
| model: { | |
| ...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel | |
| ? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks } | |
| : undefined), | |
| primary: model, | |
| }, | |
| }, | |
| }, | |
| }; | |
| } | |
| function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" | "token" { | |
| if (credential.type === "api_key") { | |
| return "api_key"; | |
| } | |
| if (credential.type === "token") { | |
| return "token"; | |
| } | |
| return "oauth"; | |
| } | |
| export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: RuntimeEnv) { | |
| if (!process.stdin.isTTY) { | |
| throw new Error("models auth login requires an interactive TTY."); | |
| } | |
| const snapshot = await readConfigFileSnapshot(); | |
| if (!snapshot.valid) { | |
| const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"); | |
| throw new Error(`Invalid config at ${snapshot.path}\n${issues}`); | |
| } | |
| const config = snapshot.config; | |
| const defaultAgentId = resolveDefaultAgentId(config); | |
| const agentDir = resolveAgentDir(config, defaultAgentId); | |
| const workspaceDir = | |
| resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir(); | |
| const providers = resolvePluginProviders({ config, workspaceDir }); | |
| if (providers.length === 0) { | |
| throw new Error( | |
| `No provider plugins found. Install one via \`${formatCliCommand("openclaw plugins install")}\`.`, | |
| ); | |
| } | |
| const prompter = createClackPrompter(); | |
| const selectedProvider = | |
| resolveProviderMatch(providers, opts.provider) ?? | |
| (await prompter | |
| .select({ | |
| message: "Select a provider", | |
| options: providers.map((provider) => ({ | |
| value: provider.id, | |
| label: provider.label, | |
| hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined, | |
| })), | |
| }) | |
| .then((id) => resolveProviderMatch(providers, String(id)))); | |
| if (!selectedProvider) { | |
| throw new Error("Unknown provider. Use --provider <id> to pick a provider plugin."); | |
| } | |
| const chosenMethod = | |
| pickAuthMethod(selectedProvider, opts.method) ?? | |
| (selectedProvider.auth.length === 1 | |
| ? selectedProvider.auth[0] | |
| : await prompter | |
| .select({ | |
| message: `Auth method for ${selectedProvider.label}`, | |
| options: selectedProvider.auth.map((method) => ({ | |
| value: method.id, | |
| label: method.label, | |
| hint: method.hint, | |
| })), | |
| }) | |
| .then((id) => selectedProvider.auth.find((method) => method.id === String(id)))); | |
| if (!chosenMethod) { | |
| throw new Error("Unknown auth method. Use --method <id> to select one."); | |
| } | |
| const isRemote = isRemoteEnvironment(); | |
| const result: ProviderAuthResult = await chosenMethod.run({ | |
| config, | |
| agentDir, | |
| workspaceDir, | |
| prompter, | |
| runtime, | |
| isRemote, | |
| openUrl: async (url) => { | |
| await openUrl(url); | |
| }, | |
| oauth: { | |
| createVpsAwareHandlers: (params) => createVpsAwareOAuthHandlers(params), | |
| }, | |
| }); | |
| for (const profile of result.profiles) { | |
| upsertAuthProfile({ | |
| profileId: profile.profileId, | |
| credential: profile.credential, | |
| agentDir, | |
| }); | |
| } | |
| await updateConfig((cfg) => { | |
| let next = cfg; | |
| if (result.configPatch) { | |
| next = mergeConfigPatch(next, result.configPatch); | |
| } | |
| for (const profile of result.profiles) { | |
| next = applyAuthProfileConfig(next, { | |
| profileId: profile.profileId, | |
| provider: profile.credential.provider, | |
| mode: credentialMode(profile.credential), | |
| }); | |
| } | |
| if (opts.setDefault && result.defaultModel) { | |
| next = applyDefaultModel(next, result.defaultModel); | |
| } | |
| return next; | |
| }); | |
| logConfigUpdated(runtime); | |
| for (const profile of result.profiles) { | |
| runtime.log( | |
| `Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`, | |
| ); | |
| } | |
| if (result.defaultModel) { | |
| runtime.log( | |
| opts.setDefault | |
| ? `Default model set to ${result.defaultModel}` | |
| : `Default model available: ${result.defaultModel} (use --set-default to apply)`, | |
| ); | |
| } | |
| if (result.notes && result.notes.length > 0) { | |
| await prompter.note(result.notes.join("\n"), "Provider notes"); | |
| } | |
| } | |