kimaki 0.4.89 → 0.4.91

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 (146) hide show
  1. package/dist/agent-model.e2e.test.js +82 -3
  2. package/dist/anthropic-auth-plugin.js +246 -195
  3. package/dist/anthropic-auth-plugin.test.js +125 -0
  4. package/dist/anthropic-auth-state.js +231 -0
  5. package/dist/bin.js +6 -3
  6. package/dist/cli-parsing.test.js +23 -0
  7. package/dist/cli-send-thread.e2e.test.js +4 -3
  8. package/dist/cli.js +76 -46
  9. package/dist/commands/btw.js +7 -2
  10. package/dist/commands/merge-worktree.js +6 -3
  11. package/dist/commands/new-worktree.js +18 -7
  12. package/dist/commands/worktrees.js +71 -7
  13. package/dist/context-awareness-plugin.js +52 -50
  14. package/dist/context-awareness-plugin.test.js +68 -1
  15. package/dist/discord-bot.js +131 -58
  16. package/dist/discord-utils.test.js +19 -0
  17. package/dist/errors.js +0 -5
  18. package/dist/event-stream-real-capture.e2e.test.js +2 -1
  19. package/dist/exec-async.js +26 -0
  20. package/dist/external-opencode-sync.js +33 -72
  21. package/dist/forum-sync/config.js +2 -2
  22. package/dist/forum-sync/markdown.js +4 -8
  23. package/dist/gateway-proxy.e2e.test.js +2 -1
  24. package/dist/hrana-server.js +11 -3
  25. package/dist/image-optimizer-plugin.js +153 -0
  26. package/dist/ipc-tools-plugin.js +11 -4
  27. package/dist/kimaki-digital-twin.e2e.test.js +2 -1
  28. package/dist/kimaki-opencode-plugin.js +1 -0
  29. package/dist/logger.js +0 -1
  30. package/dist/markdown.js +2 -2
  31. package/dist/markdown.test.js +5 -6
  32. package/dist/message-finish-field.e2e.test.js +2 -1
  33. package/dist/message-preprocessing.js +100 -16
  34. package/dist/onboarding-tutorial.js +1 -1
  35. package/dist/opencode-command-detection.js +70 -0
  36. package/dist/opencode-command-detection.test.js +210 -0
  37. package/dist/opencode-interrupt-plugin.js +64 -8
  38. package/dist/opencode-interrupt-plugin.test.js +23 -39
  39. package/dist/opencode.js +34 -32
  40. package/dist/pkce.js +23 -0
  41. package/dist/plugin-logger.js +59 -0
  42. package/dist/queue-advanced-abort.e2e.test.js +14 -15
  43. package/dist/queue-advanced-e2e-setup.js +2 -0
  44. package/dist/queue-advanced-permissions-typing.e2e.test.js +26 -24
  45. package/dist/queue-advanced-question.e2e.test.js +149 -82
  46. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -5
  47. package/dist/queue-question-select-drain.e2e.test.js +30 -27
  48. package/dist/runtime-lifecycle.e2e.test.js +2 -1
  49. package/dist/sentry.js +7 -114
  50. package/dist/session-handler/event-stream-state.js +1 -1
  51. package/dist/session-handler/thread-runtime-state.js +9 -0
  52. package/dist/session-handler/thread-session-runtime.js +210 -53
  53. package/dist/session-title-rename.test.js +80 -0
  54. package/dist/startup-time.e2e.test.js +2 -1
  55. package/dist/store.js +1 -2
  56. package/dist/system-message.js +105 -49
  57. package/dist/system-message.test.js +598 -15
  58. package/dist/task-runner.js +7 -4
  59. package/dist/task-schedule.js +2 -0
  60. package/dist/test-utils.js +20 -0
  61. package/dist/thread-message-queue.e2e.test.js +34 -41
  62. package/dist/unnest-code-blocks.js +11 -1
  63. package/dist/unnest-code-blocks.test.js +32 -0
  64. package/dist/voice-handler.js +15 -5
  65. package/dist/voice-message.e2e.test.js +2 -1
  66. package/dist/voice.js +53 -23
  67. package/dist/voice.test.js +2 -0
  68. package/dist/worktree-lifecycle.e2e.test.js +1 -1
  69. package/dist/worktrees.js +111 -120
  70. package/dist/worktrees.test.js +2 -2
  71. package/package.json +18 -22
  72. package/skills/lintcn/SKILL.md +6 -1
  73. package/skills/new-skill/SKILL.md +211 -0
  74. package/skills/npm-package/SKILL.md +3 -2
  75. package/skills/spiceflow/SKILL.md +1 -1
  76. package/skills/usecomputer/SKILL.md +174 -249
  77. package/src/agent-model.e2e.test.ts +97 -2
  78. package/src/anthropic-auth-plugin.test.ts +159 -0
  79. package/src/anthropic-auth-plugin.ts +474 -403
  80. package/src/anthropic-auth-state.ts +282 -0
  81. package/src/bin.ts +6 -3
  82. package/src/cli-parsing.test.ts +32 -0
  83. package/src/cli-send-thread.e2e.test.ts +4 -2
  84. package/src/cli.ts +101 -63
  85. package/src/commands/btw.ts +8 -2
  86. package/src/commands/merge-worktree.ts +8 -3
  87. package/src/commands/new-worktree.ts +22 -10
  88. package/src/commands/worktrees.ts +86 -5
  89. package/src/context-awareness-plugin.test.ts +77 -1
  90. package/src/context-awareness-plugin.ts +85 -64
  91. package/src/discord-bot.ts +142 -60
  92. package/src/discord-utils.test.ts +21 -0
  93. package/src/errors.ts +0 -6
  94. package/src/event-stream-real-capture.e2e.test.ts +2 -1
  95. package/src/exec-async.ts +35 -0
  96. package/src/external-opencode-sync.ts +39 -85
  97. package/src/forum-sync/config.ts +2 -2
  98. package/src/forum-sync/markdown.ts +5 -9
  99. package/src/gateway-proxy.e2e.test.ts +2 -0
  100. package/src/hrana-server.ts +15 -3
  101. package/src/image-optimizer-plugin.ts +194 -0
  102. package/src/ipc-tools-plugin.ts +16 -8
  103. package/src/kimaki-digital-twin.e2e.test.ts +2 -1
  104. package/src/kimaki-opencode-plugin.ts +1 -0
  105. package/src/logger.ts +0 -1
  106. package/src/markdown.test.ts +4 -5
  107. package/src/markdown.ts +2 -2
  108. package/src/message-finish-field.e2e.test.ts +2 -1
  109. package/src/message-preprocessing.ts +117 -16
  110. package/src/onboarding-tutorial.ts +1 -1
  111. package/src/opencode-command-detection.test.ts +268 -0
  112. package/src/opencode-command-detection.ts +79 -0
  113. package/src/opencode-interrupt-plugin.test.ts +93 -50
  114. package/src/opencode-interrupt-plugin.ts +86 -9
  115. package/src/opencode.ts +34 -34
  116. package/src/plugin-logger.ts +68 -0
  117. package/src/queue-advanced-abort.e2e.test.ts +14 -15
  118. package/src/queue-advanced-e2e-setup.ts +2 -0
  119. package/src/queue-advanced-permissions-typing.e2e.test.ts +34 -24
  120. package/src/queue-advanced-question.e2e.test.ts +243 -179
  121. package/src/queue-advanced-typing-interrupt.e2e.test.ts +13 -5
  122. package/src/queue-question-select-drain.e2e.test.ts +31 -28
  123. package/src/runtime-lifecycle.e2e.test.ts +2 -0
  124. package/src/sentry.ts +7 -120
  125. package/src/session-handler/event-stream-state.ts +1 -1
  126. package/src/session-handler/thread-runtime-state.ts +17 -0
  127. package/src/session-handler/thread-session-runtime.ts +254 -52
  128. package/src/session-title-rename.test.ts +112 -0
  129. package/src/startup-time.e2e.test.ts +2 -1
  130. package/src/store.ts +3 -8
  131. package/src/system-message.test.ts +612 -0
  132. package/src/system-message.ts +136 -63
  133. package/src/task-runner.ts +7 -4
  134. package/src/task-schedule.ts +3 -0
  135. package/src/test-utils.ts +21 -0
  136. package/src/thread-message-queue.e2e.test.ts +38 -43
  137. package/src/undici.d.ts +12 -0
  138. package/src/unnest-code-blocks.test.ts +34 -0
  139. package/src/unnest-code-blocks.ts +18 -1
  140. package/src/voice-handler.ts +18 -4
  141. package/src/voice-message.e2e.test.ts +2 -0
  142. package/src/voice.test.ts +2 -0
  143. package/src/voice.ts +68 -23
  144. package/src/worktree-lifecycle.e2e.test.ts +1 -1
  145. package/src/worktrees.test.ts +2 -2
  146. package/src/worktrees.ts +152 -156
