diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 84e7119ad..cb2df5f3a 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -13,6 +13,7 @@ export type AuthChoiceGroupId = | "openai" | "anthropic" | "google" + | "copilot" | "openrouter" | "ai-gateway" | "moonshot" @@ -68,8 +69,14 @@ const AUTH_CHOICE_GROUP_DEFS: { { value: "google", label: "Google", - hint: "Gemini API key", - choices: ["gemini-api-key"], + hint: "Gemini API key + OAuth", + choices: ["gemini-api-key", "google-antigravity", "google-gemini-cli"], + }, + { + value: "copilot", + label: "Copilot", + hint: "GitHub + local proxy", + choices: ["github-copilot", "copilot-proxy"], }, { value: "openrouter", @@ -195,8 +202,23 @@ export function buildAuthChoiceOptions(params: { hint: "Uses GitHub device flow", }); options.push({ value: "gemini-api-key", label: "Google Gemini API key" }); + options.push({ + value: "google-antigravity", + label: "Google Antigravity OAuth", + hint: "Uses the bundled Antigravity auth plugin", + }); + options.push({ + value: "google-gemini-cli", + label: "Google Gemini CLI OAuth", + hint: "Uses the bundled Gemini CLI auth plugin", + }); options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }); options.push({ value: "qwen-portal", label: "Qwen OAuth" }); + options.push({ + value: "copilot-proxy", + label: "Copilot Proxy (local)", + hint: "Local proxy for VS Code Copilot models", + }); options.push({ value: "apiKey", label: "Anthropic API key" }); // Token flow is currently Anthropic-only; use CLI for advanced providers. options.push({ diff --git a/src/commands/auth-choice.apply.copilot-proxy.ts b/src/commands/auth-choice.apply.copilot-proxy.ts new file mode 100644 index 000000000..390684697 --- /dev/null +++ b/src/commands/auth-choice.apply.copilot-proxy.ts @@ -0,0 +1,14 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js"; + +export async function applyAuthChoiceCopilotProxy( + params: ApplyAuthChoiceParams, +): Promise { + return await applyAuthChoicePluginProvider(params, { + authChoice: "copilot-proxy", + pluginId: "copilot-proxy", + providerId: "copilot-proxy", + methodId: "local", + label: "Copilot Proxy", + }); +} diff --git a/src/commands/auth-choice.apply.google-antigravity.ts b/src/commands/auth-choice.apply.google-antigravity.ts new file mode 100644 index 000000000..6011b74e0 --- /dev/null +++ b/src/commands/auth-choice.apply.google-antigravity.ts @@ -0,0 +1,14 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js"; + +export async function applyAuthChoiceGoogleAntigravity( + params: ApplyAuthChoiceParams, +): Promise { + return await applyAuthChoicePluginProvider(params, { + authChoice: "google-antigravity", + pluginId: "google-antigravity-auth", + providerId: "google-antigravity", + methodId: "oauth", + label: "Google Antigravity", + }); +} diff --git a/src/commands/auth-choice.apply.google-gemini-cli.ts b/src/commands/auth-choice.apply.google-gemini-cli.ts new file mode 100644 index 000000000..d2a3281f6 --- /dev/null +++ b/src/commands/auth-choice.apply.google-gemini-cli.ts @@ -0,0 +1,14 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js"; + +export async function applyAuthChoiceGoogleGeminiCli( + params: ApplyAuthChoiceParams, +): Promise { + return await applyAuthChoicePluginProvider(params, { + authChoice: "google-gemini-cli", + pluginId: "google-gemini-cli-auth", + providerId: "google-gemini-cli", + methodId: "oauth", + label: "Google Gemini CLI", + }); +} diff --git a/src/commands/auth-choice.apply.plugin-provider.ts b/src/commands/auth-choice.apply.plugin-provider.ts new file mode 100644 index 000000000..fd280c894 --- /dev/null +++ b/src/commands/auth-choice.apply.plugin-provider.ts @@ -0,0 +1,197 @@ +import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; +import { + resolveDefaultAgentId, + resolveAgentDir, + resolveAgentWorkspaceDir, +} 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 type { ClawdbotConfig } from "../config/config.js"; +import { enablePluginInConfig } from "../plugins/enable.js"; +import { resolvePluginProviders } from "../plugins/providers.js"; +import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js"; +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { applyAuthProfileConfig } from "./onboard-auth.js"; +import { openUrl } from "./onboard-helpers.js"; +import { createVpsAwareOAuthHandlers } from "./oauth-flow.js"; +import { isRemoteEnvironment } from "./oauth-env.js"; + +export type PluginProviderAuthChoiceOptions = { + authChoice: string; + pluginId: string; + providerId: string; + methodId?: string; + label: string; +}; + +function resolveProviderMatch( + providers: ProviderPlugin[], + rawProvider: string, +): ProviderPlugin | null { + const normalized = normalizeProviderId(rawProvider); + 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 { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function mergeConfigPatch(base: T, patch: unknown): T { + if (!isPlainRecord(base) || !isPlainRecord(patch)) { + return patch as T; + } + + const next: Record = { ...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: ClawdbotConfig, model: string): ClawdbotConfig { + 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, + }, + }, + }, + }; +} + +export async function applyAuthChoicePluginProvider( + params: ApplyAuthChoiceParams, + options: PluginProviderAuthChoiceOptions, +): Promise { + if (params.authChoice !== options.authChoice) return null; + + const enableResult = enablePluginInConfig(params.config, options.pluginId); + let nextConfig = enableResult.config; + if (!enableResult.enabled) { + await params.prompter.note( + `${options.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`, + options.label, + ); + return { config: nextConfig }; + } + + const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig); + const defaultAgentId = resolveDefaultAgentId(nextConfig); + const agentDir = + params.agentDir ?? + (agentId === defaultAgentId ? resolveClawdbotAgentDir() : resolveAgentDir(nextConfig, agentId)); + const workspaceDir = + resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir(); + + const providers = resolvePluginProviders({ config: nextConfig, workspaceDir }); + const provider = resolveProviderMatch(providers, options.providerId); + if (!provider) { + await params.prompter.note( + `${options.label} auth plugin is not available. Enable it and re-run the wizard.`, + options.label, + ); + return { config: nextConfig }; + } + + const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0]; + if (!method) { + await params.prompter.note(`${options.label} auth method missing.`, options.label); + return { config: nextConfig }; + } + + const isRemote = isRemoteEnvironment(); + const result = await method.run({ + config: nextConfig, + agentDir, + workspaceDir, + prompter: params.prompter, + runtime: params.runtime, + isRemote, + openUrl: async (url) => { + await openUrl(url); + }, + oauth: { + createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts), + }, + }); + + if (result.configPatch) { + nextConfig = mergeConfigPatch(nextConfig, result.configPatch); + } + + for (const profile of result.profiles) { + upsertAuthProfile({ + profileId: profile.profileId, + credential: profile.credential, + agentDir, + }); + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: profile.profileId, + provider: profile.credential.provider, + mode: profile.credential.type === "token" ? "token" : profile.credential.type, + ...("email" in profile.credential && profile.credential.email + ? { email: profile.credential.email } + : {}), + }); + } + + let agentModelOverride: string | undefined; + if (result.defaultModel) { + if (params.setDefaultModel) { + nextConfig = applyDefaultModel(nextConfig, result.defaultModel); + await params.prompter.note( + `Default model set to ${result.defaultModel}`, + "Model configured", + ); + } else if (params.agentId) { + agentModelOverride = result.defaultModel; + await params.prompter.note( + `Default model set to ${result.defaultModel} for agent "${params.agentId}".`, + "Model configured", + ); + } + } + + if (result.notes && result.notes.length > 0) { + await params.prompter.note(result.notes.join("\n"), "Provider notes"); + } + + return { config: nextConfig, agentModelOverride }; +} diff --git a/src/commands/auth-choice.apply.qwen-portal.ts b/src/commands/auth-choice.apply.qwen-portal.ts index 4048c7a16..b8c975c59 100644 --- a/src/commands/auth-choice.apply.qwen-portal.ts +++ b/src/commands/auth-choice.apply.qwen-portal.ts @@ -1,195 +1,14 @@ -import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; -import { - resolveDefaultAgentId, - resolveAgentDir, - resolveAgentWorkspaceDir, -} 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 type { ClawdbotConfig } from "../config/config.js"; -import { resolvePluginProviders } from "../plugins/providers.js"; -import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; -import { applyAuthProfileConfig } from "./onboard-auth.js"; -import { openUrl } from "./onboard-helpers.js"; -import { createVpsAwareOAuthHandlers } from "./oauth-flow.js"; -import { isRemoteEnvironment } from "./oauth-env.js"; - -const PLUGIN_ID = "qwen-portal-auth"; -const PROVIDER_ID = "qwen-portal"; - -function enableBundledPlugin(cfg: ClawdbotConfig): ClawdbotConfig { - const existingEntry = cfg.plugins?.entries?.[PLUGIN_ID]; - return { - ...cfg, - plugins: { - ...cfg.plugins, - entries: { - ...cfg.plugins?.entries, - [PLUGIN_ID]: { - ...existingEntry, - enabled: true, - }, - }, - }, - }; -} - -function resolveProviderMatch( - providers: ProviderPlugin[], - rawProvider: string, -): ProviderPlugin | null { - const normalized = normalizeProviderId(rawProvider); - 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 { - return Boolean(value && typeof value === "object" && !Array.isArray(value)); -} - -function mergeConfigPatch(base: T, patch: unknown): T { - if (!isPlainRecord(base) || !isPlainRecord(patch)) { - return patch as T; - } - - const next: Record = { ...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: ClawdbotConfig, model: string): ClawdbotConfig { - 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, - }, - }, - }, - }; -} +import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js"; export async function applyAuthChoiceQwenPortal( params: ApplyAuthChoiceParams, ): Promise { - if (params.authChoice !== "qwen-portal") return null; - - let nextConfig = enableBundledPlugin(params.config); - const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig); - const defaultAgentId = resolveDefaultAgentId(nextConfig); - const agentDir = - params.agentDir ?? - (agentId === defaultAgentId ? resolveClawdbotAgentDir() : resolveAgentDir(nextConfig, agentId)); - const workspaceDir = - resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir(); - - const providers = resolvePluginProviders({ config: nextConfig, workspaceDir }); - const provider = resolveProviderMatch(providers, PROVIDER_ID); - if (!provider) { - await params.prompter.note( - "Qwen auth plugin is not available. Run `clawdbot plugins enable qwen-portal-auth` and re-run the wizard.", - "Qwen", - ); - return { config: nextConfig }; - } - - const method = pickAuthMethod(provider, "device") ?? provider.auth[0]; - if (!method) { - await params.prompter.note("Qwen auth method missing.", "Qwen"); - return { config: nextConfig }; - } - - const isRemote = isRemoteEnvironment(); - const result = await method.run({ - config: nextConfig, - agentDir, - workspaceDir, - prompter: params.prompter, - runtime: params.runtime, - isRemote, - openUrl: async (url) => { - await openUrl(url); - }, - oauth: { - createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts), - }, + return await applyAuthChoicePluginProvider(params, { + authChoice: "qwen-portal", + pluginId: "qwen-portal-auth", + providerId: "qwen-portal", + methodId: "device", + label: "Qwen", }); - - if (result.configPatch) { - nextConfig = mergeConfigPatch(nextConfig, result.configPatch); - } - - for (const profile of result.profiles) { - upsertAuthProfile({ - profileId: profile.profileId, - credential: profile.credential, - agentDir, - }); - - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: profile.profileId, - provider: profile.credential.provider, - mode: profile.credential.type === "token" ? "token" : profile.credential.type, - ...("email" in profile.credential && profile.credential.email - ? { email: profile.credential.email } - : {}), - }); - } - - let agentModelOverride: string | undefined; - if (result.defaultModel) { - if (params.setDefaultModel) { - nextConfig = applyDefaultModel(nextConfig, result.defaultModel); - await params.prompter.note(`Default model set to ${result.defaultModel}`, "Model configured"); - } else if (params.agentId) { - agentModelOverride = result.defaultModel; - await params.prompter.note( - `Default model set to ${result.defaultModel} for agent "${params.agentId}".`, - "Model configured", - ); - } - } - - if (result.notes && result.notes.length > 0) { - await params.prompter.note(result.notes.join("\n"), "Provider notes"); - } - - return { config: nextConfig, agentModelOverride }; } diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index 3120a0773..5ea040d5f 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -3,7 +3,10 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js"; import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js"; +import { applyAuthChoiceCopilotProxy } from "./auth-choice.apply.copilot-proxy.js"; import { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js"; +import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js"; +import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js"; import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js"; import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js"; import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; @@ -35,6 +38,9 @@ export async function applyAuthChoice( applyAuthChoiceApiProviders, applyAuthChoiceMiniMax, applyAuthChoiceGitHubCopilot, + applyAuthChoiceGoogleAntigravity, + applyAuthChoiceGoogleGeminiCli, + applyAuthChoiceCopilotProxy, applyAuthChoiceQwenPortal, ]; diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 5e9611da1..aeb7bac90 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -15,9 +15,12 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "moonshot-api-key": "moonshot", "kimi-code-api-key": "kimi-code", "gemini-api-key": "google", + "google-antigravity": "google-antigravity", + "google-gemini-cli": "google-gemini-cli", "zai-api-key": "zai", "synthetic-api-key": "synthetic", "github-copilot": "github-copilot", + "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", "minimax-api": "minimax", "minimax-api-lightning": "minimax", diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 63658d7fb..73d4e416f 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -1,13 +1,20 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js"; import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js"; -import { formatChannelPrimerLine, formatChannelSelectionLine } from "../channels/registry.js"; +import type { ChannelMeta } from "../channels/plugins/types.js"; +import { + formatChannelPrimerLine, + formatChannelSelectionLine, + listChatChannels, +} from "../channels/registry.js"; import type { ClawdbotConfig } from "../config/config.js"; +import { isChannelConfigured } from "../config/plugin-auto-enable.js"; import type { DmPolicy } from "../config/types.js"; import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { enablePluginInConfig } from "../plugins/enable.js"; import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js"; import type { ChannelChoice } from "./onboard-types.js"; import { @@ -115,6 +122,20 @@ async function collectChannelStatus(params: { }), ), ); + const statusByChannel = new Map(statusEntries.map((entry) => [entry.channel, entry])); + const fallbackStatuses = listChatChannels() + .filter((meta) => !statusByChannel.has(meta.id)) + .map((meta) => { + const configured = isChannelConfigured(params.cfg, meta.id); + const statusLabel = configured ? "configured (plugin disabled)" : "not configured"; + return { + channel: meta.id, + configured, + statusLines: [`${meta.label}: ${statusLabel}`], + selectionHint: configured ? "configured · plugin disabled" : "not configured", + quickstartScore: 0, + }; + }); const catalogStatuses = catalogEntries.map((entry) => ({ channel: entry.id, configured: false, @@ -122,13 +143,13 @@ async function collectChannelStatus(params: { selectionHint: "plugin · install", quickstartScore: 0, })); - const combinedStatuses = [...statusEntries, ...catalogStatuses]; - const statusByChannel = new Map(combinedStatuses.map((entry) => [entry.channel, entry])); + const combinedStatuses = [...statusEntries, ...fallbackStatuses, ...catalogStatuses]; + const mergedStatusByChannel = new Map(combinedStatuses.map((entry) => [entry.channel, entry])); const statusLines = combinedStatuses.flatMap((entry) => entry.statusLines); return { installedPlugins, catalogEntries, - statusByChannel, + statusByChannel: mergedStatusByChannel, statusLines, }; } @@ -270,17 +291,28 @@ export async function setupChannels( }); if (!shouldConfigure) return cfg; + const corePrimer = listChatChannels().map((meta) => ({ + id: meta.id as ChannelChoice, + label: meta.label, + blurb: meta.blurb, + })); + const coreIds = new Set(corePrimer.map((entry) => entry.id)); const primerChannels = [ - ...installedPlugins.map((plugin) => ({ - id: plugin.id as ChannelChoice, - label: plugin.meta.label, - blurb: plugin.meta.blurb, - })), - ...catalogEntries.map((entry) => ({ - id: entry.id as ChannelChoice, - label: entry.meta.label, - blurb: entry.meta.blurb, - })), + ...corePrimer, + ...installedPlugins + .filter((plugin) => !coreIds.has(plugin.id as ChannelChoice)) + .map((plugin) => ({ + id: plugin.id as ChannelChoice, + label: plugin.meta.label, + blurb: plugin.meta.blurb, + })), + ...catalogEntries + .filter((entry) => !coreIds.has(entry.id as ChannelChoice)) + .map((entry) => ({ + id: entry.id as ChannelChoice, + label: entry.meta.label, + blurb: entry.meta.blurb, + })), ]; await noteChannelPrimer(prompter, primerChannels); @@ -301,7 +333,11 @@ export async function setupChannels( const resolveDisabledHint = (channel: ChannelChoice): string | undefined => { const plugin = getChannelPlugin(channel); - if (!plugin) return undefined; + if (!plugin) { + if (next.plugins?.entries?.[channel]?.enabled === false) return "plugin disabled"; + if (next.plugins?.enabled === false) return "plugins disabled"; + return undefined; + } const accountId = resolveChannelDefaultAccountId({ plugin, cfg: next }); const account = plugin.config.resolveAccount(next, accountId); let enabled: boolean | undefined; @@ -336,21 +372,28 @@ export async function setupChannels( }); const getChannelEntries = () => { + const core = listChatChannels(); const installed = listChannelPlugins(); const installedIds = new Set(installed.map((plugin) => plugin.id)); const catalog = listChannelPluginCatalogEntries().filter( (entry) => !installedIds.has(entry.id), ); - const entries = [ - ...installed.map((plugin) => ({ - id: plugin.id as ChannelChoice, - meta: plugin.meta, - })), - ...catalog.map((entry) => ({ - id: entry.id as ChannelChoice, - meta: entry.meta, - })), - ]; + const metaById = new Map(); + for (const meta of core) { + metaById.set(meta.id, meta); + } + for (const plugin of installed) { + metaById.set(plugin.id, plugin.meta); + } + for (const entry of catalog) { + if (!metaById.has(entry.id)) { + metaById.set(entry.id, entry.meta); + } + } + const entries = Array.from(metaById, ([id, meta]) => ({ + id: id as ChannelChoice, + meta, + })); return { entries, catalog, @@ -365,6 +408,31 @@ export async function setupChannels( statusByChannel.set(channel, status); }; + const ensureBundledPluginEnabled = async (channel: ChannelChoice): Promise => { + if (getChannelPlugin(channel)) return true; + const result = enablePluginInConfig(next, channel); + next = result.config; + if (!result.enabled) { + await prompter.note( + `Cannot enable ${channel}: ${result.reason ?? "plugin disabled"}.`, + "Channel setup", + ); + return false; + } + const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)); + reloadOnboardingPluginRegistry({ + cfg: next, + runtime, + workspaceDir, + }); + if (!getChannelPlugin(channel)) { + await prompter.note(`${channel} plugin not available.`, "Channel setup"); + return false; + } + await refreshStatus(channel); + return true; + }; + const configureChannel = async (channel: ChannelChoice) => { const adapter = getChannelOnboardingAdapter(channel); if (!adapter) { @@ -476,6 +544,9 @@ export async function setupChannels( workspaceDir, }); await refreshStatus(channel); + } else { + const enabled = await ensureBundledPluginEnabled(channel); + if (!enabled) return; } const plugin = getChannelPlugin(channel); @@ -531,10 +602,8 @@ export async function setupChannels( options?.onSelection?.(selection); const selectionNotes = new Map(); - for (const plugin of listChannelPlugins()) { - selectionNotes.set(plugin.id, formatChannelSelectionLine(plugin.meta, formatDocsLink)); - } - for (const entry of listChannelPluginCatalogEntries()) { + const { entries: selectionEntries } = getChannelEntries(); + for (const entry of selectionEntries) { selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink)); } const selectedLines = selection diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 1f46ad4f9..90bae1025 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -19,6 +19,8 @@ export type AuthChoice = | "codex-cli" | "apiKey" | "gemini-api-key" + | "google-antigravity" + | "google-gemini-cli" | "zai-api-key" | "minimax-cloud" | "minimax" @@ -26,6 +28,7 @@ export type AuthChoice = | "minimax-api-lightning" | "opencode-zen" | "github-copilot" + | "copilot-proxy" | "qwen-portal" | "skip"; export type GatewayAuthChoice = "off" | "token" | "password"; diff --git a/src/commands/onboarding/plugin-install.ts b/src/commands/onboarding/plugin-install.ts index c6fded608..4c4443ff6 100644 --- a/src/commands/onboarding/plugin-install.ts +++ b/src/commands/onboarding/plugin-install.ts @@ -5,6 +5,7 @@ import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.j import type { ClawdbotConfig } from "../../config/config.js"; import { createSubsystemLogger } from "../../logging.js"; import { recordPluginInstall } from "../../plugins/installs.js"; +import { enablePluginInConfig } from "../../plugins/enable.js"; import { loadClawdbotPlugins } from "../../plugins/loader.js"; import { installPluginFromNpmSpec } from "../../plugins/install.js"; import type { RuntimeEnv } from "../../runtime.js"; @@ -48,37 +49,6 @@ function resolveLocalPath( return null; } -function ensurePluginEnabled(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { - const entries = { - ...cfg.plugins?.entries, - [pluginId]: { - ...(cfg.plugins?.entries?.[pluginId] as Record | undefined), - enabled: true, - }, - }; - const next: ClawdbotConfig = { - ...cfg, - plugins: { - ...cfg.plugins, - ...(cfg.plugins?.enabled === false ? { enabled: true } : {}), - entries, - }, - }; - return ensurePluginAllowlist(next, pluginId); -} - -function ensurePluginAllowlist(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { - const allow = cfg.plugins?.allow; - if (!allow || allow.includes(pluginId)) return cfg; - return { - ...cfg, - plugins: { - ...cfg.plugins, - allow: [...allow, pluginId], - }, - }; -} - function addPluginLoadPath(cfg: ClawdbotConfig, pluginPath: string): ClawdbotConfig { const existing = cfg.plugins?.load?.paths ?? []; const merged = Array.from(new Set([...existing, pluginPath])); @@ -145,7 +115,7 @@ export async function ensureOnboardingPluginInstalled(params: { if (choice === "local" && localPath) { next = addPluginLoadPath(next, localPath); - next = ensurePluginEnabled(next, entry.id); + next = enablePluginInConfig(next, entry.id).config; return { cfg: next, installed: true }; } @@ -158,7 +128,7 @@ export async function ensureOnboardingPluginInstalled(params: { }); if (result.ok) { - next = ensurePluginEnabled(next, result.pluginId); + next = enablePluginInConfig(next, result.pluginId).config; next = recordPluginInstall(next, { pluginId: result.pluginId, source: "npm", @@ -181,7 +151,7 @@ export async function ensureOnboardingPluginInstalled(params: { }); if (fallback) { next = addPluginLoadPath(next, localPath); - next = ensurePluginEnabled(next, entry.id); + next = enablePluginInConfig(next, entry.id).config; return { cfg: next, installed: true }; } }