Compare commits
2 Commits
main
...
fix/direct
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9a839ff6 | ||
|
|
15c865f340 |
@ -32,6 +32,7 @@ Docs: https://docs.clawd.bot
|
||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||
- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
|
||||
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
||||
- Onboarding: honor `--auth-choice apiKey` token provider + token flags to skip prompts. (#1485) Thanks @iHildy.
|
||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
|
||||
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
||||
|
||||
@ -295,8 +295,8 @@ Options:
|
||||
- `--mode <local|remote>`
|
||||
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token` or `apiKey`)
|
||||
- `--token <token>` (non-interactive; used with `--auth-choice token` or `apiKey`)
|
||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
|
||||
- `--anthropic-api-key <key>`
|
||||
|
||||
@ -56,9 +56,12 @@ export function registerOnboardCommand(program: Command) {
|
||||
)
|
||||
.option(
|
||||
"--token-provider <id>",
|
||||
"Token provider id (non-interactive; used with --auth-choice token)",
|
||||
"Token provider id (non-interactive; used with --auth-choice token or apiKey)",
|
||||
)
|
||||
.option(
|
||||
"--token <token>",
|
||||
"Token value (non-interactive; used with --auth-choice token or apiKey)",
|
||||
)
|
||||
.option("--token <token>", "Token value (non-interactive; used with --auth-choice token)")
|
||||
.option(
|
||||
"--token-profile-id <id>",
|
||||
"Auth profile id (non-interactive; default: <provider>:manual)",
|
||||
|
||||
183
src/commands/auth-choice.api-key-flags.test.ts
Normal file
183
src/commands/auth-choice.api-key-flags.test.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { AUTH_STORE_VERSION } from "../agents/auth-profiles/constants.js";
|
||||
import { applyAuthChoice } from "./auth-choice.js";
|
||||
|
||||
const noopAsync = async () => {};
|
||||
const noop = () => {};
|
||||
const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json");
|
||||
|
||||
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const previousOpenaiKey = process.env.OPENAI_API_KEY;
|
||||
let tempStateDir: string | null = null;
|
||||
|
||||
async function setupTempState() {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
await fs.mkdir(process.env.CLAWDBOT_AGENT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function buildPrompter() {
|
||||
const text = vi.fn(async () => "");
|
||||
const confirm = vi.fn(async () => false);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text,
|
||||
confirm,
|
||||
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||
};
|
||||
return { prompter, text, confirm };
|
||||
}
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number) => {
|
||||
throw new Error(`exit:${code}`);
|
||||
}),
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempStateDir) {
|
||||
await fs.rm(tempStateDir, { recursive: true, force: true });
|
||||
tempStateDir = null;
|
||||
}
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.CLAWDBOT_STATE_DIR;
|
||||
} else {
|
||||
process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||
}
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.CLAWDBOT_AGENT_DIR;
|
||||
} else {
|
||||
process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
if (previousPiAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
}
|
||||
if (previousAnthropicKey === undefined) {
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
} else {
|
||||
process.env.ANTHROPIC_API_KEY = previousAnthropicKey;
|
||||
}
|
||||
if (previousOpenaiKey === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = previousOpenaiKey;
|
||||
}
|
||||
});
|
||||
|
||||
describe("applyAuthChoice with apiKey flags", () => {
|
||||
it("uses provided openrouter token when authChoice=apiKey", async () => {
|
||||
await setupTempState();
|
||||
const agentDir = process.env.CLAWDBOT_AGENT_DIR ?? "";
|
||||
const authProfilePath = authProfilePathFor(agentDir);
|
||||
await fs.writeFile(
|
||||
authProfilePath,
|
||||
JSON.stringify({
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
"openrouter:legacy": {
|
||||
type: "oauth",
|
||||
provider: "openrouter",
|
||||
access: "access",
|
||||
refresh: "refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const { prompter, text, confirm } = buildPrompter();
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "apiKey",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: "openrouter",
|
||||
token: "sk-openrouter-flag",
|
||||
},
|
||||
});
|
||||
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(result.config.auth?.profiles?.["openrouter:default"]).toMatchObject({
|
||||
provider: "openrouter",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as { profiles?: Record<string, { key?: string }> };
|
||||
expect(parsed.profiles?.["openrouter:default"]?.key).toBe("sk-openrouter-flag");
|
||||
});
|
||||
|
||||
it("uses provided openai token when authChoice=apiKey", async () => {
|
||||
await setupTempState();
|
||||
const { prompter, text, confirm } = buildPrompter();
|
||||
|
||||
await applyAuthChoice({
|
||||
authChoice: "apiKey",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: "openai",
|
||||
token: "sk-openai-flag",
|
||||
},
|
||||
});
|
||||
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(process.env.OPENAI_API_KEY).toBe("sk-openai-flag");
|
||||
const envPath = path.join(process.env.CLAWDBOT_STATE_DIR ?? "", ".env");
|
||||
const envContents = await fs.readFile(envPath, "utf8");
|
||||
expect(envContents).toContain("OPENAI_API_KEY=sk-openai-flag");
|
||||
});
|
||||
|
||||
it("uses provided anthropic token when authChoice=apiKey", async () => {
|
||||
await setupTempState();
|
||||
process.env.ANTHROPIC_API_KEY = "sk-env-test";
|
||||
const { prompter, text, confirm } = buildPrompter();
|
||||
|
||||
await applyAuthChoice({
|
||||
authChoice: "apiKey",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: "anthropic",
|
||||
token: "sk-anthropic-flag",
|
||||
},
|
||||
});
|
||||
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
|
||||
const authProfilePath = authProfilePathFor(process.env.CLAWDBOT_AGENT_DIR ?? "");
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as { profiles?: Record<string, { key?: string }> };
|
||||
expect(parsed.profiles?.["anthropic:default"]?.key).toBe("sk-anthropic-flag");
|
||||
});
|
||||
});
|
||||
@ -8,6 +8,7 @@ import {
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js";
|
||||
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
|
||||
@ -198,10 +199,23 @@ export async function applyAuthChoiceAnthropic(
|
||||
}
|
||||
|
||||
if (params.authChoice === "apiKey") {
|
||||
const tokenProvider = params.opts?.tokenProvider
|
||||
? normalizeProviderId(params.opts.tokenProvider)
|
||||
: undefined;
|
||||
const explicitToken =
|
||||
tokenProvider === "anthropic" ? normalizeApiKeyInput(params.opts?.token ?? "") : "";
|
||||
if (tokenProvider && tokenProvider !== "anthropic") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextConfig = params.config;
|
||||
let hasCredential = false;
|
||||
const envKey = process.env.ANTHROPIC_API_KEY?.trim();
|
||||
if (envKey) {
|
||||
if (explicitToken) {
|
||||
await setAnthropicApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing ANTHROPIC_API_KEY (env, ${formatApiKeyPreview(envKey)})?`,
|
||||
initialValue: true,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import {
|
||||
formatApiKeyPreview,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import {
|
||||
applyGoogleGeminiModelDefault,
|
||||
@ -56,53 +58,86 @@ export async function applyAuthChoiceApiProviders(
|
||||
);
|
||||
};
|
||||
|
||||
if (params.authChoice === "openrouter-api-key") {
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profileOrder = resolveAuthProfileOrder({
|
||||
cfg: nextConfig,
|
||||
store,
|
||||
provider: "openrouter",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
const tokenProvider = params.opts?.tokenProvider
|
||||
? normalizeProviderId(params.opts.tokenProvider)
|
||||
: undefined;
|
||||
const tokenValue = params.opts?.token;
|
||||
let authChoice = params.authChoice;
|
||||
if (
|
||||
authChoice === "apiKey" &&
|
||||
tokenProvider &&
|
||||
tokenProvider !== "anthropic" &&
|
||||
tokenProvider !== "openai"
|
||||
) {
|
||||
const mapped: Partial<Record<string, AuthChoice>> = {
|
||||
openrouter: "openrouter-api-key",
|
||||
"vercel-ai-gateway": "ai-gateway-api-key",
|
||||
moonshot: "moonshot-api-key",
|
||||
"kimi-code": "kimi-code-api-key",
|
||||
google: "gemini-api-key",
|
||||
zai: "zai-api-key",
|
||||
synthetic: "synthetic-api-key",
|
||||
opencode: "opencode-zen",
|
||||
};
|
||||
authChoice = mapped[tokenProvider] ?? authChoice;
|
||||
}
|
||||
|
||||
if (authChoice === "openrouter-api-key") {
|
||||
let profileId = "openrouter:default";
|
||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
||||
let hasCredential = false;
|
||||
const explicitToken =
|
||||
tokenProvider === "openrouter" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
|
||||
if (existingProfileId && existingCred?.type) {
|
||||
profileId = existingProfileId;
|
||||
mode =
|
||||
existingCred.type === "oauth"
|
||||
? "oauth"
|
||||
: existingCred.type === "token"
|
||||
? "token"
|
||||
: "api_key";
|
||||
if (explicitToken) {
|
||||
await setOpenrouterApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
} else {
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profileOrder = resolveAuthProfileOrder({
|
||||
cfg: nextConfig,
|
||||
store,
|
||||
provider: "openrouter",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) =>
|
||||
Boolean(store.profiles[profileId]),
|
||||
);
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
if (existingProfileId && existingCred?.type) {
|
||||
profileId = existingProfileId;
|
||||
mode =
|
||||
existingCred.type === "oauth"
|
||||
? "oauth"
|
||||
: existingCred.type === "token"
|
||||
? "token"
|
||||
: "api_key";
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("openrouter");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setOpenrouterApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("openrouter");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setOpenrouterApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenRouter API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
hasCredential = true;
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenRouter API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCredential) {
|
||||
@ -129,10 +164,16 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "ai-gateway-api-key") {
|
||||
if (authChoice === "ai-gateway-api-key") {
|
||||
let hasCredential = false;
|
||||
const explicitToken =
|
||||
tokenProvider === "vercel-ai-gateway" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setVercelAiGatewayApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
const envKey = resolveEnvApiKey("vercel-ai-gateway");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
@ -171,10 +212,16 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "moonshot-api-key") {
|
||||
if (authChoice === "moonshot-api-key") {
|
||||
let hasCredential = false;
|
||||
const explicitToken =
|
||||
tokenProvider === "moonshot" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setMoonshotApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
const envKey = resolveEnvApiKey("moonshot");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
@ -212,17 +259,25 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "kimi-code-api-key") {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Kimi Code uses a dedicated endpoint and API key.",
|
||||
"Get your API key at: https://www.kimi.com/code/en",
|
||||
].join("\n"),
|
||||
"Kimi Code",
|
||||
);
|
||||
if (authChoice === "kimi-code-api-key") {
|
||||
let hasCredential = false;
|
||||
const explicitToken =
|
||||
tokenProvider === "kimi-code" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setKimiCodeApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
if (!hasCredential) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Kimi Code uses a dedicated endpoint and API key.",
|
||||
"Get your API key at: https://www.kimi.com/code/en",
|
||||
].join("\n"),
|
||||
"Kimi Code",
|
||||
);
|
||||
}
|
||||
const envKey = resolveEnvApiKey("kimi-code");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
@ -261,10 +316,15 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "gemini-api-key") {
|
||||
if (authChoice === "gemini-api-key") {
|
||||
let hasCredential = false;
|
||||
const explicitToken = tokenProvider === "google" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setGeminiApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
const envKey = resolveEnvApiKey("google");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
@ -302,10 +362,15 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "zai-api-key") {
|
||||
if (authChoice === "zai-api-key") {
|
||||
let hasCredential = false;
|
||||
const explicitToken = tokenProvider === "zai" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setZaiApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
const envKey = resolveEnvApiKey("zai");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
@ -359,12 +424,18 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "synthetic-api-key") {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Synthetic API key",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
await setSyntheticApiKey(String(key).trim(), params.agentDir);
|
||||
if (authChoice === "synthetic-api-key") {
|
||||
const explicitToken =
|
||||
tokenProvider === "synthetic" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setSyntheticApiKey(explicitToken, params.agentDir);
|
||||
} else {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Synthetic API key",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
await setSyntheticApiKey(String(key).trim(), params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "synthetic:default",
|
||||
provider: "synthetic",
|
||||
@ -387,18 +458,26 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (params.authChoice === "opencode-zen") {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
||||
"Get your API key at: https://opencode.ai/auth",
|
||||
"Requires an active OpenCode Zen subscription.",
|
||||
].join("\n"),
|
||||
"OpenCode Zen",
|
||||
);
|
||||
if (authChoice === "opencode-zen") {
|
||||
let hasCredential = false;
|
||||
const explicitToken =
|
||||
tokenProvider === "opencode" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||
if (explicitToken) {
|
||||
await setOpencodeZenApiKey(explicitToken, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
if (!hasCredential) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
||||
"Get your API key at: https://opencode.ai/auth",
|
||||
"Requires an active OpenCode Zen subscription.",
|
||||
].join("\n"),
|
||||
"OpenCode Zen",
|
||||
);
|
||||
}
|
||||
const envKey = resolveEnvApiKey("opencode");
|
||||
if (envKey) {
|
||||
if (!hasCredential && envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
||||
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||
@ -20,33 +21,47 @@ import {
|
||||
export async function applyAuthChoiceOpenAI(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
if (params.authChoice === "openai-api-key") {
|
||||
const envKey = resolveEnvApiKey("openai");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
const result = upsertSharedEnvVar({
|
||||
key: "OPENAI_API_KEY",
|
||||
value: envKey.apiKey,
|
||||
const tokenProvider = params.opts?.tokenProvider
|
||||
? normalizeProviderId(params.opts.tokenProvider)
|
||||
: undefined;
|
||||
const explicitToken =
|
||||
tokenProvider === "openai" ? normalizeApiKeyInput(params.opts?.token ?? "") : "";
|
||||
let authChoice = params.authChoice;
|
||||
if (authChoice === "apiKey" && tokenProvider === "openai") {
|
||||
authChoice = "openai-api-key";
|
||||
}
|
||||
|
||||
if (authChoice === "openai-api-key") {
|
||||
if (!explicitToken) {
|
||||
const envKey = resolveEnvApiKey("openai");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
process.env.OPENAI_API_KEY = envKey.apiKey;
|
||||
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;
|
||||
}
|
||||
await params.prompter.note(
|
||||
`Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`,
|
||||
"OpenAI API key",
|
||||
);
|
||||
return { config: params.config };
|
||||
}
|
||||
await params.prompter.note(
|
||||
`Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`,
|
||||
"OpenAI API key",
|
||||
);
|
||||
return { config: params.config };
|
||||
}
|
||||
}
|
||||
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenAI API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
const key = explicitToken
|
||||
? explicitToken
|
||||
: await params.prompter.text({
|
||||
message: "Enter OpenAI API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
const trimmed = normalizeApiKeyInput(String(key));
|
||||
const result = upsertSharedEnvVar({
|
||||
key: "OPENAI_API_KEY",
|
||||
|
||||
@ -21,6 +21,10 @@ export type ApplyAuthChoiceParams = {
|
||||
agentDir?: string;
|
||||
setDefaultModel: boolean;
|
||||
agentId?: string;
|
||||
opts?: {
|
||||
tokenProvider?: string;
|
||||
token?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApplyAuthChoiceResult = {
|
||||
|
||||
@ -50,9 +50,9 @@ export type OnboardOptions = {
|
||||
acceptRisk?: boolean;
|
||||
reset?: boolean;
|
||||
authChoice?: AuthChoice;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
/** Used when `authChoice=token` in non-interactive mode, or `authChoice=apiKey` in wizard mode. */
|
||||
tokenProvider?: string;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
/** Used when `authChoice=token` in non-interactive mode, or `authChoice=apiKey` in wizard mode. */
|
||||
token?: string;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
tokenProfileId?: string;
|
||||
|
||||
@ -356,6 +356,10 @@ export async function runOnboardingWizard(
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: opts.tokenProvider,
|
||||
token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined,
|
||||
},
|
||||
});
|
||||
nextConfig = authResult.config;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user