Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions extensions/cli/spec/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ When a user first runs `cn` in interactive mode, they will be taken through "onb
2. If the CONTINUE_USE_BEDROCK environment variable is set to "1", automatically use AWS Bedrock configuration and skip interactive prompts
3. Present the user with available options:

- Log in with Continue: log them in, which will automatically create their assistant and then we can load the first assistant from the first org
- Enter your Anthropic API key: let them enter the key, and then either create a ~/.continue/config.yaml with the following contents OR update the existing config.yaml to add the model

```yaml
Expand All @@ -19,9 +18,24 @@ When a user first runs `cn` in interactive mode, they will be taken through "onb
schema: v1

models:
- uses: anthropic/claude-4-sonnet
with:
ANTHROPIC_API_KEY: <THEIR_ANTHROPIC_API_KEY>
- name: claude sonnet 4.6
provider: anthropic
model: claude-sonnet-4-6
apiKey: <THEIR_ANTHROPIC_API_KEY>
```

- Enter your OpenAI API key: let them enter the key, and then either create a ~/.continue/config.yaml with the following contents OR update the existing config.yaml to add the model

```yaml
name: Local Config
version: 1.0.0
schema: v1

models:
- name: GPT 5.4
provider: openai
model: gpt-5.4
apiKey: <THEIR_OPENAI_API_KEY>
```

When CONTINUE_USE_BEDROCK=1 is detected, it will use AWS Bedrock configuration. The user must have AWS credentials configured through the standard AWS credential chain (AWS CLI, environment variables, IAM roles, etc.).
Expand Down
56 changes: 45 additions & 11 deletions extensions/cli/src/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import * as path from "path";
import chalk from "chalk";
import { setConfigFilePermissions } from "core/util/paths.js";

import { AuthConfig, login } from "./auth/workos.js";
import { AuthConfig } from "./auth/workos.js";
import { getApiClient } from "./config.js";
import { loadConfiguration } from "./configLoader.js";
import { env } from "./env.js";
import {
getApiKeyValidationError,
isValidAnthropicApiKey,
isValidOpenAIApiKey,
} from "./util/apiKeyValidation.js";
import { question, questionWithChoices } from "./util/prompt.js";
import { updateAnthropicModelInYaml } from "./util/yamlConfigUpdater.js";
import {
updateAnthropicModelInYaml,
updateOpenAIModelInYaml,
} from "./util/yamlConfigUpdater.js";

const CONFIG_PATH = path.join(env.continueHome, "config.yaml");

Expand All @@ -32,7 +36,9 @@ export async function checkHasAcceptableModel(
}
}

