refactor: unify configure auth choice
This commit is contained in:
@@ -1,57 +0,0 @@
|
|||||||
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
|
|
||||||
index 93aa26c395e9bd0df64376408a13d15ee9e7cce7..41a439e5fc370038a5febef9e8f021ee279cf8aa 100644
|
|
||||||
--- a/dist/providers/google-gemini-cli.js
|
|
||||||
+++ b/dist/providers/google-gemini-cli.js
|
|
||||||
@@ -248,6 +248,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
|
|
||||||
break; // Success, exit retry loop
|
|
||||||
}
|
|
||||||
const errorText = await response.text();
|
|
||||||
+ // Fail immediately on 429 for Antigravity to let callers rotate accounts.
|
|
||||||
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
|
|
||||||
+ if (isAntigravity && response.status === 429) {
|
|
||||||
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
|
|
||||||
+ }
|
|
||||||
// Check if retryable
|
|
||||||
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
||||||
// Use server-provided delay or exponential backoff
|
|
||||||
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
|
|
||||||
index 188a8294f26fe1bfe3fb298a7f58e4d8eaf2a529..3fd8027edafdad4ca364af53f0a1811139705b21 100644
|
|
||||||
--- a/dist/providers/openai-codex-responses.js
|
|
||||||
+++ b/dist/providers/openai-codex-responses.js
|
|
||||||
@@ -433,9 +433,15 @@ function convertMessages(model, context) {
|
|
||||||
}
|
|
||||||
else if (msg.role === "assistant") {
|
|
||||||
const output = [];
|
|
||||||
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`.
|
|
||||||
+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but
|
|
||||||
+ // their stored reasoning items must not be replayed as standalone `reasoning` input.
|
|
||||||
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
|
|
||||||
for (const block of msg.content) {
|
|
||||||
if (block.type === "thinking" && msg.stopReason !== "error") {
|
|
||||||
if (block.thinkingSignature) {
|
|
||||||
+ if (!hasTextBlock)
|
|
||||||
+ continue;
|
|
||||||
const reasoningItem = JSON.parse(block.thinkingSignature);
|
|
||||||
output.push(reasoningItem);
|
|
||||||
}
|
|
||||||
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
|
|
||||||
index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..0bf46bfb4a6fac5a0304652e42566b2c991bab48 100644
|
|
||||||
--- a/dist/providers/openai-responses.js
|
|
||||||
+++ b/dist/providers/openai-responses.js
|
|
||||||
@@ -396,10 +396,16 @@ function convertMessages(model, context) {
|
|
||||||
}
|
|
||||||
else if (msg.role === "assistant") {
|
|
||||||
const output = [];
|
|
||||||
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`.
|
|
||||||
+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but
|
|
||||||
+ // their stored reasoning items must not be replayed as standalone `reasoning` input.
|
|
||||||
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
|
|
||||||
for (const block of msg.content) {
|
|
||||||
// Do not submit thinking blocks if the completion had an error (i.e. abort)
|
|
||||||
if (block.type === "thinking" && msg.stopReason !== "error") {
|
|
||||||
if (block.thinkingSignature) {
|
|
||||||
+ if (!hasTextBlock)
|
|
||||||
+ continue;
|
|
||||||
const reasoningItem = JSON.parse(block.thinkingSignature);
|
|
||||||
output.push(reasoningItem);
|
|
||||||
}
|
|
||||||
@@ -10,4 +10,4 @@ onlyBuiltDependencies:
|
|||||||
- sharp
|
- sharp
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@mariozechner/pi-ai@0.42.1': patches/@mariozechner__pi-ai@0.42.1.patch
|
'@mariozechner/pi-ai@0.42.2': patches/@mariozechner__pi-ai@0.42.2.patch
|
||||||
|
|||||||
@@ -8,21 +8,8 @@ import {
|
|||||||
outro as clackOutro,
|
outro as clackOutro,
|
||||||
select as clackSelect,
|
select as clackSelect,
|
||||||
text as clackText,
|
text as clackText,
|
||||||
spinner,
|
|
||||||
} from "@clack/prompts";
|
} from "@clack/prompts";
|
||||||
import {
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
loginOpenAICodex,
|
|
||||||
type OAuthCredentials,
|
|
||||||
type OAuthProvider,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
import {
|
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
|
||||||
CODEX_CLI_PROFILE_ID,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
upsertAuthProfile,
|
|
||||||
} from "../agents/auth-profiles.js";
|
|
||||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
|
||||||
import { createCliProgress } from "../cli/progress.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDBOT,
|
CONFIG_PATH_CLAWDBOT,
|
||||||
@@ -36,7 +23,6 @@ import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
|||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
|
||||||
import { listChatProviders } from "../providers/registry.js";
|
import { listChatProviders } from "../providers/registry.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
@@ -45,47 +31,26 @@ import {
|
|||||||
stylePromptMessage,
|
stylePromptMessage,
|
||||||
stylePromptTitle,
|
stylePromptTitle,
|
||||||
} from "../terminal/prompt-style.js";
|
} from "../terminal/prompt-style.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
|
||||||
import { resolveUserPath, sleep } from "../utils.js";
|
import { resolveUserPath, sleep } from "../utils.js";
|
||||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||||
import {
|
import {
|
||||||
isRemoteEnvironment,
|
WizardCancelledError,
|
||||||
loginAntigravityVpsAware,
|
type WizardPrompter,
|
||||||
} from "./antigravity-oauth.js";
|
} from "../wizard/prompts.js";
|
||||||
|
import { applyAuthChoice } from "./auth-choice.js";
|
||||||
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
||||||
import {
|
|
||||||
buildTokenProfileId,
|
|
||||||
validateAnthropicSetupToken,
|
|
||||||
} from "./auth-token.js";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
type GatewayDaemonRuntime,
|
type GatewayDaemonRuntime,
|
||||||
} from "./daemon-runtime.js";
|
} from "./daemon-runtime.js";
|
||||||
import {
|
|
||||||
applyGoogleGeminiModelDefault,
|
|
||||||
GOOGLE_GEMINI_DEFAULT_MODEL,
|
|
||||||
} from "./google-gemini-model-default.js";
|
|
||||||
import { healthCommand } from "./health.js";
|
import { healthCommand } from "./health.js";
|
||||||
import { formatHealthCheckFailure } from "./health-format.js";
|
import { formatHealthCheckFailure } from "./health-format.js";
|
||||||
import {
|
|
||||||
applyAuthProfileConfig,
|
|
||||||
applyMinimaxApiConfig,
|
|
||||||
applyMinimaxConfig,
|
|
||||||
applyMinimaxHostedConfig,
|
|
||||||
applyOpencodeZenConfig,
|
|
||||||
setAnthropicApiKey,
|
|
||||||
setGeminiApiKey,
|
|
||||||
setMinimaxApiKey,
|
|
||||||
setOpencodeZenApiKey,
|
|
||||||
writeOAuthCredentials,
|
|
||||||
} from "./onboard-auth.js";
|
|
||||||
import {
|
import {
|
||||||
applyWizardMetadata,
|
applyWizardMetadata,
|
||||||
DEFAULT_WORKSPACE,
|
DEFAULT_WORKSPACE,
|
||||||
ensureWorkspaceAndSessions,
|
ensureWorkspaceAndSessions,
|
||||||
guardCancel,
|
guardCancel,
|
||||||
openUrl,
|
|
||||||
printWizardHeader,
|
printWizardHeader,
|
||||||
probeGatewayReachable,
|
probeGatewayReachable,
|
||||||
randomToken,
|
randomToken,
|
||||||
@@ -95,11 +60,7 @@ import {
|
|||||||
import { setupProviders } from "./onboard-providers.js";
|
import { setupProviders } from "./onboard-providers.js";
|
||||||
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
||||||
import { setupSkills } from "./onboard-skills.js";
|
import { setupSkills } from "./onboard-skills.js";
|
||||||
import {
|
import type { AuthChoice } from "./onboard-types.js";
|
||||||
applyOpenAICodexModelDefault,
|
|
||||||
OPENAI_CODEX_DEFAULT_MODEL,
|
|
||||||
} from "./openai-codex-model-default.js";
|
|
||||||
import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js";
|
|
||||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||||
|
|
||||||
export const CONFIGURE_WIZARD_SECTIONS = [
|
export const CONFIGURE_WIZARD_SECTIONS = [
|
||||||
@@ -158,27 +119,6 @@ const multiselect = <T>(params: Parameters<typeof clackMultiselect<T>>[0]) =>
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const startOscSpinner = (label: string) => {
|
|
||||||
const spin = spinner();
|
|
||||||
spin.start(theme.accent(label));
|
|
||||||
const osc = createCliProgress({
|
|
||||||
label,
|
|
||||||
indeterminate: true,
|
|
||||||
enabled: true,
|
|
||||||
fallback: "none",
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
update: (message: string) => {
|
|
||||||
spin.message(theme.accent(message));
|
|
||||||
osc.setLabel(message);
|
|
||||||
},
|
|
||||||
stop: (message: string) => {
|
|
||||||
osc.done();
|
|
||||||
spin.stop(message);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function promptGatewayConfig(
|
async function promptGatewayConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
@@ -345,9 +285,9 @@ async function promptGatewayConfig(
|
|||||||
async function promptAuthConfig(
|
async function promptAuthConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
|
prompter: WizardPrompter,
|
||||||
): Promise<ClawdbotConfig> {
|
): Promise<ClawdbotConfig> {
|
||||||
const authChoice = guardCancel(
|
const authChoice: AuthChoice = await prompter.select({
|
||||||
await select({
|
|
||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: buildAuthChoiceOptions({
|
options: buildAuthChoiceOptions({
|
||||||
store: ensureAuthProfileStore(undefined, {
|
store: ensureAuthProfileStore(undefined, {
|
||||||
@@ -356,481 +296,18 @@ async function promptAuthConfig(
|
|||||||
includeSkip: true,
|
includeSkip: true,
|
||||||
includeClaudeCliIfMissing: true,
|
includeClaudeCliIfMissing: true,
|
||||||
}),
|
}),
|
||||||
}),
|
});
|
||||||
runtime,
|
|
||||||
) as
|
|
||||||
| "oauth"
|
|
||||||
| "setup-token"
|
|
||||||
| "claude-cli"
|
|
||||||
| "token"
|
|
||||||
| "openai-codex"
|
|
||||||
| "openai-api-key"
|
|
||||||
| "codex-cli"
|
|
||||||
| "antigravity"
|
|
||||||
| "gemini-api-key"
|
|
||||||
| "apiKey"
|
|
||||||
| "minimax-cloud"
|
|
||||||
| "minimax-api"
|
|
||||||
| "minimax"
|
|
||||||
| "opencode-zen"
|
|
||||||
| "skip";
|
|
||||||
|
|
||||||
let next = cfg;
|
let next = cfg;
|
||||||
|
if (authChoice !== "skip") {
|
||||||
if (authChoice === "claude-cli") {
|
const applied = await applyAuthChoice({
|
||||||
const store = ensureAuthProfileStore(undefined, {
|
authChoice,
|
||||||
allowKeychainPrompt: false,
|
config: next,
|
||||||
});
|
prompter,
|
||||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID] && process.stdin.isTTY) {
|
|
||||||
note(
|
|
||||||
[
|
|
||||||
"No Claude CLI credentials found yet.",
|
|
||||||
"If you have a Claude Pro/Max subscription, run `claude setup-token`.",
|
|
||||||
].join("\n"),
|
|
||||||
"Claude CLI",
|
|
||||||
);
|
|
||||||
const runNow = guardCancel(
|
|
||||||
await confirm({
|
|
||||||
message: "Run `claude setup-token` now?",
|
|
||||||
initialValue: true,
|
|
||||||
}),
|
|
||||||
runtime,
|
runtime,
|
||||||
);
|
setDefaultModel: true,
|
||||||
if (runNow) {
|
|
||||||
const res = await (async () => {
|
|
||||||
const { spawnSync } = await import("node:child_process");
|
|
||||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
|
||||||
})();
|
|
||||||
if (res.error) {
|
|
||||||
note(
|
|
||||||
`Failed to run claude: ${String(res.error)}`,
|
|
||||||
"Claude setup-token",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
|
||||||
provider: "anthropic",
|
|
||||||
mode: "token",
|
|
||||||
});
|
});
|
||||||
} else if (authChoice === "setup-token" || authChoice === "oauth") {
|
next = applied.config;
|
||||||
note(
|
|
||||||
[
|
|
||||||
"This will run `claude setup-token` to create a long-lived Anthropic token.",
|
|
||||||
"Requires an interactive TTY and a Claude Pro/Max subscription.",
|
|
||||||
].join("\n"),
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.stdin.isTTY) {
|
|
||||||
note(
|
|
||||||
"`claude setup-token` requires an interactive TTY.",
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runNow = guardCancel(
|
|
||||||
await confirm({
|
|
||||||
message: "Run `claude setup-token` now?",
|
|
||||||
initialValue: true,
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
if (!runNow) return next;
|
|
||||||
|
|
||||||
const res = await (async () => {
|
|
||||||
const { spawnSync } = await import("node:child_process");
|
|
||||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
|
||||||
})();
|
|
||||||
if (res.error) {
|
|
||||||
note(
|
|
||||||
`Failed to run claude: ${String(res.error)}`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
if (typeof res.status === "number" && res.status !== 0) {
|
|
||||||
note(
|
|
||||||
`claude setup-token failed (exit ${res.status})`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(undefined, {
|
|
||||||
allowKeychainPrompt: true,
|
|
||||||
});
|
|
||||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
|
||||||
note(
|
|
||||||
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
|
||||||
provider: "anthropic",
|
|
||||||
mode: "token",
|
|
||||||
});
|
|
||||||
} else if (authChoice === "token") {
|
|
||||||
const provider = guardCancel(
|
|
||||||
await select({
|
|
||||||
message: "Token provider",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "anthropic",
|
|
||||||
label: "Anthropic (only supported)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
) as "anthropic";
|
|
||||||
|
|
||||||
note(
|
|
||||||
[
|
|
||||||
"Run `claude setup-token` in your terminal.",
|
|
||||||
"Then paste the generated token below.",
|
|
||||||
].join("\n"),
|
|
||||||
"Anthropic token",
|
|
||||||
);
|
|
||||||
|
|
||||||
const tokenRaw = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Paste Anthropic setup-token",
|
|
||||||
validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
const token = String(tokenRaw).trim();
|
|
||||||
|
|
||||||
const profileNameRaw = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Token name (blank = default)",
|
|
||||||
placeholder: "default",
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
const profileId = buildTokenProfileId({
|
|
||||||
provider,
|
|
||||||
name: String(profileNameRaw ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
upsertAuthProfile({
|
|
||||||
profileId,
|
|
||||||
credential: {
|
|
||||||
type: "token",
|
|
||||||
provider,
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
next = applyAuthProfileConfig(next, { profileId, provider, mode: "token" });
|
|
||||||
} else if (authChoice === "openai-api-key") {
|
|
||||||
const envKey = resolveEnvApiKey("openai");
|
|
||||||
if (envKey) {
|
|
||||||
const useExisting = guardCancel(
|
|
||||||
await confirm({
|
|
||||||
message: `Use existing OPENAI_API_KEY (${envKey.source})?`,
|
|
||||||
initialValue: true,
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
if (useExisting) {
|
|
||||||
const result = upsertSharedEnvVar({
|
|
||||||
key: "OPENAI_API_KEY",
|
|
||||||
value: envKey.apiKey,
|
|
||||||
});
|
|
||||||
if (!process.env.OPENAI_API_KEY) {
|
|
||||||
process.env.OPENAI_API_KEY = envKey.apiKey;
|
|
||||||
}
|
|
||||||
note(
|
|
||||||
`Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`,
|
|
||||||
"OpenAI API key",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter OpenAI API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
const trimmed = String(key).trim();
|
|
||||||
const result = upsertSharedEnvVar({
|
|
||||||
key: "OPENAI_API_KEY",
|
|
||||||
value: trimmed,
|
|
||||||
});
|
|
||||||
process.env.OPENAI_API_KEY = trimmed;
|
|
||||||
note(
|
|
||||||
`Saved OPENAI_API_KEY to ${result.path} for launchd compatibility.`,
|
|
||||||
"OpenAI API key",
|
|
||||||
);
|
|
||||||
} else if (authChoice === "openai-codex") {
|
|
||||||
const isRemote = isRemoteEnvironment();
|
|
||||||
note(
|
|
||||||
isRemote
|
|
||||||
? [
|
|
||||||
"You are running in a remote/VPS environment.",
|
|
||||||
"A URL will be shown for you to open in your LOCAL browser.",
|
|
||||||
"After signing in, paste the redirect URL back here.",
|
|
||||||
].join("\n")
|
|
||||||
: [
|
|
||||||
"Browser will open for OpenAI authentication.",
|
|
||||||
"If the callback doesn't auto-complete, paste the redirect URL.",
|
|
||||||
"OpenAI OAuth uses localhost:1455 for the callback.",
|
|
||||||
].join("\n"),
|
|
||||||
"OpenAI Codex OAuth",
|
|
||||||
);
|
|
||||||
const spin = startOscSpinner("Starting OAuth flow…");
|
|
||||||
let manualCodePromise: Promise<string> | undefined;
|
|
||||||
try {
|
|
||||||
const creds = await loginOpenAICodex({
|
|
||||||
onAuth: async ({ url }) => {
|
|
||||||
if (isRemote) {
|
|
||||||
spin.update("OAuth URL ready (see below)…");
|
|
||||||
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
|
||||||
manualCodePromise = text({
|
|
||||||
message: "Paste the redirect URL (or authorization code)",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}).then((value) => String(guardCancel(value, runtime)));
|
|
||||||
} else {
|
|
||||||
spin.update("Complete sign-in in browser…");
|
|
||||||
await openUrl(url);
|
|
||||||
runtime.log(`Open: ${url}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPrompt: async (prompt) => {
|
|
||||||
if (manualCodePromise) return manualCodePromise;
|
|
||||||
const code = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: prompt.message,
|
|
||||||
placeholder: prompt.placeholder,
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
return String(code);
|
|
||||||
},
|
|
||||||
onProgress: (msg) => spin.update(msg),
|
|
||||||
});
|
|
||||||
spin.stop("OpenAI OAuth complete");
|
|
||||||
if (creds) {
|
|
||||||
await writeOAuthCredentials(
|
|
||||||
"openai-codex" as unknown as OAuthProvider,
|
|
||||||
creds,
|
|
||||||
);
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "openai-codex:default",
|
|
||||||
provider: "openai-codex",
|
|
||||||
mode: "oauth",
|
|
||||||
});
|
|
||||||
const applied = applyOpenAICodexModelDefault(next);
|
|
||||||
next = applied.next;
|
|
||||||
if (applied.changed) {
|
|
||||||
note(
|
|
||||||
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
spin.stop("OpenAI OAuth failed");
|
|
||||||
runtime.error(String(err));
|
|
||||||
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
|
||||||
}
|
|
||||||
} else if (authChoice === "codex-cli") {
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: CODEX_CLI_PROFILE_ID,
|
|
||||||
provider: "openai-codex",
|
|
||||||
mode: "oauth",
|
|
||||||
});
|
|
||||||
const applied = applyOpenAICodexModelDefault(next);
|
|
||||||
next = applied.next;
|
|
||||||
if (applied.changed) {
|
|
||||||
note(
|
|
||||||
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (authChoice === "antigravity") {
|
|
||||||
const isRemote = isRemoteEnvironment();
|
|
||||||
note(
|
|
||||||
isRemote
|
|
||||||
? [
|
|
||||||
"You are running in a remote/VPS environment.",
|
|
||||||
"A URL will be shown for you to open in your LOCAL browser.",
|
|
||||||
"After signing in, copy the redirect URL and paste it back here.",
|
|
||||||
].join("\n")
|
|
||||||
: [
|
|
||||||
"Browser will open for Google authentication.",
|
|
||||||
"Sign in with your Google account that has Antigravity access.",
|
|
||||||
"The callback will be captured automatically on localhost:51121.",
|
|
||||||
].join("\n"),
|
|
||||||
"Google Antigravity OAuth",
|
|
||||||
);
|
|
||||||
const spin = startOscSpinner("Starting OAuth flow…");
|
|
||||||
let oauthCreds: OAuthCredentials | null = null;
|
|
||||||
try {
|
|
||||||
oauthCreds = await loginAntigravityVpsAware(
|
|
||||||
async (url) => {
|
|
||||||
if (isRemote) {
|
|
||||||
spin.stop("OAuth URL ready");
|
|
||||||
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
|
||||||
} else {
|
|
||||||
spin.update("Complete sign-in in browser…");
|
|
||||||
await openUrl(url);
|
|
||||||
runtime.log(`Open: ${url}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(msg) => spin.update(msg),
|
|
||||||
);
|
|
||||||
spin.stop("Antigravity OAuth complete");
|
|
||||||
if (oauthCreds) {
|
|
||||||
await writeOAuthCredentials("google-antigravity", oauthCreds);
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: `google-antigravity:${oauthCreds.email ?? "default"}`,
|
|
||||||
provider: "google-antigravity",
|
|
||||||
mode: "oauth",
|
|
||||||
});
|
|
||||||
// Set default model to Claude Opus 4.5 via Antigravity
|
|
||||||
const existingDefaults = next.agents?.defaults;
|
|
||||||
const existingModel = existingDefaults?.model;
|
|
||||||
const existingModels = existingDefaults?.models;
|
|
||||||
next = {
|
|
||||||
...next,
|
|
||||||
agents: {
|
|
||||||
...next.agents,
|
|
||||||
defaults: {
|
|
||||||
...existingDefaults,
|
|
||||||
model: {
|
|
||||||
...(existingModel &&
|
|
||||||
"fallbacks" in (existingModel as Record<string, unknown>)
|
|
||||||
? {
|
|
||||||
fallbacks: (existingModel as { fallbacks?: string[] })
|
|
||||||
.fallbacks,
|
|
||||||
}
|
|
||||||
: undefined),
|
|
||||||
primary: "google-antigravity/claude-opus-4-5-thinking",
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
...existingModels,
|
|
||||||
"google-antigravity/claude-opus-4-5-thinking":
|
|
||||||
existingModels?.[
|
|
||||||
"google-antigravity/claude-opus-4-5-thinking"
|
|
||||||
] ?? {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
note(
|
|
||||||
"Default model set to google-antigravity/claude-opus-4-5-thinking",
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
spin.stop("Antigravity OAuth failed");
|
|
||||||
runtime.error(String(err));
|
|
||||||
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
|
||||||
}
|
|
||||||
} else if (authChoice === "gemini-api-key") {
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter Gemini API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
await setGeminiApiKey(String(key).trim());
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "google:default",
|
|
||||||
provider: "google",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
const applied = applyGoogleGeminiModelDefault(next);
|
|
||||||
next = applied.next;
|
|
||||||
if (applied.changed) {
|
|
||||||
note(
|
|
||||||
`Default model set to ${GOOGLE_GEMINI_DEFAULT_MODEL}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (authChoice === "apiKey") {
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter Anthropic API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
await setAnthropicApiKey(String(key).trim());
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "anthropic:default",
|
|
||||||
provider: "anthropic",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
} else if (authChoice === "minimax-cloud") {
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter MiniMax API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
await setMinimaxApiKey(String(key).trim());
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "minimax:default",
|
|
||||||
provider: "minimax",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
next = applyMinimaxHostedConfig(next);
|
|
||||||
} else if (authChoice === "minimax") {
|
|
||||||
next = applyMinimaxConfig(next);
|
|
||||||
} else if (authChoice === "minimax-api") {
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter MiniMax API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
await setMinimaxApiKey(String(key).trim());
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "minimax:default",
|
|
||||||
provider: "minimax",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
next = applyMinimaxApiConfig(next);
|
|
||||||
} else if (authChoice === "opencode-zen") {
|
|
||||||
note(
|
|
||||||
[
|
|
||||||
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
|
||||||
"Get your API key at: https://opencode.ai/auth",
|
|
||||||
].join("\n"),
|
|
||||||
"OpenCode Zen",
|
|
||||||
);
|
|
||||||
const key = guardCancel(
|
|
||||||
await text({
|
|
||||||
message: "Enter OpenCode Zen API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
await setOpencodeZenApiKey(String(key).trim());
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: "opencode-zen:default",
|
|
||||||
provider: "opencode-zen",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
next = applyOpencodeZenConfig(next);
|
|
||||||
note(
|
|
||||||
`Default model set to ${OPENCODE_ZEN_DEFAULT_MODEL}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentModel =
|
const currentModel =
|
||||||
@@ -1051,9 +528,12 @@ export async function runConfigureWizard(
|
|||||||
opts: ConfigureWizardParams,
|
opts: ConfigureWizardParams,
|
||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
printWizardHeader(runtime);
|
printWizardHeader(runtime);
|
||||||
intro(
|
intro(
|
||||||
opts.command === "update" ? "Clawdbot update wizard" : "Clawdbot configure",
|
opts.command === "update"
|
||||||
|
? "Clawdbot update wizard"
|
||||||
|
: "Clawdbot configure",
|
||||||
);
|
);
|
||||||
const prompter = createClackPrompter();
|
const prompter = createClackPrompter();
|
||||||
|
|
||||||
@@ -1225,7 +705,7 @@ export async function runConfigureWizard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selected.includes("model")) {
|
if (selected.includes("model")) {
|
||||||
nextConfig = await promptAuthConfig(nextConfig, runtime);
|
nextConfig = await promptAuthConfig(nextConfig, runtime, prompter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.includes("gateway")) {
|
if (selected.includes("gateway")) {
|
||||||
@@ -1350,6 +830,13 @@ export async function runConfigureWizard(
|
|||||||
);
|
);
|
||||||
|
|
||||||
outro("Configure complete.");
|
outro("Configure complete.");
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof WizardCancelledError) {
|
||||||
|
runtime.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function configureCommand(runtime: RuntimeEnv = defaultRuntime) {
|
export async function configureCommand(runtime: RuntimeEnv = defaultRuntime) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ describe("applyMinimaxApiConfig", () => {
|
|||||||
).toMatchObject({ alias: "Minimax", params: { custom: "value" } });
|
).toMatchObject({ alias: "Minimax", params: { custom: "value" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces existing minimax provider entirely", () => {
|
it("merges existing minimax provider models", () => {
|
||||||
const cfg = applyMinimaxApiConfig({
|
const cfg = applyMinimaxApiConfig({
|
||||||
models: {
|
models: {
|
||||||
providers: {
|
providers: {
|
||||||
@@ -204,7 +204,11 @@ describe("applyMinimaxApiConfig", () => {
|
|||||||
"https://api.minimax.io/anthropic",
|
"https://api.minimax.io/anthropic",
|
||||||
);
|
);
|
||||||
expect(cfg.models?.providers?.minimax?.api).toBe("anthropic-messages");
|
expect(cfg.models?.providers?.minimax?.api).toBe("anthropic-messages");
|
||||||
expect(cfg.models?.providers?.minimax?.models[0]?.id).toBe("MiniMax-M2.1");
|
expect(cfg.models?.providers?.minimax?.apiKey).toBe("old-key");
|
||||||
|
expect(cfg.models?.providers?.minimax?.models.map((m) => m.id)).toEqual([
|
||||||
|
"old-model",
|
||||||
|
"MiniMax-M2.1",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves other providers when adding minimax", () => {
|
it("preserves other providers when adding minimax", () => {
|
||||||
|
|||||||
@@ -334,11 +334,27 @@ export function applyMinimaxApiProviderConfig(
|
|||||||
modelId: string = "MiniMax-M2.1",
|
modelId: string = "MiniMax-M2.1",
|
||||||
): ClawdbotConfig {
|
): ClawdbotConfig {
|
||||||
const providers = { ...cfg.models?.providers };
|
const providers = { ...cfg.models?.providers };
|
||||||
|
const existingProvider = providers.minimax;
|
||||||
|
const existingModels = Array.isArray(existingProvider?.models)
|
||||||
|
? existingProvider.models
|
||||||
|
: [];
|
||||||
|
const apiModel = buildMinimaxApiModelDefinition(modelId);
|
||||||
|
const hasApiModel = existingModels.some((model) => model.id === modelId);
|
||||||
|
const mergedModels = hasApiModel
|
||||||
|
? existingModels
|
||||||
|
: [...existingModels, apiModel];
|
||||||
|
const { apiKey: existingApiKey, ...existingProviderRest } =
|
||||||
|
(existingProvider ?? {}) as Record<string, unknown> as { apiKey?: string };
|
||||||
|
const resolvedApiKey =
|
||||||
|
typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||||
|
const normalizedApiKey =
|
||||||
|
resolvedApiKey?.trim() === "minimax" ? "" : resolvedApiKey;
|
||||||
providers.minimax = {
|
providers.minimax = {
|
||||||
|
...existingProviderRest,
|
||||||
baseUrl: MINIMAX_API_BASE_URL,
|
baseUrl: MINIMAX_API_BASE_URL,
|
||||||
// apiKey omitted: resolved via MINIMAX_API_KEY env var or auth profile by default.
|
|
||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [buildMinimaxApiModelDefinition(modelId)],
|
...(normalizedApiKey?.trim() ? { apiKey: normalizedApiKey } : {}),
|
||||||
|
models: mergedModels.length > 0 ? mergedModels : [apiModel],
|
||||||
};
|
};
|
||||||
|
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
|
|||||||
Reference in New Issue
Block a user