@@ -6,12 +6,13 @@
6
6
  *
7
7
  * cd ~/.config/opencode
8
8
  * bun init -y
9
- * bun add @openauthjs/openauth proper-lockfile
9
+ * bun add proper-lockfile
10
10
  *
11
- * Handles two concerns:
11
+ * Handles three concerns:
12
12
  * 1. OAuth login + token refresh (PKCE flow against claude.ai)
13
13
  * 2. Request/response rewriting (tool names, system prompt, beta headers)
14
14
  * so the Anthropic API treats requests as Claude Code CLI requests.
15
+ * 3. Multi-account OAuth rotation after Anthropic rate-limit/auth failures.
15
16
  *
16
17
  * Login mode is chosen from environment:
17
18
  * - `KIMAKI` set: remote-first pasted callback URL/raw code flow
@@ -21,53 +22,67 @@
21
22
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/anthropic.ts
22
23
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
23
24
  */
24
- import { generatePKCE } from "@openauthjs/openauth/pkce";
25
- import { spawn } from "node:child_process";
26
- import * as fs from "node:fs/promises";
27
- import { createServer } from "node:http";
28
- import { homedir } from "node:os";
29
- import path from "node:path";
30
- import lockfile from "proper-lockfile";
25
+ import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from './anthropic-auth-state.js';
26
+ // PKCE (Proof Key for Code Exchange) using Web Crypto API.
27
+ // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
28
+ function base64urlEncode(bytes) {
29
+ let binary = '';
30
+ for (const byte of bytes) {
31
+ binary += String.fromCharCode(byte);
32
+ }
33
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
34
+ }
35
+ async function generatePKCE() {
36
+ const verifierBytes = new Uint8Array(32);
37
+ crypto.getRandomValues(verifierBytes);
38
+ const verifier = base64urlEncode(verifierBytes);
39
+ const data = new TextEncoder().encode(verifier);
40
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
41
+ const challenge = base64urlEncode(new Uint8Array(hashBuffer));
42
+ return { verifier, challenge };
43
+ }
44
+ import { spawn } from 'node:child_process';
45
+ import { createServer } from 'node:http';
31
46
  // --- Constants ---
