opencode-anthropic-fix 0.1.32 → 0.2.1

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.
Files changed (42) hide show
  1. package/README.md +9 -6
  2. package/cli.mjs +43 -35
  3. package/index.mjs +377 -3646
  4. package/lib/backoff.mjs +1 -1
  5. package/lib/cc-credentials.mjs +1 -1
  6. package/lib/config.mjs +2 -2
  7. package/lib/config.test.mjs +305 -1
  8. package/lib/diagnose.mjs +184 -0
  9. package/lib/diagnose.test.mjs +122 -0
  10. package/lib/env.mjs +19 -0
  11. package/lib/env.test.mjs +25 -0
  12. package/lib/mimicry/cache.mjs +62 -0
  13. package/lib/mimicry/cache.test.mjs +112 -0
  14. package/lib/mimicry/headers.mjs +528 -0
  15. package/lib/mimicry/headers.test.mjs +50 -0
  16. package/lib/mimicry/models.mjs +146 -0
  17. package/lib/mimicry/models.test.mjs +132 -0
  18. package/lib/mimicry/request-body.mjs +682 -0
  19. package/lib/mimicry/request-body.test.mjs +34 -0
  20. package/lib/mimicry/request-helpers.mjs +243 -0
  21. package/lib/mimicry/request-helpers.test.mjs +155 -0
  22. package/lib/mimicry/response-stream.mjs +429 -0
  23. package/lib/mimicry/response-stream.test.mjs +127 -0
  24. package/lib/mimicry/system-prompt.mjs +638 -0
  25. package/lib/mimicry/system-prompt.test.mjs +73 -0
  26. package/lib/redact.mjs +147 -0
  27. package/lib/redact.test.mjs +154 -0
  28. package/lib/refresh-lock.test.mjs +52 -0
  29. package/lib/retry/overload-loop.mjs +83 -0
  30. package/lib/retry/overload-loop.test.mjs +107 -0
  31. package/lib/rotation.mjs +16 -0
  32. package/lib/rotation.test.mjs +46 -0
  33. package/lib/session-metrics.mjs +75 -0
  34. package/lib/session-metrics.test.mjs +67 -0
  35. package/lib/storage.test.mjs +82 -0
  36. package/lib/token-economy/microcompact.mjs +35 -0
  37. package/lib/token-economy/microcompact.test.mjs +28 -0
  38. package/lib/token-economy/transforms.mjs +637 -0
  39. package/lib/token-economy/transforms.test.mjs +192 -0
  40. package/lib/tuning.mjs +70 -0
  41. package/lib/tuning.test.mjs +89 -0
  42. package/package.json +4 -1
package/README.md CHANGED
@@ -365,11 +365,12 @@ Pending slash OAuth flows expire after 10 minutes. If completion fails with an e
365
365
 
366
366
  Control how the plugin picks which account to use for each request.
367
367
 
