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.
|
- 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.
|
- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
|
||||||
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
- 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.
|
- 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: 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.
|
- 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>`
|
- `--mode <local|remote>`
|
||||||
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
|
- `--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>`
|
- `--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-provider <id>` (non-interactive; used with `--auth-choice token` or `apiKey`)
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
- `--token <token>` (non-interactive; used with `--auth-choice token` or `apiKey`)
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
|
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
|
||||||
- `--anthropic-api-key <key>`
|
- `--anthropic-api-key <key>`
|
||||||
|
|||||||
@ -56,9 +56,12 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--token-provider <id>",
|
"--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(
|
.option(
|
||||||
"--token-profile-id <id>",
|
"--token-profile-id <id>",
|
||||||
"Auth profile id (non-interactive; default: <provider>:manual)",
|
"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,
|
normalizeApiKeyInput,
|
||||||
validateApiKeyInput,
|
validateApiKeyInput,
|
||||||
} from "./auth-choice.api-key.js";
|
} from "./auth-choice.api-key.js";
|
||||||
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js";
|
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js";
|
||||||
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
|
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
|
||||||
@ -198,10 +199,23 @@ export async function applyAuthChoiceAnthropic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "apiKey") {
|
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 nextConfig = params.config;
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
const envKey = process.env.ANTHROPIC_API_KEY?.trim();
|
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({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing ANTHROPIC_API_KEY (env, ${formatApiKeyPreview(envKey)})?`,
|
message: `Use existing ANTHROPIC_API_KEY (env, ${formatApiKeyPreview(envKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
||||||
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||||
import {
|
import {
|
||||||
formatApiKeyPreview,
|
formatApiKeyPreview,
|
||||||
@ -6,6 +7,7 @@ import {
|
|||||||
validateApiKeyInput,
|
validateApiKeyInput,
|
||||||
} from "./auth-choice.api-key.js";
|
} from "./auth-choice.api-key.js";
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.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 { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||||
import {
|
import {
|
||||||
applyGoogleGeminiModelDefault,
|
applyGoogleGeminiModelDefault,
|
||||||
@ -56,7 +58,41 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.authChoice === "openrouter-api-key") {
|
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 (explicitToken) {
|
||||||
|
await setOpenrouterApiKey(explicitToken, params.agentDir);
|
||||||
|
hasCredential = true;
|
||||||
|
} else {
|
||||||
const store = ensureAuthProfileStore(params.agentDir, {
|
const store = ensureAuthProfileStore(params.agentDir, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
});
|
});
|
||||||
@ -65,12 +101,10 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
store,
|
store,
|
||||||
provider: "openrouter",
|
provider: "openrouter",
|
||||||
});
|
});
|
||||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
const existingProfileId = profileOrder.find((profileId) =>
|
||||||
|
Boolean(store.profiles[profileId]),
|
||||||
|
);
|
||||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||||
let profileId = "openrouter:default";
|
|
||||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
|
||||||
let hasCredential = false;
|
|
||||||
|
|
||||||
if (existingProfileId && existingCred?.type) {
|
if (existingProfileId && existingCred?.type) {
|
||||||
profileId = existingProfileId;
|
profileId = existingProfileId;
|
||||||
mode =
|
mode =
|
||||||
@ -104,6 +138,7 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||||
hasCredential = true;
|
hasCredential = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasCredential) {
|
if (hasCredential) {
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
@ -129,10 +164,16 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "ai-gateway-api-key") {
|
if (authChoice === "ai-gateway-api-key") {
|
||||||
let hasCredential = false;
|
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");
|
const envKey = resolveEnvApiKey("vercel-ai-gateway");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@ -171,10 +212,16 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "moonshot-api-key") {
|
if (authChoice === "moonshot-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
const explicitToken =
|
||||||
|
tokenProvider === "moonshot" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||||
|
if (explicitToken) {
|
||||||
|
await setMoonshotApiKey(explicitToken, params.agentDir);
|
||||||
|
hasCredential = true;
|
||||||
|
}
|
||||||
const envKey = resolveEnvApiKey("moonshot");
|
const envKey = resolveEnvApiKey("moonshot");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@ -212,7 +259,15 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "kimi-code-api-key") {
|
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(
|
await params.prompter.note(
|
||||||
[
|
[
|
||||||
"Kimi Code uses a dedicated endpoint and API key.",
|
"Kimi Code uses a dedicated endpoint and API key.",
|
||||||
@ -220,9 +275,9 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Kimi Code",
|
"Kimi Code",
|
||||||
);
|
);
|
||||||
let hasCredential = false;
|
}
|
||||||
const envKey = resolveEnvApiKey("kimi-code");
|
const envKey = resolveEnvApiKey("kimi-code");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@ -261,10 +316,15 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "gemini-api-key") {
|
if (authChoice === "gemini-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
const explicitToken = tokenProvider === "google" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||||
|
if (explicitToken) {
|
||||||
|
await setGeminiApiKey(explicitToken, params.agentDir);
|
||||||
|
hasCredential = true;
|
||||||
|
}
|
||||||
const envKey = resolveEnvApiKey("google");
|
const envKey = resolveEnvApiKey("google");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@ -302,10 +362,15 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "zai-api-key") {
|
if (authChoice === "zai-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
const explicitToken = tokenProvider === "zai" ? normalizeApiKeyInput(tokenValue ?? "") : "";
|
||||||
|
if (explicitToken) {
|
||||||
|
await setZaiApiKey(explicitToken, params.agentDir);
|
||||||
|
hasCredential = true;
|
||||||
|
}
|
||||||
const envKey = resolveEnvApiKey("zai");
|
const envKey = resolveEnvApiKey("zai");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@ -359,12 +424,18 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "synthetic-api-key") {
|
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({
|
const key = await params.prompter.text({
|
||||||
message: "Enter Synthetic API key",
|
message: "Enter Synthetic API key",
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||||
});
|
});
|
||||||
await setSyntheticApiKey(String(key).trim(), params.agentDir);
|
await setSyntheticApiKey(String(key).trim(), params.agentDir);
|
||||||
|
}
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
profileId: "synthetic:default",
|
profileId: "synthetic:default",
|
||||||
provider: "synthetic",
|
provider: "synthetic",
|
||||||
@ -387,7 +458,15 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.authChoice === "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(
|
await params.prompter.note(
|
||||||
[
|
[
|
||||||
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
||||||
@ -396,9 +475,9 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
"OpenCode Zen",
|
"OpenCode Zen",
|
||||||
);
|
);
|
||||||
let hasCredential = false;
|
}
|
||||||
const envKey = resolveEnvApiKey("opencode");
|
const envKey = resolveEnvApiKey("opencode");
|
||||||
if (envKey) {
|
if (!hasCredential && envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
||||||
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
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 { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
||||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||||
@ -20,7 +21,18 @@ import {
|
|||||||
export async function applyAuthChoiceOpenAI(
|
export async function applyAuthChoiceOpenAI(
|
||||||
params: ApplyAuthChoiceParams,
|
params: ApplyAuthChoiceParams,
|
||||||
): Promise<ApplyAuthChoiceResult | null> {
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
if (params.authChoice === "openai-api-key") {
|
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");
|
const envKey = resolveEnvApiKey("openai");
|
||||||
if (envKey) {
|
if (envKey) {
|
||||||
const useExisting = await params.prompter.confirm({
|
const useExisting = await params.prompter.confirm({
|
||||||
@ -42,8 +54,11 @@ export async function applyAuthChoiceOpenAI(
|
|||||||
return { config: params.config };
|
return { config: params.config };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const key = await params.prompter.text({
|
const key = explicitToken
|
||||||
|
? explicitToken
|
||||||
|
: await params.prompter.text({
|
||||||
message: "Enter OpenAI API key",
|
message: "Enter OpenAI API key",
|
||||||
validate: validateApiKeyInput,
|
validate: validateApiKeyInput,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,6 +21,10 @@ export type ApplyAuthChoiceParams = {
|
|||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
setDefaultModel: boolean;
|
setDefaultModel: boolean;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
opts?: {
|
||||||
|
tokenProvider?: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApplyAuthChoiceResult = {
|
export type ApplyAuthChoiceResult = {
|
||||||
|
|||||||
@ -50,9 +50,9 @@ export type OnboardOptions = {
|
|||||||
acceptRisk?: boolean;
|
acceptRisk?: boolean;
|
||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
authChoice?: AuthChoice;
|
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;
|
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;
|
token?: string;
|
||||||
/** Used when `authChoice=token` in non-interactive mode. */
|
/** Used when `authChoice=token` in non-interactive mode. */
|
||||||
tokenProfileId?: string;
|
tokenProfileId?: string;
|
||||||
|
|||||||
@ -356,6 +356,10 @@ export async function runOnboardingWizard(
|
|||||||
prompter,
|
prompter,
|
||||||
runtime,
|
runtime,
|
||||||
setDefaultModel: true,
|
setDefaultModel: true,
|
||||||
|
opts: {
|
||||||
|
tokenProvider: opts.tokenProvider,
|
||||||
|
token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
nextConfig = authResult.config;
|
nextConfig = authResult.config;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user