summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-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
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"