368
- | Strategy | Behavior | Best For |
369
- | ---------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
370
- | **`sticky`** (default) | Stay on one account until it fails or is rate-limited | Single account, predictable behavior, full feature compatibility |
371
- | **`round-robin`** | Rotate through accounts on every request | Spreading load evenly across accounts (see [limitations](#round-robin-limitations)) |
372
- | **`hybrid`** | Score-based selection with stickiness bias. Considers health, token budget, and freshness | Multiple accounts with varying rate limits |
368
+ | Strategy | Behavior | Best For |
369
+ | ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
370
+ | **`sticky`** (default) | Stay on one account until it fails or is rate-limited | Single account, predictable behavior, full feature compatibility |
371
+ | **`round-robin`** | Rotate through accounts on every request | Spreading load evenly across accounts (see [limitations](#round-robin-limitations)) |
372
+ | **`hybrid`** | Score-based selection with stickiness bias. Considers health, token budget, and freshness | Multiple accounts with varying rate limits |
373
+ | **`single`** | Use one account only; stop and alert when it fails or is rate-limited (never auto-switches) | Simple multi-account setups (e.g. work vs personal) where requests must never mix accounts |
373
374
 
374
375
  ### Change Strategy
375
376
 
@@ -413,7 +414,7 @@ Configuration is stored at `~/.config/opencode/anthropic-auth.json`. All setting
413
414
 
414
415
  ```jsonc
415
416
  {
416
- // Account selection strategy: "sticky" | "round-robin" | "hybrid"
417
+ // Account selection strategy: "sticky" | "round-robin" | "hybrid" | "single"
417
418
  "account_selection_strategy": "sticky",
418
419
 
419
420
  // Seconds before consecutive failure count resets (60-7200)
@@ -597,6 +598,8 @@ Account credentials are stored with restrictive file permissions (owner read/wri
597
598
 
598
599
  ## Troubleshooting
599
600
 
601
+ Run `opencode-anthropic-auth diagnose` to write a redacted, shareable diagnostic bundle, or add `--stdout` to print it.
602
+
600
603
  ### "Provider not showing up"
601
604
 
602
605
  Make sure the plugin is installed in `~/.config/opencode/plugin/`. Restart OpenCode after installing.
package/cli.mjs CHANGED
@@ -29,13 +29,17 @@
29
29
  */
30
30
 
31
31
  import { loadAccounts, saveAccounts, getStoragePath, createDefaultStats } from "./lib/storage.mjs";
32
- import { loadConfig, saveConfig, getConfigPath, VALID_STRATEGIES, CLIENT_ID } from "./lib/config.mjs";
33
- import { authorize, exchange, revoke } from "./lib/oauth.mjs";
32
+ import { loadConfig, saveConfig, getConfigPath, VALID_STRATEGIES } from "./lib/config.mjs";
33
+ import { authorize, exchange, refreshToken, revoke } from "./lib/oauth.mjs";
34
+ import { adjustActiveIndexAfterRemoval } from "./lib/account-state.mjs";
34
35
  import { AsyncLocalStorage } from "node:async_hooks";
36
+ import { writeFile } from "node:fs/promises";
37
+ import path from "node:path";
35
38
  import { pathToFileURL } from "node:url";
36
39
  import { exec } from "node:child_process";
37
40
  import { createInterface } from "node:readline/promises";
38
41
  import { stdin, stdout } from "node:process";
42
+ import { buildDiagnosticBundle } from "./lib/diagnose.mjs";
39
43
 
40
44
  // ---------------------------------------------------------------------------
41
45
  // Color helpers — zero dependencies, respects NO_COLOR / TTY
@@ -146,18 +150,7 @@ function rpad(str, width) {
146
150
  */
147
151
  export async function refreshAccessToken(account) {
148
152
  try {
149
- const resp = await fetch("https://platform.claude.com/v1/oauth/token", {
150
- method: "POST",
151
- headers: { "Content-Type": "application/json" },
152
- body: JSON.stringify({
153
- grant_type: "refresh_token",
154
- refresh_token: account.refreshToken,
155
- client_id: CLIENT_ID,
156
- }),
157
- signal: AbortSignal.timeout(5000),
158
- });
159
- if (!resp.ok) return null;
160
- const json = await resp.json();
153
+ const json = await refreshToken(account.refreshToken, { signal: AbortSignal.timeout(5000) });
161
154
  account.access = json.access_token;
162
155
  account.expires = Date.now() + json.expires_in * 1000;
163
156
  if (json.refresh_token) account.refreshToken = json.refresh_token;
@@ -483,13 +476,7 @@ export async function cmdLogout(arg, opts = {}) {
483
476
  stored.accounts.splice(idx, 1);
484
477
 
485
478
  // Adjust active index
486
- if (stored.accounts.length === 0) {
487
- stored.activeIndex = 0;
488
- } else if (stored.activeIndex >= stored.accounts.length) {
489
- stored.activeIndex = stored.accounts.length - 1;
490
- } else if (stored.activeIndex > idx) {
491
- stored.activeIndex--;
492
- }
479
+ adjustActiveIndexAfterRemoval(stored, idx);
493
480
 
494
481
  await saveAccounts(stored);
495
482
  console.log(c.green(`Logged out account #${n} (${label}).`));
@@ -1019,13 +1006,7 @@ export async function cmdRemove(arg, opts = {}) {
1019
1006
  stored.accounts.splice(idx, 1);
1020
1007
 
1021
1008
  // Adjust active index
1022
- if (stored.accounts.length === 0) {
1023
- stored.activeIndex = 0;
1024
- } else if (stored.activeIndex >= stored.accounts.length) {
1025
- stored.activeIndex = stored.accounts.length - 1;
1026
- } else if (stored.activeIndex > idx) {
1027
- stored.activeIndex--;
1028
- }
1009
+ adjustActiveIndexAfterRemoval(stored, idx);
1029
1010
 
1030
1011
  await saveAccounts(stored);
1031
1012
  console.log(c.green(`Removed account #${n} (${label}).`));
@@ -1172,6 +1153,7 @@ export async function cmdStrategy(arg) {
1172
1153
  sticky: "Stay on one account until it fails or is rate-limited",
1173
1154
  "round-robin": "Rotate through accounts on every request",
1174
1155
  hybrid: "Prefer healthy accounts, rotate when degraded",
1156
+ single: "Use one account only; stop and alert when it fails or is rate-limited",
1175
1157
  };
1176
1158
 
1177
1159
  for (const s of VALID_STRATEGIES) {
@@ -1514,13 +1496,7 @@ export async function cmdManage() {
1514
1496
  if (confirm.trim().toLowerCase() === "y") {
1515
1497
  stored.accounts.splice(idx, 1);
1516
1498
  // Adjust active index
1517
- if (stored.accounts.length === 0) {
1518
- stored.activeIndex = 0;
1519
- } else if (stored.activeIndex >= stored.accounts.length) {
1520
- stored.activeIndex = stored.accounts.length - 1;
1521
- } else if (stored.activeIndex > idx) {
1522
- stored.activeIndex--;
1523
- }
1499
+ adjustActiveIndexAfterRemoval(stored, idx);
1524
1500
  await saveAccounts(stored);
1525
1501
  console.log(c.green(`Removed account #${num}.`));
1526
1502
  } else {
@@ -1557,6 +1533,34 @@ export async function cmdManage() {
1557
1533
  return 0;
1558
1534
  }
1559
1535
 
1536
+ /**
1537
+ * Write or print a redacted diagnostic bundle.
1538
+ *
1539
+ * @param {string | undefined} _arg
1540
+ * @param {{ stdout?: boolean }} [opts]
1541
+ * @returns {Promise<number>} exit code
1542
+ */
1543
+ export async function cmdDiagnose(_arg, opts = {}) {
1544
+ try {
1545
+ const bundle = await buildDiagnosticBundle();
1546
+ const json = JSON.stringify(bundle, null, 2);
1547
+ if (opts.stdout) {
1548
+ console.log(json);
1549
+ return 0;
1550
+ }
1551
+
1552
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1553
+ const outputPath = path.resolve(process.cwd(), `opencode-anthropic-diagnose-${timestamp}.json`);
1554
+ await writeFile(outputPath, `${json}\n`, "utf8");
1555
+ console.log(outputPath);
1556
+ return 0;
1557
+ } catch (error) {
1558
+ const message = error instanceof Error ? error.message : String(error);
1559
+ console.error(c.red(`Failed to create diagnostic bundle: ${message}`));
1560
+ return 1;
1561
+ }
1562
+ }
1563
+
1560
1564
  /**
1561
1565
  * Show help text.
1562
1566
  */
@@ -1588,6 +1592,7 @@ ${c.dim("Account Commands:")}
1588
1592
  ${pad(c.cyan("strategy") + " [name]", 22)}Show or change selection strategy
1589
1593
  ${pad(c.cyan("config"), 22)}Show configuration and file paths
1590
1594
  ${pad(c.cyan("manage"), 22)}Interactive account management menu
1595
+ ${pad(c.cyan("diagnose"), 22)}Write a redacted diagnostic bundle ${c.dim("(dg; --stdout to print)")}
1591
1596
  ${pad(c.cyan("help"), 22)}Show this help message
1592
1597
 
1593
1598
  ${c.dim("Options:")}
@@ -1732,6 +1737,9 @@ async function dispatch(argv) {
1732
1737
  case "manage":
1733
1738
  case "mg":
1734
1739
  return cmdManage();
1740
+ case "diagnose":
1741
+ case "dg":
1742
+ return cmdDiagnose(arg, { stdout: flags.includes("--stdout") });
1735
1743
  case "help":
1736
1744
  case "-h":
1737
1745
  case "--help":