feat(onboarding): wire plugin-backed auth choices
This commit is contained in:
@@ -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({
|
||||
|
||||
14
src/commands/auth-choice.apply.copilot-proxy.ts
Normal file
14
src/commands/auth-choice.apply.copilot-proxy.ts
Normal file
@@ -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<ApplyAuthChoiceResult | null> {
|
||||
return await applyAuthChoicePluginProvider(params, {
|
||||
authChoice: "copilot-proxy",
|
||||
pluginId: "copilot-proxy",
|
||||
providerId: "copilot-proxy",
|
||||
methodId: "local",
|
||||
label: "Copilot Proxy",
|
||||
});
|
||||
}
|
||||
14
src/commands/auth-choice.apply.google-antigravity.ts
Normal file
14
src/commands/auth-choice.apply.google-antigravity.ts
Normal file
@@ -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<ApplyAuthChoiceResult | null> {
|
||||
return await applyAuthChoicePluginProvider(params, {
|
||||
authChoice: "google-antigravity",
|
||||
pluginId: "google-antigravity-auth",
|
||||
providerId: "google-antigravity",
|
||||
methodId: "oauth",
|
||||
label: "Google Antigravity",
|
||||
});
|
||||
}
|
||||
14
src/commands/auth-choice.apply.google-gemini-cli.ts
Normal file
14
src/commands/auth-choice.apply.google-gemini-cli.ts
Normal file
@@ -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<ApplyAuthChoiceResult | null> {
|
||||
return await applyAuthChoicePluginProvider(params, {
|
||||
authChoice: "google-gemini-cli",
|
||||
pluginId: "google-gemini-cli-auth",
|
||||
providerId: "google-gemini-cli",
|
||||
methodId: "oauth",
|
||||
label: "Google Gemini CLI",
|
||||
});
|
||||
}
|
||||
197
src/commands/auth-choice.apply.plugin-provider.ts
Normal file
197
src/commands/auth-choice.apply.plugin-provider.ts
Normal file
@@ -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<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: 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<ApplyAuthChoiceResult | null> {
|
||||
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 };
|
||||
}
|
||||
@@ -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<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: 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<ApplyAuthChoiceResult | null> {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"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",
|
||||
|
||||
@@ -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<string, ChannelMeta>();
|
||||
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<boolean> => {
|
||||
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<string, string>();
|
||||
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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, unknown> | 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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user