summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-10 13:30:08 -0400
committerDax Raad <[email protected]>2025-06-10 13:30:13 -0400
commitef7f1f0761e9b02021f147a21915d7506fb08f88 (patch)
tree0dd122f6f805aa95ed4eda587a2b610fc8f49e15
parent96b5a079fff01cedfa6d849fc6346f5d70a57d41 (diff)
downloadopencode-ef7f1f0761e9b02021f147a21915d7506fb08f88.tar.gz
opencode-ef7f1f0761e9b02021f147a21915d7506fb08f88.zip
sync
-rw-r--r--bun.lock18
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/auth/anthropic.ts5
-rw-r--r--packages/opencode/src/auth/keys.ts20
-rw-r--r--packages/opencode/src/cli/cmd/login-anthropic.ts10
-rw-r--r--packages/opencode/src/cli/cmd/provider.ts129
-rw-r--r--packages/opencode/src/cli/ui.ts18
-rw-r--r--packages/opencode/src/index.ts9
-rw-r--r--packages/opencode/src/provider/models.ts8
-rw-r--r--packages/opencode/src/provider/provider.ts1
10 files changed, 191 insertions, 29 deletions
diff --git a/bun.lock b/bun.lock
index b17b1bf16..2682d254d 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/[email protected]", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
+ "@clack/core": ["@clack/[email protected]", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
+
+ "@clack/prompts": ["@clack/[email protected]", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
+
"@cloudflare/kv-asset-handler": ["@cloudflare/[email protected]", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
"@cloudflare/unenv-preset": ["@cloudflare/[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
+ "bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
+
"bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
+ "default-browser": ["[email protected]", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
+
+ "default-browser-id": ["[email protected]", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
+
"define-data-property": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
+
"defu": ["[email protected]", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@@ -1167,6 +1181,8 @@
"oniguruma-to-es": ["[email protected]", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
+ "open": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"opencode": ["opencode@workspace:packages/opencode"],
@@ -1325,6 +1341,8 @@
"router": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
+
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["[email protected]", "", { "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 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"