mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 11:14:23 +01:00
claude oauth support
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
66
packages/opencode/src/auth/anthropic.ts
Normal file
66
packages/opencode/src/auth/anthropic.ts
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
20
packages/opencode/src/cli/cmd/generate.ts
Normal file
20
packages/opencode/src/cli/cmd/generate.ts
Normal file
@@ -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
|
||||
|
||||
22
packages/opencode/src/cli/cmd/login-anthropic.ts
Normal file
22
packages/opencode/src/cli/cmd/login-anthropic.ts
Normal file
@@ -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)
|
||||
},
|
||||
}
|
||||
140
packages/opencode/src/cli/cmd/run.ts
Normal file
140
packages/opencode/src/cli/cmd/run.ts
Normal file
@@ -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()
|
||||
})
|
||||
},
|
||||
}
|
||||
44
packages/opencode/src/cli/ui.ts
Normal file
44
packages/opencode/src/cli/ui.ts
Normal file
@@ -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())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
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)
|
||||
}
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
@@ -249,6 +268,15 @@ ${app.git ? await ListTool.execute({ path: app.path.cwd }, { sessionID: input.se
|
||||
generateText({
|
||||
maxOutputTokens: 80,
|
||||
messages: convertToModelMessages([
|
||||
{
|
||||
role: "system",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: PROMPT_ANTHROPIC_SPOOF.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
parts: [
|
||||
|
||||
1
packages/opencode/src/session/prompt/anthropic_spoof.txt
Normal file
1
packages/opencode/src/session/prompt/anthropic_spoof.txt
Normal file
@@ -0,0 +1 @@
|
||||
You are Claude Code, Anthropic's official CLI for Claude.
|
||||
Reference in New Issue
Block a user