32
47
  const CLIENT_ID = (() => {
33
- const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
34
- return typeof atob === "function"
48
+ const encoded = 'OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl';
49
+ return typeof atob === 'function'
35
50
  ? atob(encoded)
36
- : Buffer.from(encoded, "base64").toString("utf8");
51
+ : Buffer.from(encoded, 'base64').toString('utf8');
37
52
  })();
38
- const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
39
- const CREATE_API_KEY_URL = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key";
53
+ const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
54
+ const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key';
40
55
  const CALLBACK_PORT = 53692;
41
- const CALLBACK_PATH = "/callback";
56
+ const CALLBACK_PATH = '/callback';
42
57
  const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
43
- const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
58
+ const SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
44
59
  const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
45
- const CLAUDE_CODE_VERSION = "2.1.75";
60
+ const CLAUDE_CODE_VERSION = '2.1.75';
46
61
  const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
47
- const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
48
- const CLAUDE_CODE_BETA = "claude-code-20250219";
49
- const OAUTH_BETA = "oauth-2025-04-20";
50
- const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
51
- const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
62
+ const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
63
+ const CLAUDE_CODE_BETA = 'claude-code-20250219';
64
+ const OAUTH_BETA = 'oauth-2025-04-20';
65
+ const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14';
66
+ const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
52
67
  const ANTHROPIC_HOSTS = new Set([
53
- "api.anthropic.com",
54
- "claude.ai",
55
- "console.anthropic.com",
56
- "platform.claude.com",
68
+ 'api.anthropic.com',
69
+ 'claude.ai',
70
+ 'console.anthropic.com',
71
+ 'platform.claude.com',
57
72
  ]);
58
73
  const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME = {
59
- bash: "Bash",
60
- edit: "Edit",
61
- glob: "Glob",
62
- grep: "Grep",
63
- question: "AskUserQuestion",
64
- read: "Read",
65
- skill: "Skill",
66
- task: "Task",
67
- todowrite: "TodoWrite",
68
- webfetch: "WebFetch",
69
- websearch: "WebSearch",
70
- write: "Write",
74
+ bash: 'Bash',
75
+ edit: 'Edit',
76
+ glob: 'Glob',
77
+ grep: 'Grep',
78
+ question: 'AskUserQuestion',
79
+ read: 'Read',
80
+ skill: 'Skill',
81
+ task: 'Task',
82
+ todowrite: 'TodoWrite',
83
+ webfetch: 'WebFetch',
84
+ websearch: 'WebSearch',
85
+ write: 'Write',
71
86
  };
72
87
  // --- HTTP helpers ---
73
88
  // Claude OAuth token exchange can 429 when this runs inside the opencode auth
@@ -82,7 +97,9 @@ async function requestText(urlString, options) {
82
97
  method: options.method,
83
98
  url: urlString,
84
99
  });
