This commit is contained in:
Dax Raad
2025-06-10 13:30:08 -04:00
parent 96b5a079ff
commit ef7f1f0761
10 changed files with 193 additions and 31 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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")
},
})

View File

@@ -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<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()

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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"