Dax Raad 8 месяцев назад
Родитель
Сommit
35b03e4cb3

+ 37 - 3
bun.lock

@@ -24,9 +24,9 @@
         "@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",
@@ -38,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",
       },
@@ -45,6 +46,7 @@
         "@tsconfig/bun": "1.0.7",
         "@types/bun": "latest",
         "@types/turndown": "5.0.5",
+        "@types/yargs": "17.0.33",
         "typescript": "catalog:",
       },
     },
@@ -286,14 +288,24 @@
 
     "@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
 
+    "@openauthjs/openauth": ["@openauthjs/[email protected]", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
+
     "@opencode/function": ["@opencode/function@workspace:packages/function"],
 
     "@opencode/web": ["@opencode/web@workspace:packages/web"],
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
+    "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
+
+    "@oslojs/binary": ["@oslojs/[email protected]", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
+
+    "@oslojs/crypto": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
+
     "@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
 
+    "@oslojs/jwt": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
+
     "@pagefind/darwin-arm64": ["@pagefind/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A=="],
 
     "@pagefind/darwin-x64": ["@pagefind/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow=="],
@@ -416,6 +428,10 @@
 
     "@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
 
+    "@types/yargs": ["@types/[email protected]", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
+
+    "@types/yargs-parser": ["@types/[email protected]", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
+
     "@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
 
     "accepts": ["[email protected]", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -434,6 +450,8 @@
 
     "anymatch": ["[email protected]", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
 
+    "arctic": ["[email protected]", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
+
     "arg": ["[email protected]", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
 
     "argparse": ["[email protected]", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -510,8 +528,6 @@
 
     "bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
 
-    "cac": ["[email protected]", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
-
     "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=="],
 
     "call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -542,6 +558,8 @@
 
     "cli-boxes": ["[email protected]", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
 
+    "cliui": ["[email protected]", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
+
     "clone": ["[email protected]", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
 
     "clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -734,6 +752,8 @@
 
     "gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
 
+    "get-caller-file": ["[email protected]", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
     "get-east-asian-width": ["[email protected]", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
 
     "get-intrinsic": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -1498,8 +1518,12 @@
 
     "xxhash-wasm": ["[email protected]", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
 
+    "y18n": ["[email protected]", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
     "yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
 
+    "yargs": ["[email protected]", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
+
     "yargs-parser": ["[email protected]", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
 
     "yocto-queue": ["[email protected]", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
@@ -1526,6 +1550,14 @@
 
     "@babel/helper-compilation-targets/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
+    "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
+
+    "@openauthjs/openauth/aws4fetch": ["[email protected]", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
+
+    "@openauthjs/openauth/jose": ["[email protected]", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
+
+    "@oslojs/jwt/@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
+
     "@rollup/pluginutils/estree-walker": ["[email protected]", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
 
     "@swc/helpers/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1582,6 +1614,8 @@
 
     "vscode-languageserver-protocol/vscode-jsonrpc": ["[email protected]", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],
 
+    "yargs/yargs-parser": ["[email protected]", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
+
     "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/[email protected]", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
 
     "@babel/helper-compilation-targets/lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],

+ 3 - 1
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"
   }

+ 5 - 1
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

+ 66 - 0
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")
+    }
+  }
+}

+ 20 - 0
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
+

+ 22 - 0
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)
+  },
+}

+ 140 - 0
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()
+    })
+  },
+}

+ 44 - 0
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())
+      })
+    })
+  }
+}

+ 56 - 190
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()

+ 36 - 1
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,

+ 28 - 0
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",
@@ -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 - 0
packages/opencode/src/session/prompt/anthropic_spoof.txt

@@ -0,0 +1 @@
+You are Claude Code, Anthropic's official CLI for Claude.