85
- const child = spawn("node", ["-e", `
100
+ const child = spawn('node', [
101
+ '-e',
102
+ `
86
103
  const input = JSON.parse(process.argv[1]);
87
104
  (async () => {
88
105
  const response = await fetch(input.url, {
@@ -100,33 +117,35 @@ const input = JSON.parse(process.argv[1]);
100
117
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
101
118
  process.exit(1);
102
119
  });
103
- `.trim(), payload], {
104
- stdio: ["ignore", "pipe", "pipe"],
120
+ `.trim(),
121
+ payload,
122
+ ], {
123
+ stdio: ['ignore', 'pipe', 'pipe'],
105
124
  });
106
- let stdout = "";
107
- let stderr = "";
125
+ let stdout = '';
126
+ let stderr = '';
108
127
  const timeout = setTimeout(() => {
109
128
  child.kill();
110
129
  reject(new Error(`Request timed out. url=${urlString}`));
111
130
  }, 30_000);
112
- child.stdout.on("data", (chunk) => {
131
+ child.stdout.on('data', (chunk) => {
113
132
  stdout += String(chunk);
114
133
  });
115
- child.stderr.on("data", (chunk) => {
134
+ child.stderr.on('data', (chunk) => {
116
135
  stderr += String(chunk);
117
136
  });
118
- child.on("error", (error) => {
137
+ child.on('error', (error) => {
119
138
  clearTimeout(timeout);
120
139
  reject(error);
121
140
  });
122
- child.on("close", (code) => {
141
+ child.on('close', (code) => {
123
142
  clearTimeout(timeout);
124
143
  if (code !== 0) {
125
144
  let details = stderr.trim();
126
145
  try {
127
146
  const parsed = JSON.parse(details);
128
- if (typeof parsed.status === "number") {
129
- reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`));
147
+ if (typeof parsed.status === 'number') {
148
+ reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ''}`));
130
149
  return;
131
150
  }
132
151
  }
@@ -143,42 +162,17 @@ const input = JSON.parse(process.argv[1]);
143
162
  async function postJson(url, body) {
144
163
  const requestBody = JSON.stringify(body);
145
164
  const responseText = await requestText(url, {
146
- method: "POST",
165
+ method: 'POST',
147
166
  headers: {
148
- Accept: "application/json",
149
- "Content-Length": String(Buffer.byteLength(requestBody)),
150
- "Content-Type": "application/json",
167
+ Accept: 'application/json',
168
+ 'Content-Length': String(Buffer.byteLength(requestBody)),
169
+ 'Content-Type': 'application/json',
151
170
  },
152
171
  body: requestBody,
153
172
  });
154
173
  return JSON.parse(responseText);
155
174
  }
156
- // --- File lock for token refresh ---
157
- let pendingRefresh;
158
- function authFilePath() {
159
- if (process.env.XDG_DATA_HOME) {
160
- return path.join(process.env.XDG_DATA_HOME, "opencode", "auth.json");
161
- }
162
- return path.join(homedir(), ".local", "share", "opencode", "auth.json");
163
- }
164
- async function withAuthRefreshLock(fn) {
165
- const file = authFilePath();
166
- await fs.mkdir(path.dirname(file), { recursive: true });
167
- await fs.appendFile(file, "");
168
- const release = await lockfile.lock(file, {
169
- realpath: false,
170
- stale: 30_000,
171
- update: 15_000,
172
- retries: { factor: 1.3, forever: true, maxTimeout: 1_000, minTimeout: 100 },
173
- onCompromised: () => { },
174
- });
175
- try {
176
- return await fn();
177
- }
178
- finally {
179
- await release().catch(() => { });
180
- }
181
- }
175
+ const pendingRefresh = new Map();
182
176
  // --- OAuth token exchange & refresh ---
183
177
  function parseTokenResponse(json) {
184
178
  const data = json;
@@ -192,7 +186,7 @@ function tokenExpiry(expiresIn) {
192
186
  }
193
187
  async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
194
188
  const json = await postJson(TOKEN_URL, {
195
- grant_type: "authorization_code",
189
+ grant_type: 'authorization_code',
196
190
  client_id: CLIENT_ID,
197
191
  code,
198
192
  state,
@@ -201,7 +195,7 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
201
195
  });
202
196
  const data = parseTokenResponse(json);
203
197
  return {
204
- type: "success",
198
+ type: 'success',
205
199
  refresh: data.refresh_token,
206
200
  access: data.access_token,
207
201
  expires: tokenExpiry(data.expires_in),
@@ -209,13 +203,13 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
209
203
  }
210
204
  async function refreshAnthropicToken(refreshToken) {
211
205
  const json = await postJson(TOKEN_URL, {
212
- grant_type: "refresh_token",
206
+ grant_type: 'refresh_token',
213
207
  client_id: CLIENT_ID,
214
208
  refresh_token: refreshToken,
215
209
  });
216
210
  const data = parseTokenResponse(json);
217
211
  return {
218
- type: "oauth",
212
+ type: 'oauth',
219
213
  refresh: data.refresh_token,
220
214
  access: data.access_token,
221
215
  expires: tokenExpiry(data.expires_in),
@@ -223,15 +217,15 @@ async function refreshAnthropicToken(refreshToken) {
223
217
  }
224
218
  async function createApiKey(accessToken) {
225
219
  const responseText = await requestText(CREATE_API_KEY_URL, {
226
- method: "POST",
220
+ method: 'POST',
227
221
  headers: {
228
- Accept: "application/json",
222
+ Accept: 'application/json',
229
223
  authorization: `Bearer ${accessToken}`,
230
- "Content-Type": "application/json",
224
+ 'Content-Type': 'application/json',
231
225
  },
232
226
  });
233
227
  const json = JSON.parse(responseText);
234
- return { type: "success", key: json.raw_key };
228
+ return { type: 'success', key: json.raw_key };
235
229
  }
236
230
  async function startCallbackServer(expectedState) {
237
231
  return new Promise((resolve, reject) => {
@@ -247,37 +241,45 @@ async function startCallbackServer(expectedState) {
247
241
  });
248
242
  const server = createServer((req, res) => {
249
243
  try {
250
- const url = new URL(req.url || "", "http://localhost");
244
+ const url = new URL(req.url || '', 'http://localhost');
251
245
  if (url.pathname !== CALLBACK_PATH) {
252
- res.writeHead(404).end("Not found");
246
+ res.writeHead(404).end('Not found');
253
247
  return;
254
248
  }
255
- const code = url.searchParams.get("code");
256
- const state = url.searchParams.get("state");
257
- const error = url.searchParams.get("error");
249
+ const code = url.searchParams.get('code');
250
+ const state = url.searchParams.get('state');
251
+ const error = url.searchParams.get('error');
258
252
  if (error || !code || !state || state !== expectedState) {
259
- res.writeHead(400).end("Authentication failed: " + (error || "missing code/state"));
253
+ res.writeHead(400).end('Authentication failed: ' + (error || 'missing code/state'));
260
254
  return;
261
255
  }
262
- res.writeHead(200, { "Content-Type": "text/plain" }).end("Authentication successful. You can close this window.");
256
+ res
257
+ .writeHead(200, { 'Content-Type': 'text/plain' })
258
+ .end('Authentication successful. You can close this window.');
263
259
  settle?.({ code, state });
264
260
  }
265
261
  catch {
266
- res.writeHead(500).end("Internal error");
262
+ res.writeHead(500).end('Internal error');
267
263
  }
268
264
  });
269
- server.once("error", reject);
270
- server.listen(CALLBACK_PORT, "127.0.0.1", () => {
265
+ server.once('error', reject);
266
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
271
267
  resolve({
272
268
  server,
273
- cancelWait: () => { settle?.(null); },
269
+ cancelWait: () => {
270
+ settle?.(null);
271
+ },
274
272
  waitForCode: () => waitPromise,
275
273
  });
276
274
  });
277
275
  });
278
276
  }
279
277
  function closeServer(server) {
280
- return new Promise((resolve) => { server.close(() => { resolve(); }); });
278
+ return new Promise((resolve) => {
279
+ server.close(() => {
280
+ resolve();
281
+ });
282
+ });
281
283
  }
282
284
  // --- Authorization flow ---
283
285
  // Unified flow: beginAuthorizationFlow starts PKCE + callback server,
@@ -286,13 +288,13 @@ async function beginAuthorizationFlow() {
286
288
  const pkce = await generatePKCE();
287
289
  const callbackServer = await startCallbackServer(pkce.verifier);
288
290
  const authParams = new URLSearchParams({
289
- code: "true",
291
+ code: 'true',
290
292
  client_id: CLIENT_ID,
291
- response_type: "code",
293
+ response_type: 'code',
292
294
  redirect_uri: REDIRECT_URI,
293
295
  scope: SCOPES,
294
296
  code_challenge: pkce.challenge,
295
- code_challenge_method: "S256",
297
+ code_challenge_method: 'S256',
296
298
  state: pkce.verifier,
297
299
  });
298
300
  return {
@@ -306,7 +308,11 @@ async function waitForCallback(callbackServer, manualInput) {
306
308
  // Try localhost callback first (instant check)
307
309
  const quick = await Promise.race([
308
310
  callbackServer.waitForCode(),
309
- new Promise((r) => { setTimeout(() => { r(null); }, 50); }),
311
+ new Promise((r) => {
312
+ setTimeout(() => {
313
+ r(null);
314
+ }, 50);
315
+ }),
310
316
  ]);
311
317
  if (quick?.code)
312
318
  return quick;
@@ -318,10 +324,14 @@ async function waitForCallback(callbackServer, manualInput) {
318
324
  // Wait for localhost callback with timeout
319
325
  const result = await Promise.race([
320
326
  callbackServer.waitForCode(),
321
- new Promise((r) => { setTimeout(() => { r(null); }, OAUTH_TIMEOUT_MS); }),
327
+ new Promise((r) => {
328
+ setTimeout(() => {
329
+ r(null);
330
+ }, OAUTH_TIMEOUT_MS);
331
+ }),
322
332
  ]);
323
333
  if (!result?.code) {
324
- throw new Error("Timed out waiting for OAuth callback");
334
+ throw new Error('Timed out waiting for OAuth callback');
325
335
  }
326
336
  return result;
327
337
  }
@@ -333,25 +343,25 @@ async function waitForCallback(callbackServer, manualInput) {
333
343
  function parseManualInput(input) {
334
344
  try {
335
345
  const url = new URL(input);
336
- const code = url.searchParams.get("code");
337
- const state = url.searchParams.get("state");
346
+ const code = url.searchParams.get('code');
347
+ const state = url.searchParams.get('state');
338
348
  if (code)
339
- return { code, state: state || "" };
349
+ return { code, state: state || '' };
340
350
  }
341
351
  catch {
342
352
  // not a URL
343
353
  }
344
- if (input.includes("#")) {
345
- const [code = "", state = ""] = input.split("#", 2);
354
+ if (input.includes('#')) {
355
+ const [code = '', state = ''] = input.split('#', 2);
346
356
  return { code, state };
347
357
  }
348
- if (input.includes("code=")) {
358
+ if (input.includes('code=')) {
349
359
  const params = new URLSearchParams(input);
350
- const code = params.get("code");
360
+ const code = params.get('code');
351
361
  if (code)
352
- return { code, state: params.get("state") || "" };
362
+ return { code, state: params.get('state') || '' };
353
363
  }
354
- return { code: input, state: "" };
364
+ return { code: input, state: '' };
355
365
  }
356
366
  // Unified authorize handler: returns either OAuth tokens or an API key,
357
367
  // for both auto and remote-first modes.
@@ -363,16 +373,22 @@ function buildAuthorizeHandler(mode) {
363
373
  const finalize = async (result) => {
364
374
  const verifier = auth.verifier;
365
375
  const creds = await exchangeAuthorizationCode(result.code, result.state || verifier, verifier, REDIRECT_URI);
366
- if (mode === "apikey") {
376
+ if (mode === 'apikey') {
367
377
  return createApiKey(creds.access);
368
378
  }
379
+ await rememberAnthropicOAuth({
380
+ type: 'oauth',
381
+ refresh: creds.refresh,
382
+ access: creds.access,
383
+ expires: creds.expires,
384
+ });
369
385
  return creds;
370
386
  };
371
387
  if (!isRemote) {
372
388
  return {
373
389
  url: auth.url,
374
- instructions: "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
375
- method: "auto",
390
+ instructions: 'Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.',
391
+ method: 'auto',
376
392
  callback: async () => {
377
393
  pendingAuthResult ??= (async () => {
378
394
  try {
@@ -381,7 +397,7 @@ function buildAuthorizeHandler(mode) {
381
397
  }
382
398
  catch (error) {
383
399
  console.error(`[anthropic-auth] ${error}`);
384
- return { type: "failed" };
400
+ return { type: 'failed' };
385
401
  }
386
402
  })();
387
403
  return pendingAuthResult;
@@ -390,8 +406,8 @@ function buildAuthorizeHandler(mode) {
390
406
  }
391
407
  return {
392
408
  url: auth.url,
393
- instructions: "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
394
- method: "code",
409
+ instructions: 'Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.',
410
+ method: 'code',
395
411
  callback: async (input) => {
396
412
  pendingAuthResult ??= (async () => {
397
413
  try {
@@ -400,7 +416,7 @@ function buildAuthorizeHandler(mode) {
400
416
  }
401
417
  catch (error) {
402
418
  console.error(`[anthropic-auth] ${error}`);
403
- return { type: "failed" };
419
+ return { type: 'failed' };
404
420
  }
405
421
  })();
406
422
  return pendingAuthResult;
@@ -418,23 +434,23 @@ function sanitizeSystemText(text) {
418
434
  return text.replaceAll(OPENCODE_IDENTITY, CLAUDE_CODE_IDENTITY);
419
435
  }
420
436
  function prependClaudeCodeIdentity(system) {
421
- const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
422
- if (typeof system === "undefined")
437
+ const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY };
438
+ if (typeof system === 'undefined')
423
439
  return [identityBlock];
424
- if (typeof system === "string") {
440
+ if (typeof system === 'string') {
425
441
  const sanitized = sanitizeSystemText(system);
426
442
  if (sanitized === CLAUDE_CODE_IDENTITY)
427
443
  return [identityBlock];
428
- return [identityBlock, { type: "text", text: sanitized }];
444
+ return [identityBlock, { type: 'text', text: sanitized }];
429
445
  }
430
446
  if (!Array.isArray(system))
431
447
  return [identityBlock, system];
432
448
  const sanitized = system.map((item) => {
433
- if (typeof item === "string")
434
- return { type: "text", text: sanitizeSystemText(item) };
435
- if (item && typeof item === "object" && item.type === "text") {
449
+ if (typeof item === 'string')
450
+ return { type: 'text', text: sanitizeSystemText(item) };
451
+ if (item && typeof item === 'object' && item.type === 'text') {
436
452
  const text = item.text;
437
- if (typeof text === "string") {
453
+ if (typeof text === 'string') {
438
454
  return { ...item, text: sanitizeSystemText(text) };
439
455
  }
440
456
  }
@@ -442,8 +458,8 @@ function prependClaudeCodeIdentity(system) {
442
458
  });
443
459
  const first = sanitized[0];
444
460
  if (first &&
445
- typeof first === "object" &&
446
- first.type === "text" &&
461
+ typeof first === 'object' &&
462
+ first.type === 'text' &&
447
463
  first.text === CLAUDE_CODE_IDENTITY) {
448
464
  return sanitized;
449
465
  }
@@ -455,14 +471,14 @@ function rewriteRequestPayload(body) {
455
471
  try {
456
472
  const payload = JSON.parse(body);
457
473
  const reverseToolNameMap = new Map();
458
- const modelId = typeof payload.model === "string" ? payload.model : undefined;
474
+ const modelId = typeof payload.model === 'string' ? payload.model : undefined;
459
475
  // Build reverse map and rename tools
460
476
  if (Array.isArray(payload.tools)) {
461
477
  payload.tools = payload.tools.map((tool) => {
462
- if (!tool || typeof tool !== "object")
478
+ if (!tool || typeof tool !== 'object')
463
479
  return tool;
464
480
  const name = tool.name;
465
- if (typeof name !== "string")
481
+ if (typeof name !== 'string')
466
482
  return tool;
467
483
  const mapped = toClaudeCodeToolName(name);
468
484
  reverseToolNameMap.set(mapped, name);
@@ -473,10 +489,10 @@ function rewriteRequestPayload(body) {
473
489
  payload.system = prependClaudeCodeIdentity(payload.system);
474
490
  // Rename tool_choice
475
491
  if (payload.tool_choice &&
476
- typeof payload.tool_choice === "object" &&
477
- payload.tool_choice.type === "tool") {
492
+ typeof payload.tool_choice === 'object' &&
493
+ payload.tool_choice.type === 'tool') {
478
494
  const name = payload.tool_choice.name;
479
- if (typeof name === "string") {
495
+ if (typeof name === 'string') {
480
496
  payload.tool_choice = {
481
497
  ...payload.tool_choice,
482
498
  name: toClaudeCodeToolName(name),
@@ -486,7 +502,7 @@ function rewriteRequestPayload(body) {
486
502
  // Rename tool_use blocks in messages
487
503
  if (Array.isArray(payload.messages)) {
488
504
  payload.messages = payload.messages.map((message) => {
489
- if (!message || typeof message !== "object")
505
+ if (!message || typeof message !== 'object')
490
506
  return message;
491
507
  const content = message.content;
492
508
  if (!Array.isArray(content))
@@ -494,10 +510,10 @@ function rewriteRequestPayload(body) {
494
510
  return {
495
511
  ...message,
496
512
  content: content.map((block) => {
497
- if (!block || typeof block !== "object")
513
+ if (!block || typeof block !== 'object')
498
514
  return block;
499
515
  const b = block;
500
- if (b.type !== "tool_use" || typeof b.name !== "string")
516
+ if (b.type !== 'tool_use' || typeof b.name !== 'string')
501
517
  return block;
502
518
  return { ...block, name: toClaudeCodeToolName(b.name) };
503
519
  }),
@@ -516,7 +532,7 @@ function wrapResponseStream(response, reverseToolNameMap) {
516
532
  const reader = response.body.getReader();
517
533
  const decoder = new TextDecoder();
518
534
  const encoder = new TextEncoder();
519
- let carry = "";
535
+ let carry = '';
520
536
  const transform = (text) => {
521
537
  return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name) => {
522
538
  const original = reverseToolNameMap.get(name);
@@ -554,10 +570,10 @@ function wrapResponseStream(response, reverseToolNameMap) {
554
570
  // --- Beta headers ---
555
571
  function getRequiredBetas(modelId) {
556
572
  const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA];
557
- const isAdaptive = modelId?.includes("opus-4-6") ||
558
- modelId?.includes("opus-4.6") ||
559
- modelId?.includes("sonnet-4-6") ||
560
- modelId?.includes("sonnet-4.6");
573
+ const isAdaptive = modelId?.includes('opus-4-6') ||
574
+ modelId?.includes('opus-4.6') ||
575
+ modelId?.includes('sonnet-4-6') ||
576
+ modelId?.includes('sonnet-4.6');
561
577
  if (!isAdaptive)
562
578
  betas.push(INTERLEAVED_THINKING_BETA);
563
579
  return betas;
@@ -566,13 +582,16 @@ function mergeBetas(existing, required) {
566
582
  return [
567
583
  ...new Set([
568
584
  ...required,
569
- ...(existing || "").split(",").map((s) => s.trim()).filter(Boolean),
585
+ ...(existing || '')
586
+ .split(',')
587
+ .map((s) => s.trim())
588
+ .filter(Boolean),
570
589
  ]),
571
- ].join(",");
590
+ ].join(',');
572
591
  }
573
592
  // --- Token refresh with dedup ---
574
593
  function isOAuthStored(auth) {
575
- return auth.type === "oauth";
594
+ return auth.type === 'oauth';
576
595
  }
577
596
  async function getFreshOAuth(getAuth, client) {
578
597
  const auth = await getAuth();
@@ -580,38 +599,46 @@ async function getFreshOAuth(getAuth, client) {
580
599
  return undefined;
581
600
  if (auth.access && auth.expires > Date.now())
582
601
  return auth;
583
- if (!pendingRefresh) {
584
- pendingRefresh = withAuthRefreshLock(async () => {
585
- const latest = await getAuth();
586
- if (!isOAuthStored(latest)) {
587
- throw new Error("Anthropic OAuth credentials disappeared during refresh");
588
- }
589
- if (latest.access && latest.expires > Date.now())
590
- return latest;
591
- const refreshed = await refreshAnthropicToken(latest.refresh);
592
- await client.auth.set({ path: { id: "anthropic" }, body: refreshed });
593
- return refreshed;
594
- }).finally(() => {
595
- pendingRefresh = undefined;
596
- });
602
+ const pending = pendingRefresh.get(auth.refresh);
603
+ if (pending) {
604
+ return pending;
597
605
  }
598
- return pendingRefresh;
606
+ const refreshPromise = withAuthStateLock(async () => {
607
+ const latest = await getAuth();
608
+ if (!isOAuthStored(latest)) {
609
+ throw new Error('Anthropic OAuth credentials disappeared during refresh');
610
+ }
611
+ if (latest.access && latest.expires > Date.now())
612
+ return latest;
613
+ const refreshed = await refreshAnthropicToken(latest.refresh);
614
+ await setAnthropicAuth(refreshed, client);
615
+ const store = await loadAccountStore();
616
+ if (store.accounts.length > 0) {
617
+ upsertAccount(store, refreshed);
618
+ await saveAccountStore(store);
619
+ }
620
+ return refreshed;
621
+ });
622
+ pendingRefresh.set(auth.refresh, refreshPromise);
623
+ return refreshPromise.finally(() => {
624
+ pendingRefresh.delete(auth.refresh);
625
+ });
599
626
  }
600
627
  // --- Plugin export ---
601
628
  const AnthropicAuthPlugin = async ({ client }) => {
602
629
  return {
603
630
  auth: {
604
- provider: "anthropic",
631
+ provider: 'anthropic',
605
632
  async loader(getAuth, provider) {
606
633
  const auth = await getAuth();
607
- if (auth.type !== "oauth")
634
+ if (auth.type !== 'oauth')
608
635
  return {};
609
636
  // Zero out costs for OAuth users (Claude Pro/Max subscription)
610
637
  for (const model of Object.values(provider.models)) {
611
638
  model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
612
639
  }
613
640
  return {
614
- apiKey: "",
641
+ apiKey: '',
615
642
  async fetch(input, init) {
616
643
  const url = (() => {
617
644
  try {
@@ -623,55 +650,79 @@ const AnthropicAuthPlugin = async ({ client }) => {
623
650
  })();
624
651
  if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
625
652
  return fetch(input, init);
626
- const freshAuth = await getFreshOAuth(getAuth, client);
627
- if (!freshAuth)
628
- return fetch(input, init);
629
- const originalBody = typeof init?.body === "string"
653
+ const originalBody = typeof init?.body === 'string'
630
654
  ? init.body
631
655
  : input instanceof Request
632
- ? await input.clone().text().catch(() => undefined)
656
+ ? await input
657
+ .clone()
658
+ .text()
659
+ .catch(() => undefined)
633
660
  : undefined;
634
661
  const rewritten = rewriteRequestPayload(originalBody);
635
662
  const headers = new Headers(init?.headers);
636
663
  if (input instanceof Request) {
637
- input.headers.forEach((v, k) => { if (!headers.has(k))
638
- headers.set(k, v); });
664
+ input.headers.forEach((v, k) => {
665
+ if (!headers.has(k))
666
+ headers.set(k, v);
667
+ });
639
668
  }
640
669
  const betas = getRequiredBetas(rewritten.modelId);
641
- headers.set("accept", "application/json");
642
- headers.set("anthropic-beta", mergeBetas(headers.get("anthropic-beta"), betas));
643
- headers.set("anthropic-dangerous-direct-browser-access", "true");
644
- headers.set("authorization", `Bearer ${freshAuth.access}`);
645
- headers.set("user-agent", process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`);
646
- headers.set("x-app", "cli");
647
- headers.delete("x-api-key");
648
- const response = await fetch(input, {
649
- ...(init ?? {}),
650
- body: rewritten.body,
651
- headers,
652
- });
670
+ const runRequest = async (auth) => {
671
+ const requestHeaders = new Headers(headers);
672
+ requestHeaders.set('accept', 'application/json');
673
+ requestHeaders.set('anthropic-beta', mergeBetas(requestHeaders.get('anthropic-beta'), betas));
674
+ requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true');
675
+ requestHeaders.set('authorization', `Bearer ${auth.access}`);
676
+ requestHeaders.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`);
677
+ requestHeaders.set('x-app', 'cli');
678
+ requestHeaders.delete('x-api-key');
679
+ return fetch(input, {
680
+ ...(init ?? {}),
681
+ body: rewritten.body,
682
+ headers: requestHeaders,
683
+ });
684
+ };
685
+ const freshAuth = await getFreshOAuth(getAuth, client);
686
+ if (!freshAuth)
687
+ return fetch(input, init);
688
+ let response = await runRequest(freshAuth);
689
+ if (!response.ok) {
690
+ const bodyText = await response
691
+ .clone()
692
+ .text()
693
+ .catch(() => '');
694
+ if (shouldRotateAuth(response.status, bodyText)) {
695
+ const rotated = await rotateAnthropicAccount(freshAuth, client);
696
+ if (rotated) {
697
+ const retryAuth = await getFreshOAuth(getAuth, client);
698
+ if (retryAuth) {
699
+ response = await runRequest(retryAuth);
700
+ }
701
+ }
702
+ }
703
+ }
653
704
  return wrapResponseStream(response, rewritten.reverseToolNameMap);
654
705
  },
655
706
  };
656
707
  },
657
708
  methods: [
658
709
  {
659
- label: "Claude Pro/Max",
660
- type: "oauth",
661
- authorize: buildAuthorizeHandler("oauth"),
710
+ label: 'Claude Pro/Max',
711
+ type: 'oauth',
712
+ authorize: buildAuthorizeHandler('oauth'),
662
713
  },
663
714
  {
664
- label: "Create an API Key",
665
- type: "oauth",
666
- authorize: buildAuthorizeHandler("apikey"),
715
+ label: 'Create an API Key',
716
+ type: 'oauth',
717
+ authorize: buildAuthorizeHandler('apikey'),
667
718
  },
668
719
  {
669
- provider: "anthropic",
670
- label: "Manually enter API Key",
671
- type: "api",
720
+ provider: 'anthropic',
721
+ label: 'Manually enter API Key',
722
+ type: 'api',
672
723
  },
673
724
  ],
674
725
  },
675
726
  };
676
727
  };
677
- export { AnthropicAuthPlugin as anthropicAuthPlugin };
728
+ export { AnthropicAuthPlugin as anthropicAuthPlugin, };