export async function createOrUpdateConfig(apiKey: string): Promise<void> {
export async function createOrUpdateConfigAnthropic(
apiKey: string,
): Promise<void> {
const configDir = path.dirname(CONFIG_PATH);

if (!fs.existsSync(configDir)) {
Expand All @@ -48,6 +54,24 @@ export async function createOrUpdateConfig(apiKey: string): Promise<void> {
setConfigFilePermissions(CONFIG_PATH);
}

export async function createOrUpdateConfigOpenAI(
apiKey: string,
): Promise<void> {
const configDir = path.dirname(CONFIG_PATH);

if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}

const existingContent = fs.existsSync(CONFIG_PATH)
? fs.readFileSync(CONFIG_PATH, "utf8")
: "";

const updatedContent = updateOpenAIModelInYaml(existingContent, apiKey);
fs.writeFileSync(CONFIG_PATH, updatedContent);
setConfigFilePermissions(CONFIG_PATH);
}

export async function runOnboardingFlow(
configPath: string | undefined,
): Promise<boolean> {
Expand Down Expand Up @@ -76,7 +100,7 @@ export async function runOnboardingFlow(
// In test/CI environment, check for ANTHROPIC_API_KEY first
if (process.env.ANTHROPIC_API_KEY) {
console.log(chalk.blue("✓ Using ANTHROPIC_API_KEY from environment"));
await createOrUpdateConfig(process.env.ANTHROPIC_API_KEY);
await createOrUpdateConfigAnthropic(process.env.ANTHROPIC_API_KEY);
console.log(chalk.gray(` Config saved to: ${CONFIG_PATH}`));
return false;
}
Expand All @@ -87,8 +111,8 @@ export async function runOnboardingFlow(

// Step 4: Present user with two options
console.log(chalk.yellow("How do you want to get started?"));
console.log(chalk.white("1. ⏩ Log in with Continue"));
console.log(chalk.white("2. 🔑 Enter your Anthropic API key"));
console.log(chalk.white("1. 🔑 Enter your Anthropic API key"));
console.log(chalk.white("2. 🔑 Enter your OpenAI API key"));

const choice = await questionWithChoices(
chalk.yellow("\nEnter choice (1): "),
Expand All @@ -98,18 +122,28 @@ export async function runOnboardingFlow(
);

if (choice === "1" || choice === "") {
await login();
return true;
} else if (choice === "2") {
const apiKey = await question(
chalk.white("\nEnter your Anthropic API key: "),
);

if (!isValidAnthropicApiKey(apiKey)) {
throw new Error(getApiKeyValidationError(apiKey));
throw new Error(getApiKeyValidationError(apiKey, "anthropic"));
}

await createOrUpdateConfigAnthropic(apiKey);
console.log(
chalk.green(`✓ Config file updated successfully at ${CONFIG_PATH}`),
);

return true;
} else if (choice === "2") {
const apiKey = await question(chalk.white("\nEnter your OpenAI API key: "));

if (!isValidOpenAIApiKey(apiKey)) {
throw new Error(getApiKeyValidationError(apiKey, "openai"));
}

await createOrUpdateConfig(apiKey);
await createOrUpdateConfigOpenAI(apiKey);
console.log(
chalk.green(`✓ Config file updated successfully at ${CONFIG_PATH}`),
);
Expand Down
3 changes: 2 additions & 1 deletion extensions/cli/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) {
!commandOptions.config &&
process.env.ANTHROPIC_API_KEY
) {
const { createOrUpdateConfig } = await import("../onboarding.js");
const { createOrUpdateConfigAnthropic: createOrUpdateConfig } =
await import("../onboarding.js");
const { env } = await import("../env.js");
const path = await import("path");

Expand Down
37 changes: 32 additions & 5 deletions extensions/cli/src/util/apiKeyValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,51 @@ export function isValidAnthropicApiKey(
return apiKey.startsWith("sk-ant-") && apiKey.length > "sk-ant-".length;
}

/**
* Validates an OpenAI API key format
* @param apiKey The API key to validate
* @returns true if the API key is valid, false otherwise
*/
export function isValidOpenAIApiKey(
apiKey: string | null | undefined,
): boolean {
if (!apiKey || typeof apiKey !== "string") {
return false;
}

return apiKey.startsWith("sk-") && apiKey.length > "sk-".length;
}

/**
* Gets a user-friendly error message for invalid API keys
* @param apiKey The API key that failed validation
* @param provider The provider name (e.g., "Anthropic", "OpenAI")
* @returns A descriptive error message
*/
export function getApiKeyValidationError(
apiKey: string | null | undefined,
provider: "anthropic" | "openai" = "anthropic",
): string {
if (!apiKey || typeof apiKey !== "string") {
return "API key is required";
}

if (!apiKey.startsWith("sk-ant-")) {
return 'API key must start with "sk-ant-"';
}
if (provider === "anthropic") {
if (!apiKey.startsWith("sk-ant-")) {
return 'API key must start with "sk-ant-"';
}

if (apiKey.length <= "sk-ant-".length) {
return "API key is too short";
}
} else if (provider === "openai") {
if (!apiKey.startsWith("sk-")) {
return 'API key must start with "sk-"';
}

if (apiKey.length <= "sk-ant-".length) {
return "API key is too short";
if (apiKey.length <= "sk-".length) {
return "API key is too short";
}
}

return "Invalid API key format";
Expand Down
Loading
Loading