diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/package.json | 2 | ||||
| -rw-r--r-- | packages/opencode/src/auth/anthropic.ts | 5 | ||||
| -rw-r--r-- | packages/opencode/src/auth/keys.ts | 20 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/login-anthropic.ts | 10 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/provider.ts | 129 | ||||
| -rw-r--r-- | packages/opencode/src/cli/ui.ts | 18 | ||||
| -rw-r--r-- | packages/opencode/src/index.ts | 9 | ||||
| -rw-r--r-- | packages/opencode/src/provider/models.ts | 8 | ||||
| -rw-r--r-- | packages/opencode/src/provider/provider.ts | 1 |
9 files changed, 173 insertions, 29 deletions
diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0318c46d7..7f99ad27d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -21,6 +21,7 @@ "typescript": "catalog:" }, "dependencies": { + "@clack/prompts": "0.11.0", "@flystorage/file-storage": "1.1.0", "@flystorage/local-fs": "1.1.0", "@hono/zod-validator": "0.5.0", @@ -32,6 +33,7 @@ "env-paths": "3.0.0", "hono": "4.7.10", "hono-openapi": "0.4.8", + "open": "10.1.2", "remeda": "2.22.3", "ts-lsp-client": "1.0.3", "turndown": "7.2.0", diff --git a/packages/opencode/src/auth/anthropic.ts b/packages/opencode/src/auth/anthropic.ts index cd1c23319..6d8913c80 100644 --- a/packages/opencode/src/auth/anthropic.ts +++ b/packages/opencode/src/auth/anthropic.ts @@ -2,11 +2,12 @@ import { generatePKCE } from "@openauthjs/openauth/pkce" import { Global } from "../global" import path from "path" import fs from "fs/promises" -import type { BunFile } from "bun" export namespace AuthAnthropic { const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" + const file = Bun.file(path.join(Global.Path.data, "auth", "anthropic.json")) + export async function authorize() { const pkce = await generatePKCE() const url = new URL("https://claude.ai/oauth/authorize", import.meta.url) @@ -47,13 +48,11 @@ export namespace AuthAnthropic { }), }) if (!result.ok) throw new ExchangeFailed() - const file = Bun.file(path.join(Global.Path.data, "anthropic.json")) await Bun.write(file, result) await fs.chmod(file.name!, 0o600) } export async function access() { - const file = Bun.file(path.join(Global.Path.data, "anthropic.json")) if (!(await file.exists())) return const result = await file.json() const refresh = result.refresh_token diff --git a/packages/opencode/src/auth/keys.ts b/packages/opencode/src/auth/keys.ts new file mode 100644 index 000000000..9f240a18f --- /dev/null +++ b/packages/opencode/src/auth/keys.ts @@ -0,0 +1,20 @@ +import path from "path" +import { Global } from "../global" +import fs from "fs/promises" + +export namespace AuthKeys { + const file = Bun.file(path.join(Global.Path.data, "auth", "keys.json")) + + export async function get() { + return file + .json() + .catch(() => ({})) + .then((x) => x as Record<string, string>) + } + + export async function set(key: string, value: string) { + const env = await get() + await Bun.write(file, JSON.stringify({ ...env, [key]: value })) + await fs.chmod(file.name!, 0o600) + } +} diff --git a/packages/opencode/src/cli/cmd/login-anthropic.ts b/packages/opencode/src/cli/cmd/login-anthropic.ts index 57533c5fb..64df8bebf 100644 --- a/packages/opencode/src/cli/cmd/login-anthropic.ts +++ b/packages/opencode/src/cli/cmd/login-anthropic.ts @@ -3,18 +3,16 @@ import { UI } from "../ui" // Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs - - export const LoginAnthropicCommand = { command: "anthropic", describe: "Login to Anthropic", handler: async () => { const { url, verifier } = await AuthAnthropic.authorize() - UI.print("Login to Anthropic") - UI.print("Open the following URL in your browser:") - UI.print(url) - UI.print("") + UI.println("Login to Anthropic") + UI.println("Open the following URL in your browser:") + UI.println(url) + UI.println("") const code = await UI.input("Paste the authorization code here: ") await AuthAnthropic.exchange(code, verifier) diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts new file mode 100644 index 000000000..ed74e83f6 --- /dev/null +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -0,0 +1,129 @@ +import { AuthAnthropic } from "../../auth/anthropic" +import { AuthKeys } from "../../auth/keys" +import { UI } from "../ui" +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import open from "open" + +const OPENCODE = [ + `█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀`, + `█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀`, + `▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`, +] + +export const ProviderCommand = cmd({ + command: "provider", + builder: (yargs) => + yargs + .command(ProviderAddCommand) + .command(ProviderListCommand) + .demandCommand(), + describe: "initialize opencode", + async handler() {}, +}) + +export const ProviderListCommand = cmd({ + command: "list", + aliases: ["ls"], + describe: "list providers", + async handler() { + prompts.intro("Configured Providers") + const keys = await AuthKeys.get() + for (const key of Object.keys(keys)) { + prompts.log.success(key) + } + prompts.outro("3 providers configured") + }, +}) + +const ProviderAddCommand = cmd({ + command: "add", + describe: "add credentials for various providers", + async handler() { + UI.empty() + for (const row of OPENCODE) { + UI.print(" ") + for (let i = 0; i < row.length; i++) { + const color = + i < 18 ? Bun.color("white", "ansi") : Bun.color("gray", "ansi") + const char = row[i] + UI.print(color + char) + } + UI.println() + } + UI.empty() + + prompts.intro("Setup") + const keys = await AuthKeys.get() + const provider = await prompts.select({ + message: "Configure a provider", + options: [ + { + label: "Anthropic", + value: "anthropic", + hint: keys["anthropic"] ? "configured" : "", + }, + { + label: "OpenAI", + value: "openai", + hint: keys["openai"] ? "configured" : "", + }, + { + label: "Google", + value: "google", + hint: keys["google"] ? "configured" : "", + }, + ], + }) + if (prompts.isCancel(provider)) return + + if (provider === "anthropic") { + const method = await prompts.select({ + message: "Login method", + options: [ + { + label: "Claude Pro/Max", + value: "oauth", + }, + { + label: "API Key", + value: "api", + }, + ], + }) + if (prompts.isCancel(method)) return + + if (method === "oauth") { + // some weird bug where program exits without this + await new Promise((resolve) => setTimeout(resolve, 10)) + const { url, verifier } = await AuthAnthropic.authorize() + prompts.note("Opening browser...") + await open(url) + prompts.log.info(url) + + const code = await prompts.text({ + message: "Paste the authorization code here: ", + validate: (x) => (x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(code)) return + await AuthAnthropic.exchange(code, verifier) + .then(() => { + prompts.log.success("Login successful") + }) + .catch(() => { + prompts.log.error("Invalid code") + }) + prompts.outro("Done") + return + } + } + + const key = await prompts.password({ + message: "Enter your API key", + }) + if (prompts.isCancel(key)) return + await AuthKeys.set(provider, key) + + prompts.outro("Done") + }, +}) diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index d08163baa..2579c6fda 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -16,24 +16,30 @@ export namespace UI { TEXT_INFO_BOLD: "\x1b[94m\x1b[1m", } - + export function println(...message: string[]) { + print(...message) + Bun.stderr.write("\n") + } export function print(...message: string[]) { + blank = false Bun.stderr.write(message.join(" ")) - Bun.stderr.write("\n") } + let blank = false export function empty() { - print("" + Style.TEXT_NORMAL) + if (blank) return + println("" + Style.TEXT_NORMAL) + blank = true } export async function input(prompt: string): Promise<string> { - const readline = require('readline') + const readline = require("readline") const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }) - + return new Promise((resolve) => { rl.question(prompt, (answer: string) => { rl.close() diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 34ff0b684..af090d48a 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -11,11 +11,11 @@ import { Global } from "./global" import yargs from "yargs" import { hideBin } from "yargs/helpers" import { RunCommand } from "./cli/cmd/run" -import { LoginAnthropicCommand } from "./cli/cmd/login-anthropic" import { GenerateCommand } from "./cli/cmd/generate" import { VERSION } from "./cli/version" import { ScrapCommand } from "./cli/cmd/scrap" import { Log } from "./util/log" +import { ProviderCommand } from "./cli/cmd/provider" await Log.init({ print: process.argv.includes("--print-logs") }) @@ -70,11 +70,6 @@ yargs(hideBin(process.argv)) .command(RunCommand) .command(GenerateCommand) .command(ScrapCommand) - .command({ - command: "login", - describe: "generate credentials for various providers", - builder: (yargs) => yargs.command(LoginAnthropicCommand).demandCommand(), - handler: () => {}, - }) + .command(ProviderCommand) .help() .parse() diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 2d673f2a6..cb5cf4d18 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -4,13 +4,9 @@ import path from "path" export namespace ModelsDev { const log = Log.create({ service: "models.dev" }) - - function filepath() { - return path.join(Global.Path.data, "models.json") - } + const file = Bun.file(path.join(Global.Path.cache, "models.json")) export async function get() { - const file = Bun.file(filepath()) if (await file.exists()) { refresh() return file.json() @@ -24,6 +20,6 @@ export namespace ModelsDev { const result = await fetch("https://models.dev/api.json") if (!result.ok) throw new Error(`Failed to fetch models.dev: ${result.statusText}`) - await Bun.write(filepath(), result) + await Bun.write(file, result) } } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 23ae48047..5cc72fc7d 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -18,7 +18,6 @@ import { LspHoverTool } from "../tool/lsp-hover" import { PatchTool } from "../tool/patch" import { ReadTool } from "../tool/read" import type { Tool } from "../tool/tool" - import { WriteTool } from "../tool/write" import { TodoReadTool, TodoWriteTool } from "../tool/todo" import { AuthAnthropic } from "../auth/anthropic" |
