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.
@@ -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_