From ef7f1f0761e9b02021f147a21915d7506fb08f88 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 10 Jun 2025 13:30:08 -0400 Subject: [PATCH] sync --- bun.lock | 18 +++ packages/opencode/package.json | 2 + packages/opencode/src/auth/anthropic.ts | 5 +- packages/opencode/src/auth/keys.ts | 20 +++ .../opencode/src/cli/cmd/login-anthropic.ts | 10 +- packages/opencode/src/cli/cmd/provider.ts | 129 ++++++++++++++++++ packages/opencode/src/cli/ui.ts | 22 +-- packages/opencode/src/index.ts | 9 +- packages/opencode/src/provider/models.ts | 8 +- packages/opencode/src/provider/provider.ts | 1 - 10 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 packages/opencode/src/auth/keys.ts create mode 100644 packages/opencode/src/cli/cmd/provider.ts diff --git a/bun.lock b/bun.lock index b17b1bf1..2682d254 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "name": "opencode", "version": "0.0.0", "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", @@ -163,6 +165,10 @@ "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.17", "workerd": "^1.20250508.0" }, "optionalPeers": ["workerd"] }, "sha512-MtUgNl+QkQyhQvv5bbWP+BpBC1N0me4CHHuP2H4ktmOMKdB/6kkz/lo+zqiA4mEazb4y+1cwyNjVrQ2DWeE4mg=="], @@ -557,6 +563,8 @@ "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -649,8 +657,14 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], + + "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -1167,6 +1181,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "opencode": ["opencode@workspace:packages/opencode"], @@ -1325,6 +1341,8 @@ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0318c46d..7f99ad27 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 cd1c2331..6d8913c8 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 00000000..9f240a18 --- /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) + } + + 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 57533c5f..64df8beb 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 00000000..ed74e83f --- /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 d08163ba..2579c6fd 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 print(...message: string[]) { - Bun.stderr.write(message.join(" ")) + export function println(...message: string[]) { + print(...message) Bun.stderr.write("\n") } + export function print(...message: string[]) { + blank = false + Bun.stderr.write(message.join(" ")) + } + + 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 { - 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 34ff0b68..af090d48 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 2d673f2a..cb5cf4d1 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 23ae4804..5cc72fc7 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"