opencode-anthropic-fix 0.0.16 → 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/index.mjs +466 -90
- package/lib/account-state.mjs +50 -0
- package/lib/account-state.test.mjs +109 -0
- package/lib/accounts.mjs +100 -11
- package/lib/accounts.test.mjs +95 -0
- package/lib/config.mjs +60 -0
- package/lib/oauth.mjs +35 -0
- package/lib/refresh-lock.mjs +123 -0
- package/lib/refresh-lock.test.mjs +79 -0
- package/lib/storage.mjs +76 -1
- package/lib/storage.test.mjs +133 -0
- 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 -1773
- 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 -2690
- package/nul +0 -1
- package/script/publish.ts +0 -29
- package/scripts/build.mjs +0 -34
- package/scripts/install.mjs +0 -319
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
# Coding Conventions
|
|
2
|
-
|
|
3
|
-
**Analysis Date:** 2026-02-07
|
|
4
|
-
|
|
5
|
-
## Language & Module System
|
|
6
|
-
|
|
7
|
-
**Language:** Pure JavaScript (ES2025) — no TypeScript, no compilation for source files.
|
|
8
|
-
**Module System:** ESM exclusively. All source files use `.mjs` extension.
|
|
9
|
-
**Type Safety:** JSDoc annotations provide full type information without a compile step.
|
|
10
|
-
|
|
11
|
-
## Naming Patterns
|
|
12
|
-
|
|
13
|
-
**Files:**
|
|
14
|
-
|
|
15
|
-
- Source files: `kebab-case.mjs` — e.g., `lib/storage.mjs`, `lib/backoff.mjs`
|
|
16
|
-
- Test files: `kebab-case.test.mjs` — co-located next to source
|
|
17
|
-
- Scripts: `kebab-case.mjs` in `scripts/` — e.g., `scripts/build.mjs`, `scripts/install.mjs`
|
|
18
|
-
- Entry points: `index.mjs`, `cli.mjs` at project root
|
|
19
|
-
|
|
20
|
-
**Functions:**
|
|
21
|
-
|
|
22
|
-
- Use `camelCase` for all functions: `loadAccounts`, `calculateBackoffMs`, `parseRetryAfterHeader`
|
|
23
|
-
- Factory/builder functions: `make*` prefix — `makeStoredAccount`, `makeClient`
|
|
24
|
-
- Validation functions: `validate*` prefix — `validateAccount`, `validateConfig`, `validateStats`
|
|
25
|
-
- Boolean-returning functions: `is*` prefix — `isAccountSpecificError`, `isUsable`
|
|
26
|
-
- Getters: `get*` prefix — `getScore`, `getConfigDir`, `getStoragePath`, `getCurrentIndex`
|
|
27
|
-
- Event recorders: `record*` prefix — `recordSuccess`, `recordRateLimit`, `recordFailure`
|
|
28
|
-
|
|
29
|
-
**Variables:**
|
|
30
|
-
|
|
31
|
-
- Use `camelCase`: `storagePath`, `configDir`, `activeIndex`
|
|
32
|
-
- Constants: `UPPER_SNAKE_CASE` — `MAX_ACCOUNTS`, `RATE_LIMIT_KEY`, `CURRENT_VERSION`
|
|
33
|
-
- Unused parameters: prefix with `_` — `_error`, `_response` (enforced by ESLint `argsIgnorePattern: "^_"`)
|
|
34
|
-
|
|
35
|
-
**Classes:**
|
|
36
|
-
|
|
37
|
-
- Use `PascalCase`: `AccountManager`, `HealthScoreTracker`, `TokenBucketTracker`
|
|
38
|
-
- Private fields use `#` prefix (true private fields): `#accounts`, `#cursor`, `#config`, `#scores`
|
|
39
|
-
- No underscore-prefixed "private" conventions — always use `#` for private state
|
|
40
|
-
|
|
41
|
-
**Types (JSDoc):**
|
|
42
|
-
|
|
43
|
-
- Use `PascalCase` for `@typedef` names: `ManagedAccount`, `AccountStorage`, `RateLimitReason`
|
|
44
|
-
- String literal unions: `@typedef {'AUTH_FAILED' | 'QUOTA_EXHAUSTED' | 'RATE_LIMIT_EXCEEDED'} RateLimitReason`
|
|
45
|
-
|
|
46
|
-
## Code Style
|
|
47
|
-
|
|
48
|
-
**Formatting (Prettier):**
|
|
49
|
-
|
|
50
|
-
- Config: `.prettierrc`
|
|
51
|
-
- Print width: 120
|
|
52
|
-
- Indent: 2 spaces (no tabs)
|
|
53
|
-
- Semicolons: always
|
|
54
|
-
- Quotes: double quotes (`"`)
|
|
55
|
-
- Trailing commas: all (including function parameters)
|
|
56
|
-
- Bracket spacing: true
|
|
57
|
-
- Arrow parens: always
|
|
58
|
-
|
|
59
|
-
**Linting (ESLint):**
|
|
60
|
-
|
|
61
|
-
- Config: `eslint.config.mjs` (flat config format)
|
|
62
|
-
- Base: `@eslint/js` recommended
|
|
63
|
-
- ECMAScript: 2025
|
|
64
|
-
- Key rules:
|
|
65
|
-
- `prefer-const`: error — always use `const` unless reassignment needed
|
|
66
|
-
- `no-var`: error — never use `var`
|
|
67
|
-
- `eqeqeq: smart` — use `===` except allow `== null` for null/undefined checks
|
|
68
|
-
- `no-console: off` — this is a CLI tool, console output is expected
|
|
69
|
-
- `no-constant-condition: { checkLoops: false }` — `while (true)` is intentional
|
|
70
|
-
- `no-unused-vars: warn` with `argsIgnorePattern: "^_"` and `varsIgnorePattern: "^_"`
|
|
71
|
-
- Globals: Node.js + Web API globals declared explicitly (no `node: true` environment)
|
|
72
|
-
|
|
73
|
-
**Pre-commit (Husky + lint-staged):**
|
|
74
|
-
|
|
75
|
-
- Config: `package.json` under `lint-staged`
|
|
76
|
-
- `.mjs` files: `prettier --write` then `eslint --fix`
|
|
77
|
-
- `.json`, `.md` files: `prettier --write`
|
|
78
|
-
- Pre-push hook: runs `npm test`
|
|
79
|
-
|
|
80
|
-
## Import Organization
|
|
81
|
-
|
|
82
|
-
**Order:**
|
|
83
|
-
|
|
84
|
-
1. External packages — `import { createClient } from "@openauthjs/openauth/client"`
|
|
85
|
-
2. Node built-ins — `import { promises as fs } from "node:fs"`, `import { join } from "node:path"`
|
|
86
|
-
3. Local modules — `import { loadAccounts } from "./lib/storage.mjs"`
|
|
87
|
-
|
|
88
|
-
**Rules:**
|
|
89
|
-
|
|
90
|
-
- Always use `node:` prefix for Node.js built-in modules
|
|
91
|
-
- Always include `.mjs` extension in relative imports
|
|
92
|
-
- Named exports only — no default exports anywhere in the codebase
|
|
93
|
-
- Destructured imports preferred: `import { loadAccounts, saveAccounts } from "./storage.mjs"`
|
|
94
|
-
|
|
95
|
-
**Path Aliases:**
|
|
96
|
-
|
|
97
|
-
- None configured. All imports use relative paths.
|
|
98
|
-
|
|
99
|
-
## JSDoc Type System
|
|
100
|
-
|
|
101
|
-
**Every function gets JSDoc:**
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
/**
|
|
105
|
-
* Parse the Retry-After header from a response.
|
|
106
|
-
* Supports both seconds (integer) and HTTP-date formats.
|
|
107
|
-
* @param {Response} response
|
|
108
|
-
* @returns {number | null} Retry-after duration in milliseconds, or null
|
|
109
|
-
*/
|
|
110
|
-
export function parseRetryAfterHeader(response) {
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**Type definitions at the top of files using `@typedef`:**
|
|
114
|
-
|
|
115
|
-
```javascript
|
|
116
|
-
/**
|
|
117
|
-
* @typedef {object} ManagedAccount
|
|
118
|
-
* @property {string} id
|
|
119
|
-
* @property {number} index
|
|
120
|
-
* @property {string} [email]
|
|
121
|
-
* @property {string} refreshToken
|
|
122
|
-
* @property {boolean} enabled
|
|
123
|
-
* @property {AccountStats} stats
|
|
124
|
-
*/
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Cross-file type imports:**
|
|
128
|
-
|
|
129
|
-
```javascript
|
|
130
|
-
/**
|
|
131
|
-
* @typedef {import('./config.mjs').AnthropicAuthConfig} AnthropicAuthConfig
|
|
132
|
-
* @typedef {import('./storage.mjs').AccountMetadata} AccountMetadata
|
|
133
|
-
*/
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Inline JSDoc casts for type narrowing:**
|
|
137
|
-
|
|
138
|
-
```javascript
|
|
139
|
-
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
140
|
-
const hs = /** @type {Record<string, unknown>} */ (raw.health_score);
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
**Private class field annotations:**
|
|
144
|
-
|
|
145
|
-
```javascript
|
|
146
|
-
/** @type {ManagedAccount[]} */
|
|
147
|
-
#accounts = [];
|
|
148
|
-
/** @type {Map<string, StatsDelta>} */
|
|
149
|
-
#statsDeltas = new Map();
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Error Handling
|
|
153
|
-
|
|
154
|
-
**Pattern 1: Silent catch for non-critical operations**
|
|
155
|
-
Used when failure should not disrupt the main flow:
|
|
156
|
-
|
|
157
|
-
```javascript
|
|
158
|
-
try {
|
|
159
|
-
await fs.unlink(tempPath);
|
|
160
|
-
} catch {
|
|
161
|
-
// Ignore cleanup errors
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Pattern 2: Error code discrimination for fs operations**
|
|
166
|
-
|
|
167
|
-
```javascript
|
|
168
|
-
try {
|
|
169
|
-
const content = await fs.readFile(storagePath, "utf-8");
|
|
170
|
-
// ...process
|
|
171
|
-
} catch (error) {
|
|
172
|
-
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
173
|
-
if (code === "ENOENT") return null;
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
**Pattern 3: Graceful degradation — return defaults on failure**
|
|
179
|
-
Config loading, account loading, etc. return defaults or null rather than throwing:
|
|
180
|
-
|
|
181
|
-
```javascript
|
|
182
|
-
export function loadConfig() {
|
|
183
|
-
try {
|
|
184
|
-
// ...try to read and parse
|
|
185
|
-
} catch {
|
|
186
|
-
return { ...DEFAULT_CONFIG };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Pattern 4: Propagate errors for critical paths**
|
|
192
|
-
Storage save errors are thrown after cleanup, so callers know the save failed:
|
|
193
|
-
|
|
194
|
-
```javascript
|
|
195
|
-
try {
|
|
196
|
-
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
197
|
-
await fs.rename(tempPath, storagePath);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
try {
|
|
200
|
-
await fs.unlink(tempPath);
|
|
201
|
-
} catch {
|
|
202
|
-
/* ignore */
|
|
203
|
-
}
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**General rule:** Never crash the plugin. If something fails, log/ignore and continue. The plugin is middleware for a CLI tool — availability beats correctness for non-critical paths.
|
|
209
|
-
|
|
210
|
-
## File I/O Patterns
|
|
211
|
-
|
|
212
|
-
**Atomic writes (temp file + rename):**
|
|
213
|
-
All data persistence uses this pattern to prevent corruption on crash:
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
const tempPath = `${storagePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
217
|
-
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
218
|
-
await fs.rename(tempPath, storagePath);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
See `lib/storage.mjs` `saveAccounts()`.
|
|
222
|
-
|
|
223
|
-
**File permissions:**
|
|
224
|
-
Sensitive files (accounts with tokens) are written with `mode: 0o600` (owner read/write only).
|
|
225
|
-
|
|
226
|
-
**Debounced persistence:**
|
|
227
|
-
`AccountManager` in `lib/accounts.mjs` uses a 1-second debounce (`#saveTimeout`) to coalesce rapid state changes into a single disk write.
|
|
228
|
-
|
|
229
|
-
**Merge-on-save:**
|
|
230
|
-
Before writing, `AccountManager._persistNow()` re-reads the current disk state and merges in-memory deltas. This supports multiple CLI/plugin instances running concurrently without data loss.
|
|
231
|
-
|
|
232
|
-
## Code Organization
|
|
233
|
-
|
|
234
|
-
**Section comments:**
|
|
235
|
-
Major logical sections are separated with comment blocks:
|
|
236
|
-
|
|
237
|
-
```javascript
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
// Color helpers — zero dependencies, respects NO_COLOR / TTY
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**Module structure pattern:**
|
|
244
|
-
|
|
245
|
-
1. Imports
|
|
246
|
-
2. JSDoc `@typedef` declarations
|
|
247
|
-
3. Constants (`const UPPER_SNAKE = ...`)
|
|
248
|
-
4. Internal/private helper functions
|
|
249
|
-
5. Exported functions/classes
|
|
250
|
-
6. Main entry point (in entry files like `cli.mjs`)
|
|
251
|
-
|
|
252
|
-
**Export pattern:**
|
|
253
|
-
|
|
254
|
-
- Named exports only: `export function ...`, `export class ...`, `export const ...`
|
|
255
|
-
- No barrel files — each module exports its own public API
|
|
256
|
-
- No default exports
|
|
257
|
-
|
|
258
|
-
## CLI Patterns
|
|
259
|
-
|
|
260
|
-
**Command functions return exit codes:**
|
|
261
|
-
|
|
262
|
-
```javascript
|
|
263
|
-
async function cmdList(storage) {
|
|
264
|
-
// ...
|
|
265
|
-
return 0; // success
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**Color helpers respect `NO_COLOR` and TTY:**
|
|
270
|
-
|
|
271
|
-
```javascript
|
|
272
|
-
let USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
273
|
-
const ansi = (code, text) => (USE_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
274
|
-
const c = {
|
|
275
|
-
bold: (t) => ansi("1", t),
|
|
276
|
-
dim: (t) => ansi("2", t),
|
|
277
|
-
green: (t) => ansi("32", t),
|
|
278
|
-
// ...
|
|
279
|
-
};
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**Interactive prompts:**
|
|
283
|
-
Use `node:readline/promises` with `createInterface({ input: stdin, output: stdout })`.
|
|
284
|
-
|
|
285
|
-
## Validation Patterns
|
|
286
|
-
|
|
287
|
-
**Config validation with clamping:**
|
|
288
|
-
Invalid or out-of-range values are clamped to valid ranges, never rejected:
|
|
289
|
-
|
|
290
|
-
```javascript
|
|
291
|
-
function clampNumber(value, min, max, fallback) {
|
|
292
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
293
|
-
return Math.max(min, Math.min(max, value));
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
See `lib/config.mjs` `validateConfig()`.
|
|
298
|
-
|
|
299
|
-
**Account validation with field-level sanitization:**
|
|
300
|
-
Each field is individually validated and defaulted. Invalid accounts (no refreshToken) return null and are filtered out:
|
|
301
|
-
|
|
302
|
-
```javascript
|
|
303
|
-
if (typeof acc.refreshToken !== "string" || !acc.refreshToken.trim()) return null;
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
See `lib/storage.mjs` `validateAccount()`.
|
|
307
|
-
|
|
308
|
-
**Deduplication:**
|
|
309
|
-
Accounts are deduplicated by `refreshToken` — the one with the latest `lastUsed` timestamp wins. See `lib/storage.mjs` `deduplicateByRefreshToken()`.
|
|
310
|
-
|
|
311
|
-
## Null Handling
|
|
312
|
-
|
|
313
|
-
**Use `== null` for null/undefined checks** (enabled by `eqeqeq: smart`):
|
|
314
|
-
|
|
315
|
-
```javascript
|
|
316
|
-
if (body == null) {
|
|
317
|
-
return { errorType, message, text };
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
**Nullable fields use `| null` in JSDoc, not `undefined`:**
|
|
322
|
-
|
|
323
|
-
```javascript
|
|
324
|
-
/** @property {number | null} lastFailureTime */
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
**Optional fields use `[bracket]` notation in JSDoc:**
|
|
328
|
-
|
|
329
|
-
```javascript
|
|
330
|
-
/** @property {string} [email] */
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
_Convention analysis: 2026-02-07_
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
# External Integrations
|
|
2
|
-
|
|
3
|
-
**Analysis Date:** 2026-02-07
|
|
4
|
-
|
|
5
|
-
## APIs & External Services
|
|
6
|
-
|
|
7
|
-
**Anthropic OAuth (Primary Integration):**
|
|
8
|
-
|
|
9
|
-
- **Purpose:** OAuth 2.0 authentication for Claude Pro/Max subscriptions and API key creation
|
|
10
|
-
- **SDK/Client:** Native `fetch()` — no SDK wrapper
|
|
11
|
-
- **Auth:** OAuth 2.0 with PKCE (Authorization Code flow)
|
|
12
|
-
- **Client ID:** `9d1c250a-e61b-44d9-88ed-5944d1962f5e` (hardcoded in `lib/config.mjs:71`)
|
|
13
|
-
- **PKCE Library:** `@openauthjs/openauth/pkce` — `generatePKCE()` function
|
|
14
|
-
|
|
15
|
-
**Anthropic OAuth Endpoints (used in `index.mjs` and `cli.mjs`):**
|
|
16
|
-
|
|
17
|
-
| Endpoint | Method | Purpose | Used In |
|
|
18
|
-
| --------------------------------------------------------------- | ------------- | ------------------------------------- | ---------------------------------------------- |
|
|
19
|
-
| `https://claude.ai/oauth/authorize` | GET (browser) | OAuth authorization (Max plan) | `index.mjs:21` |
|
|
20
|
-
| `https://console.anthropic.com/oauth/authorize` | GET (browser) | OAuth authorization (Console/API key) | `index.mjs:21` |
|
|
21
|
-
| `https://console.anthropic.com/v1/oauth/token` | POST | Token exchange & refresh | `index.mjs:42`, `index.mjs:536`, `cli.mjs:140` |
|
|
22
|
-
| `https://api.anthropic.com/api/oauth/claude_cli/create_api_key` | POST | Create API key from OAuth token | `index.mjs:1028` |
|
|
23
|
-
| `https://api.anthropic.com/api/oauth/usage` | GET | Fetch per-account usage quotas | `cli.mjs:168` |
|
|
24
|
-
|
|
25
|
-
**Anthropic Messages API (Proxied):**
|
|
26
|
-
|
|
27
|
-
- **Purpose:** The plugin intercepts and transforms requests to the Anthropic Messages API
|
|
28
|
-
- **Endpoint:** `/v1/messages` — detected via URL pathname matching
|
|
29
|
-
- **Transforms applied:**
|
|
30
|
-
- Adds `?beta=true` query parameter (`index.mjs:296`)
|
|
31
|
-
- Sets `Authorization: Bearer {access_token}` header
|
|
32
|
-
- Adds `anthropic-beta: oauth-2025-04-20,interleaved-thinking-2025-05-14` header
|
|
33
|
-
- Sets `user-agent: claude-cli/2.1.2 (external, cli)`
|
|
34
|
-
- Removes `x-api-key` header
|
|
35
|
-
- Prefixes tool names with `mcp_` in request body
|
|
36
|
-
- Strips `mcp_` prefix from tool names in response stream
|
|
37
|
-
- Rewrites `OpenCode` → `Claude Code` and `opencode` → `Claude` in system prompts
|
|
38
|
-
|
|
39
|
-
**OAuth Flow Parameters:**
|
|
40
|
-
|
|
41
|
-
- Redirect URI: `https://console.anthropic.com/oauth/code/callback`
|
|
42
|
-
- Scopes: `org:create_api_key user:profile user:inference`
|
|
43
|
-
- PKCE: S256 challenge method
|
|
44
|
-
- Grant types: `authorization_code` (initial), `refresh_token` (renewal)
|
|
45
|
-
|
|
46
|
-
## Data Storage
|
|
47
|
-
|
|
48
|
-
**Databases:**
|
|
49
|
-
|
|
50
|
-
- None — All storage is local filesystem JSON files
|
|
51
|
-
|
|
52
|
-
**File Storage (Local):**
|
|
53
|
-
|
|
54
|
-
- `~/.config/opencode/anthropic-accounts.json` — Multi-account OAuth token storage
|
|
55
|
-
- Written by: `lib/storage.mjs` (`saveAccounts()`)
|
|
56
|
-
- Read by: `lib/storage.mjs` (`loadAccounts()`)
|
|
57
|
-
- Format: JSON with version, accounts array, activeIndex
|
|
58
|
-
- Security: File mode `0o600` (owner read/write only)
|
|
59
|
-
- Write strategy: Atomic (temp file + rename via `randomBytes(6)` suffix)
|
|
60
|
-
- Auto-deduplication by refresh token on load
|
|
61
|
-
- Auto `.gitignore` maintained in config directory
|
|
62
|
-
|
|
63
|
-
- `~/.config/opencode/anthropic-auth.json` — Plugin configuration
|
|
64
|
-
- Written by: `lib/config.mjs` (`saveConfig()`)
|
|
65
|
-
- Read by: `lib/config.mjs` (`loadConfig()`)
|
|
66
|
-
- Format: JSON with strategy, health scores, token bucket settings
|
|
67
|
-
- Security: File mode `0o600`
|
|
68
|
-
- Write strategy: Atomic (temp file + rename via PID suffix)
|
|
69
|
-
|
|
70
|
-
**Caching:**
|
|
71
|
-
|
|
72
|
-
- In-memory only — Health scores (`HealthScoreTracker`), token buckets (`TokenBucketTracker`), and stats deltas (`Map<string, StatsDelta>`) are held in memory during the process lifetime
|
|
73
|
-
- No external cache service
|
|
74
|
-
|
|
75
|
-
## Authentication & Identity
|
|
76
|
-
|
|
77
|
-
**Auth Provider:** Anthropic OAuth 2.0 (custom implementation)
|
|
78
|
-
|
|
79
|
-
**Implementation:**
|
|
80
|
-
|
|
81
|
-
- PKCE Authorization Code flow for initial login
|
|
82
|
-
- Refresh token rotation for session maintenance
|
|
83
|
-
- Multi-account support (up to 10 accounts, `MAX_ACCOUNTS` in `lib/accounts.mjs:35`)
|
|
84
|
-
- Single-flight refresh protection (prevents concurrent refresh races in `index.mjs:659`)
|
|
85
|
-
- Automatic account switching on auth failure, rate limit, or quota exhaustion
|
|
86
|
-
|
|
87
|
-
**Token Lifecycle:**
|
|
88
|
-
|
|
89
|
-
1. User visits authorization URL in browser
|
|
90
|
-
2. User pastes authorization code back to CLI
|
|
91
|
-
3. Plugin exchanges code for access + refresh tokens
|
|
92
|
-
4. Access token used for API requests (short-lived)
|
|
93
|
-
5. Refresh token used to obtain new access tokens when expired
|
|
94
|
-
6. Plugin persists tokens to `anthropic-accounts.json`
|
|
95
|
-
7. Also syncs primary account to OpenCode's `auth.json` for compatibility
|
|
96
|
-
|
|
97
|
-
**Account Selection Strategies (configurable in `lib/rotation.mjs`):**
|
|
98
|
-
|
|
99
|
-
- `sticky` — Stay on current account until it fails (default)
|
|
100
|
-
- `round-robin` — Rotate through accounts on every request
|
|
101
|
-
- `hybrid` — Score-based selection using health scores + token buckets + freshness
|
|
102
|
-
|
|
103
|
-
## OpenCode Plugin SDK Integration
|
|
104
|
-
|
|
105
|
-
**SDK:** `@opencode-ai/plugin` ^0.4.45 (via `@opencode-ai/sdk` 0.4.19)
|
|
106
|
-
|
|
107
|
-
**Plugin hooks used:**
|
|
108
|
-
|
|
109
|
-
- `auth.loader(getAuth, provider)` — Custom auth loader that intercepts fetch requests
|
|
110
|
-
- `auth.methods[]` — Three auth methods (OAuth Max, OAuth Console/API Key, Manual API Key)
|
|
111
|
-
- `experimental.chat.system.transform` — Prepends Claude Code identity to system prompts
|
|
112
|
-
|
|
113
|
-
**SDK Client API used:**
|
|
114
|
-
|
|
115
|
-
- `client.auth.set()` — Persist OAuth tokens to OpenCode's auth store (`index.mjs:580`)
|
|
116
|
-
- `client.tui?.showToast()` — Display notifications in OpenCode TUI (`index.mjs:638`)
|
|
117
|
-
|
|
118
|
-
## Monitoring & Observability
|
|
119
|
-
|
|
120
|
-
**Error Tracking:**
|
|
121
|
-
|
|
122
|
-
- None — No external error tracking service
|
|
123
|
-
|
|
124
|
-
**Logs:**
|
|
125
|
-
|
|
126
|
-
- Debug logging to `stderr` via `console.error()` when `config.debug` is enabled
|
|
127
|
-
- Prefix: `[opencode-anthropic-auth]`
|
|
128
|
-
- Controlled by: `OPENCODE_ANTHROPIC_DEBUG=1` env var or `debug: true` in config file
|
|
129
|
-
|
|
130
|
-
**TUI Toasts:**
|
|
131
|
-
|
|
132
|
-
- Account usage notifications (which account is active)
|
|
133
|
-
- Account switch notifications (when switching due to rate limit/failure)
|
|
134
|
-
- Error toasts (disabled accounts, auth failures)
|
|
135
|
-
- Configurable: `toasts.quiet` suppresses non-error toasts, `toasts.debounce_seconds` rate-limits toast frequency
|
|
136
|
-
|
|
137
|
-
## CI/CD & Deployment
|
|
138
|
-
|
|
139
|
-
**Hosting:**
|
|
140
|
-
|
|
141
|
-
- npm registry (`opencode-anthropic-auth` package, currently v0.0.13)
|
|
142
|
-
- Self-installed to user machine (not a hosted service)
|
|
143
|
-
|
|
144
|
-
**CI Pipeline:**
|
|
145
|
-
|
|
146
|
-
- GitHub Actions: `.github/workflows/publish.yml`
|
|
147
|
-
- Trigger: Manual (`workflow_dispatch`) with optional version bump type
|
|
148
|
-
- Steps: Checkout → Node.js 24 → `npm install` → `npm publish --access public`
|
|
149
|
-
- Auth: `NPM_TOKEN` secret
|
|
150
|
-
|
|
151
|
-
**Publishing Flow:**
|
|
152
|
-
|
|
153
|
-
1. Run `bun script/publish.ts [patch|minor|major]` locally
|
|
154
|
-
2. Bumps version in `package.json`, commits, pushes
|
|
155
|
-
3. Triggers GitHub Actions workflow via `gh workflow run`
|
|
156
|
-
4. CI publishes to npm
|
|
157
|
-
|
|
158
|
-
## Environment Configuration
|
|
159
|
-
|
|
160
|
-
**Required env vars:**
|
|
161
|
-
|
|
162
|
-
- None required for runtime (config file is optional, defaults are sane)
|
|
163
|
-
|
|
164
|
-
**Optional env vars (runtime):**
|
|
165
|
-
|
|
166
|
-
- `OPENCODE_ANTHROPIC_STRATEGY` — Override account selection strategy
|
|
167
|
-
- `OPENCODE_ANTHROPIC_DEBUG` — Toggle debug logging
|
|
168
|
-
- `OPENCODE_ANTHROPIC_QUIET` — Suppress non-error toasts
|
|
169
|
-
- `XDG_CONFIG_HOME` — Override config/storage directory base
|
|
170
|
-
- `NO_COLOR` — Disable ANSI colors in CLI output
|
|
171
|
-
|
|
172
|
-
**CI/CD env vars:**
|
|
173
|
-
|
|
174
|
-
- `NPM_TOKEN` — npm authentication for publishing (GitHub Actions secret)
|
|
175
|
-
- `NODE_AUTH_TOKEN` — Mapped from `NPM_TOKEN` in workflow
|
|
176
|
-
|
|
177
|
-
**Secrets location:**
|
|
178
|
-
|
|
179
|
-
- OAuth tokens stored in `~/.config/opencode/anthropic-accounts.json` (file mode 0o600)
|
|
180
|
-
- `.gitignore` auto-maintained in config directory to prevent accidental commits
|
|
181
|
-
- No secrets in repository
|
|
182
|
-
|
|
183
|
-
## Webhooks & Callbacks
|
|
184
|
-
|
|
185
|
-
**Incoming:**
|
|
186
|
-
|
|
187
|
-
- OAuth callback URL: `https://console.anthropic.com/oauth/code/callback` (handled by Anthropic's servers; user pastes the returned code manually)
|
|
188
|
-
|
|
189
|
-
**Outgoing:**
|
|
190
|
-
|
|
191
|
-
- None
|
|
192
|
-
|
|
193
|
-
## SSE Stream Processing
|
|
194
|
-
|
|
195
|
-
**The plugin processes Server-Sent Events (SSE) streams from the Anthropic API:**
|
|
196
|
-
|
|
197
|
-
- Strips `mcp_` prefix from tool names in response stream (`index.mjs:489`)
|
|
198
|
-
- Extracts token usage from `message_start` and `message_delta` events (`index.mjs:317-341`)
|
|
199
|
-
- Detects mid-stream account errors for proactive switching (`index.mjs:368-392`)
|
|
200
|
-
- Implemented via `ReadableStream` wrapping of response body (`index.mjs:465-492`)
|
|
201
|
-
|
|
202
|
-
## Rate Limit & Backoff System
|
|
203
|
-
|
|
204
|
-
**The plugin implements sophisticated rate limit handling across accounts:**
|
|
205
|
-
|
|
206
|
-
- Categorizes errors: `AUTH_FAILED`, `QUOTA_EXHAUSTED`, `RATE_LIMIT_EXCEEDED` (`lib/backoff.mjs`)
|
|
207
|
-
- Respects `Retry-After` headers (both seconds and HTTP-date formats)
|
|
208
|
-
- Escalating backoffs for quota exhaustion: 1m → 5m → 30m → 2h
|
|
209
|
-
- Fixed backoffs: Auth failure 5s, Rate limit 30s
|
|
210
|
-
- Health scoring with passive recovery (configurable in `lib/rotation.mjs`)
|
|
211
|
-
- Token bucket for request gating (`lib/rotation.mjs`)
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
_Integration audit: 2026-02-07_
|