summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-05 11:50:54 -0400
committerDax Raad <[email protected]>2025-06-05 11:51:06 -0400
commit35b03e4cb3af58126a5292fe186530527c858645 (patch)
tree77637f9199f78182b0fedd4659a34ea70b50056d /packages
parentb3555cda30a431518467d1688f427653d448ee71 (diff)
downloadopencode-35b03e4cb3af58126a5292fe186530527c858645.tar.gz
opencode-35b03e4cb3af58126a5292fe186530527c858645.zip
claude oauth support
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/package.json4
-rw-r--r--packages/opencode/src/app/app.ts6
-rw-r--r--packages/opencode/src/auth/anthropic.ts66
-rw-r--r--packages/opencode/src/cli/cmd/generate.ts20
-rw-r--r--packages/opencode/src/cli/cmd/login-anthropic.ts22
-rw-r--r--packages/opencode/src/cli/cmd/run.ts140
-rw-r--r--packages/opencode/src/cli/ui.ts44
-rw-r--r--packages/opencode/src/index.ts246
-rw-r--r--packages/opencode/src/provider/provider.ts37
-rw-r--r--packages/opencode/src/session/index.ts28
-rw-r--r--packages/opencode/src/session/prompt/anthropic_spoof.txt1
11 files changed, 421 insertions, 193 deletions
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index a879d2cd9..0318c46d7 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -17,15 +17,16 @@
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
+ "@types/yargs": "17.0.33",
"typescript": "catalog:"
},
"dependencies": {
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
+ "@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"ai": "catalog:",
- "cac": "6.7.14",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"env-paths": "3.0.0",
@@ -37,6 +38,7 @@
"vscode-jsonrpc": "8.2.1",
"vscode-languageclient": "8",
"xdg-basedir": "5.1.0",
+ "yargs": "18.0.0",
"zod": "catalog:",
"zod-openapi": "4.2.4"
}
diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts
index 5548a481b..88f115267 100644
--- a/packages/opencode/src/app/app.ts
+++ b/packages/opencode/src/app/app.ts
@@ -37,7 +37,11 @@ export namespace App {
x ? path.dirname(x) : undefined,
)
- const data = path.join(Global.Path.data, git ?? "global")
+ const data = path.join(
+ Global.Path.data,
+ "project",
+ git ? git.split(path.sep).join("-") : "global",
+ )
const stateFile = Bun.file(path.join(data, APP_JSON))
const state = (await stateFile.json().catch(() => ({}))) as {
initialized: number
diff --git a/packages/opencode/src/auth/anthropic.ts b/packages/opencode/src/auth/anthropic.ts
new file mode 100644
index 000000000..addc7bf13
--- /dev/null
+++ b/packages/opencode/src/auth/anthropic.ts
@@ -0,0 +1,66 @@
+// 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
+
+import { generatePKCE } from "@openauthjs/openauth/pkce"
+import { Global } from "../global"
+import path from "path"
+
+export namespace AuthAnthropic {
+ export async function authorize() {
+ const pkce = await generatePKCE()
+ const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
+ url.searchParams.set("code", "true")
+ url.searchParams.set("client_id", "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
+ url.searchParams.set("response_type", "code")
+ url.searchParams.set(
+ "redirect_uri",
+ "https://console.anthropic.com/oauth/code/callback",
+ )
+ url.searchParams.set(
+ "scope",
+ "org:create_api_key user:profile user:inference",
+ )
+ url.searchParams.set("code_challenge", pkce.challenge)
+ url.searchParams.set("code_challenge_method", "S256")
+ url.searchParams.set("state", pkce.verifier)
+ return {
+ url: url.toString(),
+ verifier: pkce.verifier,
+ }
+ }
+
+ export async function exchange(code: string, verifier: string) {
+ const splits = code.split("#")
+ const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ code: splits[0],
+ state: splits[1],
+ grant_type: "authorization_code",
+ client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
+ redirect_uri: "https://console.anthropic.com/oauth/code/callback",
+ code_verifier: verifier,
+ }),
+ })
+ if (!result.ok) throw new ExchangeFailed()
+ await Bun.write(path.join(Global.Path.data, "anthropic.json"), result)
+ }
+
+ export async function load() {
+ const file = Bun.file(path.join(Global.Path.data, "anthropic.json"))
+ if (!(await file.exists())) return
+ const result = await file.json()
+ return {
+ accessToken: result.access_token as string,
+ refreshToken: result.refresh_token as string,
+ }
+ }
+
+ export class ExchangeFailed extends Error {
+ constructor() {
+ super("Exchange failed")
+ }
+ }
+}
diff --git a/packages/opencode/src/cli/cmd/generate.ts b/packages/opencode/src/cli/cmd/generate.ts
new file mode 100644
index 000000000..1390f2719
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/generate.ts
@@ -0,0 +1,20 @@
+import { Server } from "../../server/server"
+import fs from "fs/promises"
+import path from "path"
+import type { CommandModule } from "yargs"
+
+export const GenerateCommand = {
+ command: "generate",
+ describe: "Generate OpenAPI and event specs",
+ handler: async () => {
+ const specs = await Server.openapi()
+ const dir = "gen"
+ await fs.rmdir(dir, { recursive: true }).catch(() => {})
+ await fs.mkdir(dir, { recursive: true })
+ await Bun.write(
+ path.join(dir, "openapi.json"),
+ JSON.stringify(specs, null, 2),
+ )
+ },
+} satisfies CommandModule
+
diff --git a/packages/opencode/src/cli/cmd/login-anthropic.ts b/packages/opencode/src/cli/cmd/login-anthropic.ts
new file mode 100644
index 000000000..12291fbe7
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/login-anthropic.ts
@@ -0,0 +1,22 @@
+import { AuthAnthropic } from "../../auth/anthropic"
+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
+
+import { generatePKCE } from "@openauthjs/openauth/pkce"
+
+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("")
+
+ const code = await UI.input("Paste the authorization code here: ")
+ await AuthAnthropic.exchange(code, verifier)
+ },
+}
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
new file mode 100644
index 000000000..c28ae4306
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -0,0 +1,140 @@
+import type { Argv } from "yargs"
+import { App } from "../../app/app"
+import { version } from "bun"
+import { Bus } from "../../bus"
+import { Provider } from "../../provider/provider"
+import { Session } from "../../session"
+import { Share } from "../../share/share"
+import { Message } from "../../session/message"
+
+export const RunCommand = {
+ command: "run [message..]",
+ describe: "Run OpenCode with a message",
+ builder: (yargs: Argv) => {
+ return yargs
+ .positional("message", {
+ describe: "Message to send",
+ type: "string",
+ array: true,
+ default: [],
+ })
+ .option("session", {
+ describe: "Session ID to continue",
+ type: "string",
+ })
+ },
+ handler: async (args: { message: string[]; session?: string }) => {
+ const message = args.message.join(" ")
+ await App.provide({ cwd: process.cwd(), version }, async () => {
+ await Share.init()
+ const session = args.session
+ ? await Session.get(args.session)
+ : await Session.create()
+
+ const styles = {
+ TEXT_HIGHLIGHT: "\x1b[96m",
+ TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
+ TEXT_DIM: "\x1b[90m",
+ TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
+ TEXT_NORMAL: "\x1b[0m",
+ TEXT_NORMAL_BOLD: "\x1b[1m",
+ TEXT_WARNING: "\x1b[93m",
+ TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
+ TEXT_DANGER: "\x1b[91m",
+ TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
+ TEXT_SUCCESS: "\x1b[92m",
+ TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
+ TEXT_INFO: "\x1b[94m",
+ TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
+ }
+
+ let isEmpty = false
+ function stderr(...message: string[]) {
+ isEmpty = true
+ Bun.stderr.write(message.join(" "))
+ Bun.stderr.write("\n")
+ }
+
+ function empty() {
+ stderr("" + styles.TEXT_NORMAL)
+ isEmpty = true
+ }
+
+ stderr(styles.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", version)
+ empty()
+ stderr(styles.TEXT_NORMAL_BOLD + "> ", message)
+ empty()
+ stderr(
+ styles.TEXT_INFO_BOLD +
+ "~ https://dev.opencode.ai/s?id=" +
+ session.id.slice(-8),
+ )
+ empty()
+
+ function printEvent(color: string, type: string, title: string) {
+ stderr(
+ color + `|`,
+ styles.TEXT_NORMAL + styles.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
+ "",
+ styles.TEXT_NORMAL + title,
+ )
+ }
+
+ Bus.subscribe(Message.Event.PartUpdated, async (message) => {
+ const part = message.properties.part
+ if (
+ part.type === "tool-invocation" &&
+ part.toolInvocation.state === "result"
+ ) {
+ if (part.toolInvocation.toolName === "opencode_todowrite") return
+ const messages = await Session.messages(session.id)
+ const metadata =
+ messages[messages.length - 1].metadata.tool[
+ part.toolInvocation.toolCallId
+ ]
+ const args = part.toolInvocation.args as any
+ const tool = part.toolInvocation.toolName
+
+ if (tool === "opencode_edit")
+ printEvent(styles.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
+ if (tool === "opencode_bash")
+ printEvent(styles.TEXT_WARNING_BOLD, "Execute", args.command)
+ if (tool === "opencode_read")
+ printEvent(styles.TEXT_INFO_BOLD, "Read", args.filePath)
+ if (tool === "opencode_write")
+ printEvent(styles.TEXT_SUCCESS_BOLD, "Create", args.filePath)
+ if (tool === "opencode_glob")
+ printEvent(
+ styles.TEXT_INFO_BOLD,
+ "Glob",
+ args.pattern + (args.path ? " in " + args.path : ""),
+ )
+ }
+
+ if (part.type === "text") {
+ if (part.text.includes("\n")) {
+ empty()
+ stderr(part.text)
+ empty()
+ return
+ }
+ printEvent(styles.TEXT_NORMAL_BOLD, "Text", part.text)
+ }
+ })
+
+ const { providerID, modelID } = await Provider.defaultModel()
+ const result = await Session.chat({
+ sessionID: session.id,
+ providerID,
+ modelID,
+ parts: [
+ {
+ type: "text",
+ text: message,
+ },
+ ],
+ })
+ empty()
+ })
+ },
+}
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
new file mode 100644
index 000000000..d08163baa
--- /dev/null
+++ b/packages/opencode/src/cli/ui.ts
@@ -0,0 +1,44 @@
+export namespace UI {
+ export const Style = {
+ TEXT_HIGHLIGHT: "\x1b[96m",
+ TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
+ TEXT_DIM: "\x1b[90m",
+ TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
+ TEXT_NORMAL: "\x1b[0m",
+ TEXT_NORMAL_BOLD: "\x1b[1m",
+ TEXT_WARNING: "\x1b[93m",
+ TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
+ TEXT_DANGER: "\x1b[91m",
+ TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
+ TEXT_SUCCESS: "\x1b[92m",
+ TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
+ TEXT_INFO: "\x1b[94m",
+ TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
+ }
+
+
+
+ export function print(...message: string[]) {
+ Bun.stderr.write(message.join(" "))
+ Bun.stderr.write("\n")
+ }
+
+ export function empty() {
+ print("" + Style.TEXT_NORMAL)
+ }
+
+ export async function input(prompt: string): Promise<string> {
+ const readline = require('readline')
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ })
+
+ return new Promise((resolve) => {
+ rl.question(prompt, (answer: string) => {
+ rl.close()
+ resolve(answer.trim())
+ })
+ })
+ }
+}
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 0ed49f6e9..ef2daa95c 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -3,208 +3,74 @@ import { App } from "./app/app"
import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
-import { Bus } from "./bus"
-import { Session } from "./session"
-import cac from "cac"
+
import { Share } from "./share/share"
-import { Message } from "./session/message"
+
import { Global } from "./global"
-import { Provider } from "./provider/provider"
+
+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"
declare global {
const OPENCODE_VERSION: string
}
-const cli = cac("opencode")
const version = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
-cli.command("", "Start the opencode in interactive mode").action(async () => {
- await App.provide({ cwd: process.cwd(), version }, async () => {
- await Share.init()
- const server = Server.listen()
-
- let cmd = ["go", "run", "./main.go"]
- let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
- if (Bun.embeddedFiles.length > 0) {
- const blob = Bun.embeddedFiles[0] as File
- const binary = path.join(Global.Path.cache, "tui", blob.name)
- const file = Bun.file(binary)
- if (!(await file.exists())) {
- console.log("installing tui binary...")
- await Bun.write(file, blob, { mode: 0o755 })
- await fs.chmod(binary, 0o755)
- }
- cwd = process.cwd()
- cmd = [binary]
- }
- const proc = Bun.spawn({
- cmd,
- cwd,
- stdout: "inherit",
- stderr: "inherit",
- stdin: "inherit",
- env: {
- ...process.env,
- OPENCODE_SERVER: server.url.toString(),
- },
- onExit: () => {
- server.stop()
- },
- })
- await proc.exited
- await server.stop()
- })
-})
-
-cli.command("generate", "Generate OpenAPI and event specs").action(async () => {
- const specs = await Server.openapi()
- const dir = "gen"
- await fs.rmdir(dir, { recursive: true }).catch(() => {})
- await fs.mkdir(dir, { recursive: true })
- await Bun.write(
- path.join(dir, "openapi.json"),
- JSON.stringify(specs, null, 2),
- )
-})
-
-cli
- .command("run [...message]", "Run a chat message")
- .option("--session <id>", "Session ID")
- .action(async (message: string[], options) => {
- await App.provide({ cwd: process.cwd(), version }, async () => {
- await Share.init()
- const session = options.session
- ? await Session.get(options.session)
- : await Session.create()
-
- const styles = {
- TEXT_HIGHLIGHT: "\x1b[96m",
- TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
- TEXT_DIM: "\x1b[90m",
- TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
- TEXT_NORMAL: "\x1b[0m",
- TEXT_NORMAL_BOLD: "\x1b[1m",
- TEXT_WARNING: "\x1b[93m",
- TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
- TEXT_DANGER: "\x1b[91m",
- TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
- TEXT_SUCCESS: "\x1b[92m",
- TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
- TEXT_INFO: "\x1b[94m",
- TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
- }
-
- let isEmpty = false
- function stderr(...message: string[]) {
- isEmpty = true
- Bun.stderr.write(message.join(" "))
- Bun.stderr.write("\n")
- }
-
- function empty() {
- stderr("" + styles.TEXT_NORMAL)
- isEmpty = true
- }
-
- stderr(styles.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", version)
- empty()
- stderr(styles.TEXT_NORMAL_BOLD + "> ", message.join(" "))
- empty()
- stderr(
- styles.TEXT_INFO_BOLD +
- "~ https://dev.opencode.ai/s?id=" +
- session.id.slice(-8),
- )
- empty()
-
- function printEvent(color: string, type: string, title: string) {
- stderr(
- color + `|`,
- styles.TEXT_NORMAL + styles.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
- "",
- styles.TEXT_NORMAL + title,
- )
- }
-
- Bus.subscribe(Message.Event.PartUpdated, async (message) => {
- const part = message.properties.part
- if (
- part.type === "tool-invocation" &&
- part.toolInvocation.state === "result"
- ) {
- if (part.toolInvocation.toolName === "opencode_todowrite") return
- const messages = await Session.messages(session.id)
- const metadata =
- messages[messages.length - 1].metadata.tool[
- part.toolInvocation.toolCallId
- ]
- const args = part.toolInvocation.args as any
- const tool = part.toolInvocation.toolName
-
- if (tool === "opencode_edit")
- printEvent(styles.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
- if (tool === "opencode_bash")
- printEvent(styles.TEXT_WARNING_BOLD, "Execute", args.command)
- if (tool === "opencode_read")
- printEvent(styles.TEXT_INFO_BOLD, "Read", args.filePath)
- if (tool === "opencode_write")
- printEvent(styles.TEXT_SUCCESS_BOLD, "Create", args.filePath)
- if (tool === "opencode_glob")
- printEvent(
- styles.TEXT_INFO_BOLD,
- "Glob",
- args.pattern + (args.path ? " in " + args.path : ""),
- )
- }
-
- if (part.type === "text") {
- if (part.text.includes("\n")) {
- empty()
- stderr(part.text)
- empty()
- return
+yargs(hideBin(process.argv))
+ .scriptName("opencode")
+ .version(version)
+ .command({
+ command: "$0",
+ describe: "Start OpenCode TUI",
+ handler: async () => {
+ await App.provide({ cwd: process.cwd(), version }, async () => {
+ await Share.init()
+ const server = Server.listen()
+
+ let cmd = ["go", "run", "./main.go"]
+ let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
+ if (Bun.embeddedFiles.length > 0) {
+ const blob = Bun.embeddedFiles[0] as File
+ const binary = path.join(Global.Path.cache, "tui", blob.name)
+ const file = Bun.file(binary)
+ if (!(await file.exists())) {
+ console.log("installing tui binary...")
+ await Bun.write(file, blob, { mode: 0o755 })
+ await fs.chmod(binary, 0o755)
}
- printEvent(styles.TEXT_NORMAL_BOLD, "Text", part.text)
+ cwd = process.cwd()
+ cmd = [binary]
}
- })
-
- const { providerID, modelID } = await Provider.defaultModel()
- const result = await Session.chat({
- sessionID: session.id,
- providerID,
- modelID,
- parts: [
- {
- type: "text",
- text: message.join(" "),
+ const proc = Bun.spawn({
+ cmd,
+ cwd,
+ stdout: "inherit",
+ stderr: "inherit",
+ stdin: "inherit",
+ env: {
+ ...process.env,
+ OPENCODE_SERVER: server.url.toString(),
},
- ],
+ onExit: () => {
+ server.stop()
+ },
+ })
+ await proc.exited
+ await server.stop()
})
- empty()
- })
+ },
})
-
-cli.command("init", "Run a chat message").action(async () => {
- await App.provide({ cwd: process.cwd(), version }, async () => {
- const { modelID, providerID } = await Provider.defaultModel()
- console.log("Initializing...")
-
- const session = await Session.create()
-
- const unsub = Bus.subscribe(Session.Event.Updated, async (message) => {
- if (message.properties.info.share?.url)
- console.log("Share:", message.properties.info.share.url)
- unsub()
- })
-
- await Session.initialize({
- sessionID: session.id,
- modelID,
- providerID,
- })
+ .command(RunCommand)
+ .command(GenerateCommand)
+ .command({
+ command: "login",
+ describe: "generate credentials for various providers",
+ builder: (yargs) => yargs.command(LoginAnthropicCommand).demandCommand(),
+ handler: () => {},
})
-})
-
-cli.version(typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev")
-cli.help()
-cli.parse()
+ .help()
+ .parse()
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 4113954ea..6c2b34e14 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -21,6 +21,7 @@ import type { Tool } from "../tool/tool"
import { MultiEditTool } from "../tool/multiedit"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
+import { AuthAnthropic } from "../auth/anthropic"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -63,6 +64,25 @@ export namespace Provider {
google: ["GOOGLE_GENERATIVE_AI_API_KEY"], // TODO: support GEMINI_API_KEY?
}
+ const AUTODETECT2: Record<
+ string,
+ () => Promise<Record<string, any> | false>
+ > = {
+ anthropic: async () => {
+ const result = await AuthAnthropic.load()
+ if (result)
+ return {
+ apiKey: "",
+ headers: {
+ authorization: `Bearer ${result.accessToken}`,
+ "anthropic-beta": "oauth-2025-04-20",
+ },
+ }
+ if (process.env["ANTHROPIC_API_KEY"]) return {}
+ return false
+ },
+ }
+
const state = App.state("provider", async () => {
log.info("loading config")
const config = await Config.get()
@@ -72,6 +92,21 @@ export namespace Provider {
const sdk = new Map<string, SDK>()
log.info("loading")
+
+ for (const [providerID, fn] of Object.entries(AUTODETECT2)) {
+ const provider = PROVIDER_DATABASE.find((x) => x.id === providerID)
+ if (!provider) continue
+ const result = await fn()
+ if (!result) continue
+ providers.set(providerID, {
+ ...provider,
+ options: {
+ ...provider.options,
+ ...result,
+ },
+ })
+ }
+
for (const item of PROVIDER_DATABASE) {
if (!AUTODETECT[item.id].some((env) => process.env[env])) continue
log.info("found", { providerID: item.id })
@@ -177,7 +212,7 @@ export namespace Provider {
PatchTool,
ReadTool,
EditTool,
- MultiEditTool,
+ // MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index d77da209e..474131ccd 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -16,6 +16,7 @@ import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
+import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
@@ -207,6 +208,24 @@ export namespace Session {
if (msgs.length === 0) {
const app = App.info()
+ if (input.providerID === "anthropic")
+ msgs.push({
+ id: Identifier.ascending("message"),
+ role: "system",
+ parts: [
+ {
+ type: "text",
+ text: PROMPT_ANTHROPIC_SPOOF.trim(),
+ },
+ ],
+ metadata: {
+ sessionID: input.sessionID,
+ time: {
+ created: Date.now(),
+ },
+ tool: {},
+ },
+ })
const system: Message.Info = {
id: Identifier.ascending("message"),
role: "system",
@@ -254,6 +273,15 @@ ${app.git ? await ListTool.execute({ path: app.path.cwd }, { sessionID: input.se
parts: [
{
type: "text",
+ text: PROMPT_ANTHROPIC_SPOOF.trim(),
+ },
+ ],
+ },
+ {
+ role: "system",
+ parts: [
+ {
+ type: "text",
text: PROMPT_TITLE,
},
],
diff --git a/packages/opencode/src/session/prompt/anthropic_spoof.txt b/packages/opencode/src/session/prompt/anthropic_spoof.txt
new file mode 100644
index 000000000..aed6cc197
--- /dev/null
+++ b/packages/opencode/src/session/prompt/anthropic_spoof.txt
@@ -0,0 +1 @@
+You are Claude Code, Anthropic's official CLI for Claude.