opencode-anthropic-fix 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -1
- package/.github/workflows/publish.yml +0 -31
- package/.husky/pre-commit +0 -2
- package/.husky/pre-push +0 -3
- package/.planning/CLI-ENHANCEMENTS.md +0 -221
- package/.planning/codebase/ARCHITECTURE.md +0 -201
- package/.planning/codebase/CONCERNS.md +0 -219
- package/.planning/codebase/CONVENTIONS.md +0 -335
- package/.planning/codebase/INTEGRATIONS.md +0 -215
- package/.planning/codebase/STACK.md +0 -160
- package/.planning/codebase/STRUCTURE.md +0 -244
- package/.planning/codebase/TESTING.md +0 -456
- package/.prettierignore +0 -3
- package/.prettierrc +0 -10
- package/CONTRIBUTING.md +0 -525
- package/agents.md +0 -22
- package/bun.lock +0 -43
- package/cli.mjs +0 -1775
- package/cli.test.mjs +0 -1797
- package/docs/mimese-http-header-system-prompt.md +0 -326
- package/docs/plans/afcw-upstream-watcher.json +0 -495
- package/eslint.config.mjs +0 -69
- package/index.test.mjs +0 -3359
- package/nul +0 -1
- package/script/publish.ts +0 -29
- package/scripts/build.mjs +0 -34
- package/scripts/install.mjs +0 -319
- package/test/helpers/accounts-fixtures.mjs +0 -51
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-anthropic-fix",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"main": "./index.mjs",
|
|
5
|
+
"files": [
|
|
6
|
+
"index.mjs",
|
|
7
|
+
"lib/**/*.mjs",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
5
11
|
"scripts": {
|
|
6
12
|
"build": "node scripts/build.mjs",
|
|
7
13
|
"test": "vitest run",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
bump:
|
|
7
|
-
description: "Version bump type"
|
|
8
|
-
required: false
|
|
9
|
-
default: "patch"
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
id-token: write
|
|
13
|
-
contents: read
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
publish:
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v4
|
|
20
|
-
|
|
21
|
-
- uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: "24"
|
|
24
|
-
registry-url: "https://registry.npmjs.org"
|
|
25
|
-
|
|
26
|
-
- run: npm install
|
|
27
|
-
|
|
28
|
-
- run: npm publish --access public
|
|
29
|
-
env:
|
|
30
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
31
|
-
NPM_CONFIG_PROVENANCE: false
|
package/.husky/pre-commit
DELETED
package/.husky/pre-push
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# CLI Enhancements: Full Account Management
|
|
2
|
-
|
|
3
|
-
**Status:** Complete
|
|
4
|
-
**Created:** 2026-02-09
|
|
5
|
-
**Completed:** 2026-02-09
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Problem Statement
|
|
10
|
-
|
|
11
|
-
The plugin CLI (`opencode-anthropic-auth`) can **manage** existing accounts (enable, disable, remove, switch, reset) but cannot **authenticate** new ones or re-authenticate broken ones. The only way to add an account is through `opencode auth login` → selecting "Claude Pro/Max" → browser OAuth flow inside the OpenCode TUI.
|
|
12
|
-
|
|
13
|
-
This creates friction in several scenarios:
|
|
14
|
-
|
|
15
|
-
1. **Disabled account recovery** — Account #1 was disabled due to a token refresh failure. To fix it, you'd have to `remove` it via CLI, then re-login via `opencode auth login`, which is the exact workflow we want to avoid.
|
|
16
|
-
2. **No re-authentication** — There's no way to get fresh tokens for an existing broken account without deleting and re-adding it.
|
|
17
|
-
3. **No proper logout** — The `remove` command just deletes local data without revoking tokens server-side.
|
|
18
|
-
4. **Incomplete standalone tool** — The CLI should be a self-contained account management tool that doesn't depend on the OpenCode TUI for any operation.
|
|
19
|
-
|
|
20
|
-
## Solution Overview
|
|
21
|
-
|
|
22
|
-
Add four new commands to `cli.mjs` that give full account lifecycle management:
|
|
23
|
-
|
|
24
|
-
| Command | Alias | Description |
|
|
25
|
-
| -------------- | ----- | -------------------------------------------------- |
|
|
26
|
-
| `login` | `ln` | Add a new account via browser OAuth flow |
|
|
27
|
-
| `logout <N>` | `lo` | Revoke tokens + remove a specific account |
|
|
28
|
-
| `logout --all` | — | Revoke all tokens + clear all accounts |
|
|
29
|
-
| `reauth <N>` | `ra` | Re-authenticate an account with fresh OAuth tokens |
|
|
30
|
-
| `refresh <N>` | `rf` | Attempt token refresh without browser (quick fix) |
|
|
31
|
-
|
|
32
|
-
### Architecture Change
|
|
33
|
-
|
|
34
|
-
Extract the OAuth `authorize()` and `exchange()` functions from `index.mjs` into a new shared module `lib/oauth.mjs`, plus add a new `revoke()` function. Both `index.mjs` (plugin) and `cli.mjs` (standalone) import from this shared module.
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
lib/oauth.mjs (NEW)
|
|
38
|
-
├── authorize(mode) — Generate PKCE + build auth URL
|
|
39
|
-
├── exchange(code, verifier) — Exchange auth code for tokens
|
|
40
|
-
└── revoke(refreshToken) — Revoke a refresh token (best-effort)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Design Decisions
|
|
44
|
-
|
|
45
|
-
| Decision | Choice | Rationale |
|
|
46
|
-
| --------------------- | -------------------- | ----------------------------------------------------------------------- |
|
|
47
|
-
| Reauth matching | Replace at index | Simpler UX; user explicitly chooses which slot to replace |
|
|
48
|
-
| Logout behavior | Revoke + remove | Clean break; user can re-add via `login` |
|
|
49
|
-
| Token refresh command | Yes, add `refresh` | Quick fix for expired access tokens without browser round-trip |
|
|
50
|
-
| Browser opening | Auto-open + show URL | Best UX: tries `open`/`xdg-open`/`start`, always prints URL as fallback |
|
|
51
|
-
|
|
52
|
-
## Detailed Command Specifications
|
|
53
|
-
|
|
54
|
-
### `login` (alias: `ln`)
|
|
55
|
-
|
|
56
|
-
**Flow:**
|
|
57
|
-
|
|
58
|
-
1. Call `authorize("max")` to get PKCE URL + verifier
|
|
59
|
-
2. Auto-open the URL in the user's default browser
|
|
60
|
-
3. Print the URL to terminal as fallback
|
|
61
|
-
4. Prompt user to paste the authorization code (readline)
|
|
62
|
-
5. Call `exchange(code, verifier)` to get tokens
|
|
63
|
-
6. Deduplicate: check if an account with the same refresh token already exists
|
|
64
|
-
7. Add the account to `anthropic-accounts.json`, save, print confirmation
|
|
65
|
-
|
|
66
|
-
**Edge cases:**
|
|
67
|
-
|
|
68
|
-
- If exchange fails, print error and exit with code 1
|
|
69
|
-
- If max accounts (10) reached, print error
|
|
70
|
-
- If duplicate refresh token, update existing account instead of adding
|
|
71
|
-
|
|
72
|
-
### `logout <N>` (alias: `lo <N>`)
|
|
73
|
-
|
|
74
|
-
**Flow:**
|
|
75
|
-
|
|
76
|
-
1. Validate index N exists
|
|
77
|
-
2. Attempt `revoke(account.refreshToken)` — best-effort, won't block on failure
|
|
78
|
-
3. Remove the account from array (splice)
|
|
79
|
-
4. Adjust `activeIndex` if needed (same logic as existing `remove` command)
|
|
80
|
-
5. Save + print confirmation
|
|
81
|
-
|
|
82
|
-
**Flags:**
|
|
83
|
-
|
|
84
|
-
- `--force` — Skip confirmation prompt (same as `remove`)
|
|
85
|
-
- `--all` — Logout all accounts
|
|
86
|
-
|
|
87
|
-
### `logout --all`
|
|
88
|
-
|
|
89
|
-
**Flow:**
|
|
90
|
-
|
|
91
|
-
1. Iterate all accounts, attempt `revoke()` for each
|
|
92
|
-
2. Write explicit empty storage `{ version: 1, accounts: [], activeIndex: 0 }`
|
|
93
|
-
3. Print confirmation with count of revoked accounts
|
|
94
|
-
|
|
95
|
-
**Important behavior:** empty storage is treated as authoritative by `AccountManager.load()`, so plugin instances stay logged out and do not re-bootstrap from fallback auth when the accounts file explicitly contains zero accounts.
|
|
96
|
-
|
|
97
|
-
### `reauth <N>` (alias: `ra <N>`)
|
|
98
|
-
|
|
99
|
-
**Flow:**
|
|
100
|
-
|
|
101
|
-
1. Validate index N exists
|
|
102
|
-
2. Run full OAuth flow (same as `login` steps 1-5)
|
|
103
|
-
3. Replace the account at index N with the new tokens
|
|
104
|
-
4. Preserve: `id`, `addedAt`, `stats`
|
|
105
|
-
5. Reset: `enabled=true`, `consecutiveFailures=0`, `lastFailureTime=null`, `rateLimitResetTimes={}`
|
|
106
|
-
6. Save + print confirmation
|
|
107
|
-
|
|
108
|
-
**Key detail:** Replaces whatever's at index N with whatever the user logs in with, regardless of email matching.
|
|
109
|
-
|
|
110
|
-
### `refresh <N>` (alias: `rf <N>`)
|
|
111
|
-
|
|
112
|
-
**Flow:**
|
|
113
|
-
|
|
114
|
-
1. Validate index N exists
|
|
115
|
-
2. Call the existing `refreshAccessToken(account)` function
|
|
116
|
-
3. If success: update account, re-enable if disabled, save + print new token expiry
|
|
117
|
-
4. If failure: print error and suggest `reauth <N>` instead
|
|
118
|
-
|
|
119
|
-
## Files Modified
|
|
120
|
-
|
|
121
|
-
| File | Change | Status |
|
|
122
|
-
| ------------------- | -------------------------------------------------------------------- | ------ |
|
|
123
|
-
| `lib/oauth.mjs` | **NEW** — Extracted `authorize()`, `exchange()`, plus new `revoke()` | Done |
|
|
124
|
-
| `index.mjs` | Replace inline OAuth functions with imports from `lib/oauth.mjs` | Done |
|
|
125
|
-
| `cli.mjs` | Add 5 new commands + browser opener utility | Done |
|
|
126
|
-
| `scripts/build.mjs` | No change needed — esbuild follows imports automatically | N/A |
|
|
127
|
-
|
|
128
|
-
## Task List
|
|
129
|
-
|
|
130
|
-
- [x] **Task 1:** Create `lib/oauth.mjs` — extract `authorize()` and `exchange()` from `index.mjs`, add `revoke()`
|
|
131
|
-
- [x] **Task 2:** Update `index.mjs` — replace inline functions with imports from `lib/oauth.mjs`
|
|
132
|
-
- [x] **Task 3:** Add `openBrowser()` utility to `cli.mjs`
|
|
133
|
-
- [x] **Task 4:** Implement `cmdLogin` in `cli.mjs`
|
|
134
|
-
- [x] **Task 5:** Implement `cmdLogout` and `cmdLogoutAll` in `cli.mjs`
|
|
135
|
-
- [x] **Task 6:** Implement `cmdReauth` in `cli.mjs`
|
|
136
|
-
- [x] **Task 7:** Implement `cmdRefresh` in `cli.mjs`
|
|
137
|
-
- [x] **Task 8:** Wire new commands into `main()` dispatcher and update `cmdHelp()`
|
|
138
|
-
- [x] **Task 9:** Build — passes (esbuild picks up `lib/oauth.mjs` automatically)
|
|
139
|
-
- [x] **Task 10:** Tests — all 349 tests pass across 7 test files, zero failures
|
|
140
|
-
- [x] **Task 11:** Lint — eslint passes cleanly
|
|
141
|
-
- [x] **Task 12:** Live verification — `refresh 1` correctly fails (broken token), `refresh 2` succeeds (7h 59m expiry)
|
|
142
|
-
- [x] **Task 13:** Update this document with completion status
|
|
143
|
-
|
|
144
|
-
## Token Revocation Notes
|
|
145
|
-
|
|
146
|
-
Anthropic's OAuth implementation may or may not support RFC 7009 token revocation at `POST /v1/oauth/revoke`. The `revoke()` function is implemented as best-effort:
|
|
147
|
-
|
|
148
|
-
- Attempts `POST https://console.anthropic.com/v1/oauth/revoke` with `{ token: refreshToken, token_type_hint: "refresh_token", client_id: CLIENT_ID }`
|
|
149
|
-
- Returns `true` if 2xx response, `false` otherwise
|
|
150
|
-
- `logout` proceeds with local cleanup regardless of revocation result
|
|
151
|
-
- No error is surfaced to the user if revocation fails — only a dim note
|
|
152
|
-
|
|
153
|
-
## Immediate Use Case
|
|
154
|
-
|
|
155
|
-
To fix the disabled account #1:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
# Option A: Try a token refresh first (no browser)
|
|
159
|
-
opencode-anthropic-auth refresh 1
|
|
160
|
-
# Tested: correctly fails with "token refresh failed" and suggests reauth
|
|
161
|
-
|
|
162
|
-
# Option B: If refresh fails, do a full re-auth
|
|
163
|
-
opencode-anthropic-auth reauth 1
|
|
164
|
-
|
|
165
|
-
# Option C: Full nuke and re-login
|
|
166
|
-
opencode-anthropic-auth logout 1
|
|
167
|
-
opencode-anthropic-auth login
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Verified:** `refresh 2` (healthy account) succeeds and reports "Token refreshed, expires in 7h 59m".
|
|
171
|
-
|
|
172
|
-
## Updated Help Text (Preview)
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
Anthropic Multi-Account Auth CLI
|
|
176
|
-
|
|
177
|
-
Usage:
|
|
178
|
-
opencode-anthropic-auth [command] [args]
|
|
179
|
-
|
|
180
|
-
Commands:
|
|
181
|
-
list Show all accounts with status (default)
|
|
182
|
-
status Compact one-liner for scripts/prompts
|
|
183
|
-
login Add a new account via browser OAuth flow
|
|
184
|
-
logout <N> Revoke tokens and remove account N
|
|
185
|
-
logout --all Revoke all tokens and clear all accounts
|
|
186
|
-
reauth <N> Re-authenticate account N with fresh OAuth tokens
|
|
187
|
-
refresh <N> Attempt token refresh (no browser needed)
|
|
188
|
-
switch <N> Set account N as active
|
|
189
|
-
enable <N> Enable a disabled account
|
|
190
|
-
disable <N> Disable an account (skipped in rotation)
|
|
191
|
-
remove <N> Remove an account permanently
|
|
192
|
-
reset <N|all> Clear rate-limit / failure tracking
|
|
193
|
-
stats Show per-account usage statistics
|
|
194
|
-
reset-stats [N|all] Reset usage statistics
|
|
195
|
-
strategy [name] Show or change selection strategy
|
|
196
|
-
config Show configuration and file paths
|
|
197
|
-
manage Interactive account management menu
|
|
198
|
-
help Show this help message
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Post-Review Remediations
|
|
202
|
-
|
|
203
|
-
Following delegated code review, the following fixes were applied:
|
|
204
|
-
|
|
205
|
-
1. **Logout-all semantics hardened**
|
|
206
|
-
- `cmdLogoutAll` now writes explicit empty account storage instead of deleting the accounts file.
|
|
207
|
-
- `AccountManager.load` now treats existing storage (including empty storage) as authoritative, preventing fallback re-bootstrap when users explicitly logout-all.
|
|
208
|
-
|
|
209
|
-
2. **Windows browser launch fixed**
|
|
210
|
-
- `openBrowser` now uses `cmd /c start "" <url>` on Windows for correct URL handling.
|
|
211
|
-
|
|
212
|
-
3. **Reauth state message bug fixed**
|
|
213
|
-
- `cmdReauth` now captures prior disabled state (`wasDisabled`) before mutation and correctly prints "re-enabled" message when applicable.
|
|
214
|
-
|
|
215
|
-
4. **Login dedupe ordering corrected**
|
|
216
|
-
- `cmdLogin` now performs duplicate-token update path before applying max-account rejection, so existing accounts can still be refreshed when at capacity.
|
|
217
|
-
|
|
218
|
-
5. **Unit test coverage expanded for new auth commands**
|
|
219
|
-
- Added direct CLI unit tests for `cmdLogin`, `cmdLogout` (single + `--all`), `cmdReauth`, and `cmdRefresh`.
|
|
220
|
-
- Added main routing tests for new aliases/paths: `ln`, `lo --all --force`, `ra`, and `rf`.
|
|
221
|
-
- Added help text assertions for new commands/examples.
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
**Analysis Date:** 2026-02-07
|
|
4
|
-
|
|
5
|
-
## Pattern Overview
|
|
6
|
-
|
|
7
|
-
**Overall:** Plugin + CLI dual-entry-point architecture with shared library layer
|
|
8
|
-
|
|
9
|
-
**Key Characteristics:**
|
|
10
|
-
|
|
11
|
-
- OpenCode plugin providing OAuth-based multi-account Anthropic authentication
|
|
12
|
-
- Standalone CLI binary for account management (shares storage layer with plugin)
|
|
13
|
-
- Fetch interceptor pattern: the plugin wraps HTTP requests to Anthropic API, injecting OAuth tokens, transforming request/response bodies, and rotating accounts on failures
|
|
14
|
-
- Stateful account management with disk persistence and in-memory health tracking
|
|
15
|
-
- No framework — pure Node.js ESM modules with zero runtime dependencies beyond `@openauthjs/openauth`
|
|
16
|
-
|
|
17
|
-
## Layers
|
|
18
|
-
|
|
19
|
-
**Plugin Entry (`index.mjs`):**
|
|
20
|
-
|
|
21
|
-
- Purpose: OpenCode plugin that intercepts Anthropic API calls with OAuth multi-account support
|
|
22
|
-
- Location: `index.mjs`
|
|
23
|
-
- Contains: OAuth authorize/exchange helpers, request/response transform functions, `AnthropicAuthPlugin` factory, token refresh logic, CLI-style interactive prompts for auth flow
|
|
24
|
-
- Depends on: `lib/accounts.mjs`, `lib/config.mjs`, `lib/storage.mjs`, `lib/backoff.mjs`, `@openauthjs/openauth`
|
|
25
|
-
- Used by: OpenCode runtime (loaded as plugin)
|
|
26
|
-
|
|
27
|
-
**CLI Entry (`cli.mjs`):**
|
|
28
|
-
|
|
29
|
-
- Purpose: Standalone CLI for managing accounts outside of the OpenCode TUI
|
|
30
|
-
- Location: `cli.mjs`
|
|
31
|
-
- Contains: All CLI commands (list, switch, enable, disable, remove, reset, stats, strategy, config, manage), formatting/color helpers, usage quota fetching
|
|
32
|
-
- Depends on: `lib/storage.mjs`, `lib/config.mjs`
|
|
33
|
-
- Used by: End users via `opencode-anthropic-auth` binary
|
|
34
|
-
|
|
35
|
-
**Account Management (`lib/accounts.mjs`):**
|
|
36
|
-
|
|
37
|
-
- Purpose: In-memory account pool with health scoring, rotation, and disk sync
|
|
38
|
-
- Location: `lib/accounts.mjs`
|
|
39
|
-
- Contains: `AccountManager` class — the core runtime coordinator for account selection, failure tracking, rate limit handling, and usage statistics
|
|
40
|
-
- Depends on: `lib/storage.mjs`, `lib/rotation.mjs`, `lib/backoff.mjs`
|
|
41
|
-
- Used by: `index.mjs`
|
|
42
|
-
|
|
43
|
-
**Rotation Logic (`lib/rotation.mjs`):**
|
|
44
|
-
|
|
45
|
-
- Purpose: Account selection algorithms and health/token tracking
|
|
46
|
-
- Location: `lib/rotation.mjs`
|
|
47
|
-
- Contains: `HealthScoreTracker` class, `TokenBucketTracker` class, `selectAccount()` function implementing sticky/round-robin/hybrid strategies
|
|
48
|
-
- Depends on: `lib/config.mjs` (for defaults only)
|
|
49
|
-
- Used by: `lib/accounts.mjs`
|
|
50
|
-
|
|
51
|
-
**Backoff Logic (`lib/backoff.mjs`):**
|
|
52
|
-
|
|
53
|
-
- Purpose: Error classification and retry backoff calculation
|
|
54
|
-
- Location: `lib/backoff.mjs`
|
|
55
|
-
- Contains: `isAccountSpecificError()`, `parseRateLimitReason()`, `calculateBackoffMs()`, `parseRetryAfterHeader()`
|
|
56
|
-
- Depends on: Nothing
|
|
57
|
-
- Used by: `index.mjs`, `lib/accounts.mjs`
|
|
58
|
-
|
|
59
|
-
**Configuration (`lib/config.mjs`):**
|
|
60
|
-
|
|
61
|
-
- Purpose: Config file loading, validation, and env var overrides
|
|
62
|
-
- Location: `lib/config.mjs`
|
|
63
|
-
- Contains: `loadConfig()`, `saveConfig()`, `DEFAULT_CONFIG`, `CLIENT_ID`, strategy validation, XDG-compliant path resolution
|
|
64
|
-
- Depends on: Node.js builtins only
|
|
65
|
-
- Used by: `index.mjs`, `cli.mjs`, `lib/accounts.mjs`, `lib/rotation.mjs`
|
|
66
|
-
|
|
67
|
-
**Storage (`lib/storage.mjs`):**
|
|
68
|
-
|
|
69
|
-
- Purpose: Atomic JSON file persistence for account data
|
|
70
|
-
- Location: `lib/storage.mjs`
|
|
71
|
-
- Contains: `loadAccounts()`, `saveAccounts()`, `clearAccounts()`, account validation/deduplication, `.gitignore` management
|
|
72
|
-
- Depends on: `lib/config.mjs` (for config dir path)
|
|
73
|
-
- Used by: `index.mjs`, `cli.mjs`, `lib/accounts.mjs`
|
|
74
|
-
|
|
75
|
-
## Data Flow
|
|
76
|
-
|
|
77
|
-
**API Request (Plugin fetch interceptor):**
|
|
78
|
-
|
|
79
|
-
1. OpenCode calls `fetch(input, init)` on the plugin's custom fetch
|
|
80
|
-
2. `transformRequestBody()` sanitizes system prompts and prefixes tool names with `mcp_`
|
|
81
|
-
3. `transformRequestUrl()` adds `?beta=true` to `/v1/messages` endpoint
|
|
82
|
-
4. `AccountManager.getCurrentAccount()` selects best account via strategy (sticky/round-robin/hybrid)
|
|
83
|
-
5. If token expired, `refreshAccountTokenSingleFlight()` refreshes OAuth token (single-flight deduplication)
|
|
84
|
-
6. `buildRequestHeaders()` sets Authorization bearer, anthropic-beta headers, removes x-api-key
|
|
85
|
-
7. Actual `fetch()` to Anthropic API executes
|
|
86
|
-
8. On account-specific error (429, 401, 400/403 with account language): mark account, try next account (up to N accounts)
|
|
87
|
-
9. On service-wide error (529, 503, 500): return immediately (switching won't help)
|
|
88
|
-
10. On success: `transformResponse()` wraps the SSE stream to strip `mcp_` prefix from tool names, extract token usage, and detect mid-stream account errors
|
|
89
|
-
|
|
90
|
-
**Account Selection (per-request):**
|
|
91
|
-
|
|
92
|
-
1. `AccountManager.syncActiveIndexFromDisk()` reconciles with CLI changes
|
|
93
|
-
2. Build candidate list: enabled accounts, not in per-request exclusion set
|
|
94
|
-
3. `selectAccount()` applies configured strategy:
|
|
95
|
-
- **sticky**: Stay on current account unless rate-limited/failed
|
|
96
|
-
- **round-robin**: Rotate cursor through available accounts
|
|
97
|
-
- **hybrid**: Score accounts by health (weighted ×2) + token bucket (weighted ×5) + freshness, with stickiness bonus (150 points) and switch threshold (100 points)
|
|
98
|
-
4. Consume a token bucket token for the selected account
|
|
99
|
-
5. Return selected `ManagedAccount`
|
|
100
|
-
|
|
101
|
-
**OAuth Login Flow:**
|
|
102
|
-
|
|
103
|
-
1. User triggers `opencode auth login` → plugin's `auth.methods[0].authorize()` runs
|
|
104
|
-
2. If existing accounts: prompt menu (add/fresh/manage/cancel)
|
|
105
|
-
3. Generate PKCE challenge via `@openauthjs/openauth`
|
|
106
|
-
4. Build authorize URL for `claude.ai/oauth/authorize`
|
|
107
|
-
5. User pastes authorization code
|
|
108
|
-
6. `exchange()` POSTs to `console.anthropic.com/v1/oauth/token` for tokens
|
|
109
|
-
7. `AccountManager.addAccount()` stores new credentials (deduplicates by refresh token)
|
|
110
|
-
8. `AccountManager.saveToDisk()` persists to `~/.config/opencode/anthropic-accounts.json`
|
|
111
|
-
|
|
112
|
-
**State Management:**
|
|
113
|
-
|
|
114
|
-
- **In-memory (plugin process):** `AccountManager` with `HealthScoreTracker` and `TokenBucketTracker` — these are ephemeral, rebuilt on plugin load
|
|
115
|
-
- **On-disk (shared):** `anthropic-accounts.json` stores account credentials, failure tracking, rate limit reset times, usage stats, and active index
|
|
116
|
-
- **Sync mechanism:** Plugin reads `activeIndex` from disk before each request to pick up CLI changes; debounced writes (1s) persist plugin-side changes back
|
|
117
|
-
- **Stats merge-on-save:** To handle concurrent instances, `saveToDisk()` reads disk values and applies in-memory deltas rather than overwriting
|
|
118
|
-
|
|
119
|
-
## Key Abstractions
|
|
120
|
-
|
|
121
|
-
**AccountManager (`lib/accounts.mjs`):**
|
|
122
|
-
|
|
123
|
-
- Purpose: Central coordinator for multi-account state and request routing
|
|
124
|
-
- Pattern: Singleton per plugin instance with private state (`#accounts`, `#healthTracker`, `#tokenTracker`)
|
|
125
|
-
- Static factory: `AccountManager.load(config, authFallback)` bootstraps from disk + OpenCode auth
|
|
126
|
-
- Key methods: `getCurrentAccount()`, `markRateLimited()`, `markSuccess()`, `markFailure()`, `recordUsage()`, `syncActiveIndexFromDisk()`
|
|
127
|
-
|
|
128
|
-
**HealthScoreTracker (`lib/rotation.mjs`):**
|
|
129
|
-
|
|
130
|
-
- Purpose: Track per-account health with passive time-based recovery
|
|
131
|
-
- Pattern: In-memory score map with configurable initial/penalty/recovery values
|
|
132
|
-
- Score range: 0–100, min_usable threshold (default 50) gates account selection
|
|
133
|
-
|
|
134
|
-
**TokenBucketTracker (`lib/rotation.mjs`):**
|
|
135
|
-
|
|
136
|
-
- Purpose: Rate-limit requests per account using token bucket algorithm
|
|
137
|
-
- Pattern: Per-account bucket with configurable max tokens, regeneration rate
|
|
138
|
-
- Default: 50 tokens, 6/minute regen
|
|
139
|
-
|
|
140
|
-
**ManagedAccount (typedef in `lib/accounts.mjs`):**
|
|
141
|
-
|
|
142
|
-
- Purpose: Runtime representation of an Anthropic OAuth account
|
|
143
|
-
- Contains: id, index, email, refreshToken, access, expires, enabled, rateLimitResetTimes, consecutiveFailures, stats
|
|
144
|
-
- Pattern: Mutable in-memory state with periodic disk sync
|
|
145
|
-
|
|
146
|
-
## Entry Points
|
|
147
|
-
|
|
148
|
-
**Plugin (`index.mjs` → `AnthropicAuthPlugin`):**
|
|
149
|
-
|
|
150
|
-
- Location: `index.mjs`
|
|
151
|
-
- Triggers: Loaded by OpenCode plugin system as ESM module
|
|
152
|
-
- Responsibilities: Exports an async plugin factory function returning hooks for `experimental.chat.system.transform`, `auth.loader`, and `auth.methods`
|
|
153
|
-
- Exported as: `export async function AnthropicAuthPlugin({ client })`
|
|
154
|
-
|
|
155
|
-
**CLI (`cli.mjs` → `main()`):**
|
|
156
|
-
|
|
157
|
-
- Location: `cli.mjs`
|
|
158
|
-
- Triggers: Executed as `opencode-anthropic-auth [command]` via `~/.local/bin` symlink/copy
|
|
159
|
-
- Responsibilities: Parse argv, route to command handlers, manage accounts via direct storage manipulation
|
|
160
|
-
- Self-detection: Uses `import.meta.url === pathToFileURL(process.argv[1]).href` with symlink resolution
|
|
161
|
-
|
|
162
|
-
**Build (`scripts/build.mjs`):**
|
|
163
|
-
|
|
164
|
-
- Location: `scripts/build.mjs`
|
|
165
|
-
- Triggers: `npm run build`
|
|
166
|
-
- Responsibilities: Bundle `index.mjs` → `dist/opencode-anthropic-auth-plugin.js` and `cli.mjs` → `dist/opencode-anthropic-auth-cli.mjs` using esbuild (ESM, node20 target, all deps bundled except node: builtins)
|
|
167
|
-
|
|
168
|
-
**Install (`scripts/install.mjs`):**
|
|
169
|
-
|
|
170
|
-
- Location: `scripts/install.mjs`
|
|
171
|
-
- Triggers: `npm run install:link` (dev) or `npm run install:copy` (production)
|
|
172
|
-
- Responsibilities: Symlink or copy plugin to `~/.config/opencode/plugin/` and CLI to `~/.local/bin/`
|
|
173
|
-
|
|
174
|
-
## Error Handling
|
|
175
|
-
|
|
176
|
-
**Strategy:** Account-specific vs. service-wide error classification with automatic account rotation
|
|
177
|
-
|
|
178
|
-
**Patterns:**
|
|
179
|
-
|
|
180
|
-
- **Account-specific errors** (429, 401, 400/403 with account error body): Mark account with backoff, rotate to next account. Auth failures (401) invalidate cached token.
|
|
181
|
-
- **Service-wide errors** (529, 503, 500): Return immediately to caller — switching accounts won't help.
|
|
182
|
-
- **Token refresh failures**: Transient errors skip account for current request; terminal errors (400, 401, 403, `invalid_grant`) disable the account permanently.
|
|
183
|
-
- **Mid-stream SSE errors**: Detected in response stream transformer. Account marked for next request (not retried mid-stream).
|
|
184
|
-
- **Single-flight token refresh**: Concurrent requests to the same account share one in-flight refresh promise to prevent race conditions.
|
|
185
|
-
- **Backoff calculation** (`lib/backoff.mjs`): Retry-After header takes precedence. Otherwise, reason-based: AUTH_FAILED=5s, RATE_LIMIT_EXCEEDED=30s, QUOTA_EXHAUSTED=escalating [1m, 5m, 30m, 2h].
|
|
186
|
-
|
|
187
|
-
## Cross-Cutting Concerns
|
|
188
|
-
|
|
189
|
-
**Logging:** Debug logging via `console.error("[opencode-anthropic-auth]", ...)` gated by `config.debug` flag (env: `OPENCODE_ANTHROPIC_DEBUG=1`). CLI uses direct `console.log` with ANSI color helpers.
|
|
190
|
-
|
|
191
|
-
**Validation:** Config values are clamped to valid ranges in `lib/config.mjs` (`clampNumber()`). Account data is validated on load with type checks and fallbacks in `lib/storage.mjs` (`validateAccount()`). Duplicate refresh tokens are deduplicated.
|
|
192
|
-
|
|
193
|
-
**Persistence:** All disk writes use atomic temp-file-and-rename pattern. Account storage uses `randomBytes(6).hex` temp names; config uses PID-based temp names. Files are written with `mode: 0o600` for security.
|
|
194
|
-
|
|
195
|
-
**Configuration Precedence:** Config file (`~/.config/opencode/anthropic-auth.json`) → environment variable overrides (`OPENCODE_ANTHROPIC_STRATEGY`, `OPENCODE_ANTHROPIC_DEBUG`, `OPENCODE_ANTHROPIC_QUIET`).
|
|
196
|
-
|
|
197
|
-
**Toast Notifications:** Plugin sends TUI toasts via `client.tui?.showToast()` for account usage, switching, and errors. Toasts are debounced per category and suppressible via quiet mode.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
_Architecture analysis: 2026-02-07_
|