@otto-assistant/bridge 0.4.100 → 0.4.101

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.
@@ -23,7 +23,7 @@
23
23
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
24
24
  */
25
25
 
26
- import type { Plugin } from '@opencode-ai/plugin'
26
+ import type { Plugin } from "@opencode-ai/plugin";
27
27
  import {
28
28
  loadAccountStore,
29
29
  rememberAnthropicOAuth,
@@ -34,100 +34,113 @@ import {
34
34
  type OAuthStored,
35
35
  upsertAccount,
36
36
  withAuthStateLock,
37
- } from './anthropic-auth-state.js'
37
+ } from "./anthropic-auth-state.js";
38
38
  import {
39
39
  extractAnthropicAccountIdentity,
40
40
  type AnthropicAccountIdentity,
41
- } from './anthropic-account-identity.js'
41
+ } from "./anthropic-account-identity.js";
42
42
  // PKCE (Proof Key for Code Exchange) using Web Crypto API.
43
43
  // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
44
44
  function base64urlEncode(bytes: Uint8Array): string {
45
- let binary = ''
45
+ let binary = "";
46
46
  for (const byte of bytes) {
47
- binary += String.fromCharCode(byte)
47
+ binary += String.fromCharCode(byte);
48
48
  }
49
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
49
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
50
50
  }
51
51
 
52
- async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
53
- const verifierBytes = new Uint8Array(32)
54
- crypto.getRandomValues(verifierBytes)
55
- const verifier = base64urlEncode(verifierBytes)
56
- const data = new TextEncoder().encode(verifier)
57
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
58
- const challenge = base64urlEncode(new Uint8Array(hashBuffer))
59
- return { verifier, challenge }
52
+ async function generatePKCE(): Promise<{
53
+ verifier: string;
54
+ challenge: string;
55
+ }> {
56
+ const verifierBytes = new Uint8Array(32);
57
+ crypto.getRandomValues(verifierBytes);
58
+ const verifier = base64urlEncode(verifierBytes);
59
+ const data = new TextEncoder().encode(verifier);
60
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
61
+ const challenge = base64urlEncode(new Uint8Array(hashBuffer));
62
+ return { verifier, challenge };
60
63
  }
61
- import { spawn } from 'node:child_process'
62
- import { createServer, type Server } from 'node:http'
64
+ import { spawn } from "node:child_process";
65
+ import { createServer, type Server } from "node:http";
63
66
 
64
67
  // --- Constants ---
65
68
 
66
69
  const CLIENT_ID = (() => {
67
- const encoded = 'OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl'
68
- return typeof atob === 'function'
70
+ const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
71
+ return typeof atob === "function"
69
72
  ? atob(encoded)
70
- : Buffer.from(encoded, 'base64').toString('utf8')
71
- })()
72
-
73
- const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token'
74
- const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key'
75
- const CLIENT_DATA_URL = 'https://api.anthropic.com/api/oauth/claude_cli/client_data'
76
- const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile'
77
- const CALLBACK_PORT = 53692
78
- const CALLBACK_PATH = '/callback'
79
- const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`
73
+ : Buffer.from(encoded, "base64").toString("utf8");
74
+ })();
75
+
76
+ const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
77
+ const CREATE_API_KEY_URL =
78
+ "https://api.anthropic.com/api/oauth/claude_cli/create_api_key";
79
+ const CLIENT_DATA_URL =
80
+ "https://api.anthropic.com/api/oauth/claude_cli/client_data";
81
+ const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
82
+ const CALLBACK_PORT = 53692;
83
+ const CALLBACK_PATH = "/callback";
84
+ const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
80
85
  const SCOPES =
81
- 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'
82
- const OAUTH_TIMEOUT_MS = 5 * 60 * 1000
83
- const CLAUDE_CODE_VERSION = '2.1.75'
84
- const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
85
- const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.'
86
- const CLAUDE_CODE_BETA = 'claude-code-20250219'
87
- const OAUTH_BETA = 'oauth-2025-04-20'
88
- const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14'
89
- const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14'
90
- const TOAST_SESSION_HEADER = 'x-kimaki-session-id'
86
+ "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
87
+ const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
88
+ const CLAUDE_CODE_VERSION = "2.1.75";
89
+ const CLAUDE_CODE_IDENTITY =
90
+ "You are Claude Code, Anthropic's official CLI for Claude.";
91
+ const OPENCODE_IDENTITY =
92
+ "You are OpenCode, the best coding agent on the planet.";
93
+ const ANTHROPIC_PROMPT_MARKER = "Skills provide specialized instructions";
94
+ const CLAUDE_CODE_BETA = "claude-code-20250219";
95
+ const OAUTH_BETA = "oauth-2025-04-20";
96
+ const FINE_GRAINED_TOOL_STREAMING_BETA =
97
+ "fine-grained-tool-streaming-2025-05-14";
98
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
99
+ const TOAST_SESSION_HEADER = "x-kimaki-session-id";
91
100
 
92
101
  const ANTHROPIC_HOSTS = new Set([
93
- 'api.anthropic.com',
94
- 'claude.ai',
95
- 'console.anthropic.com',
96
- 'platform.claude.com',
97
- ])
102
+ "api.anthropic.com",
103
+ "claude.ai",
104
+ "console.anthropic.com",
105
+ "platform.claude.com",
106
+ ]);
98
107
 
99
108
  const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME: Record<string, string> = {
100
- bash: 'Bash',
101
- edit: 'Edit',
102
- glob: 'Glob',
103
- grep: 'Grep',
104
- question: 'AskUserQuestion',
105
- read: 'Read',
106
- skill: 'Skill',
107
- task: 'Task',
108
- todowrite: 'TodoWrite',
109
- webfetch: 'WebFetch',
110
- websearch: 'WebSearch',
111
- write: 'Write',
112
- }
109
+ bash: "Bash",
110
+ edit: "Edit",
111
+ glob: "Glob",
112
+ grep: "Grep",
113
+ question: "AskUserQuestion",
114
+ read: "Read",
115
+ skill: "Skill",
116
+ task: "Task",
117
+ todowrite: "TodoWrite",
118
+ webfetch: "WebFetch",
119
+ websearch: "WebSearch",
120
+ write: "Write",
121
+ };
113
122
 
114
123
  // --- Types ---
115
124
 
116
125
  type OAuthSuccess = {
117
- type: 'success'
118
- provider?: string
119
- refresh: string
120
- access: string
121
- expires: number
122
- }
126
+ type: "success";
127
+ provider?: string;
128
+ refresh: string;
129
+ access: string;
130
+ expires: number;
131
+ };
123
132
 
124
133
  type ApiKeySuccess = {
125
- type: 'success'
126
- provider?: string
127
- key: string
128
- }
134
+ type: "success";
135
+ provider?: string;
136
+ key: string;
137
+ };
129
138
 
130
- type AuthResult = OAuthSuccess | ApiKeySuccess | { type: 'failed' }
139
+ type AuthResult = OAuthSuccess | ApiKeySuccess | { type: "failed" };
140
+ type PluginHooks = Awaited<ReturnType<Plugin>>;
141
+ type SystemTransformHook = NonNullable<
142
+ PluginHooks["experimental.chat.system.transform"]
143
+ >;
131
144
 
132
145
  // --- HTTP helpers ---
133
146
 
@@ -138,9 +151,9 @@ type AuthResult = OAuthSuccess | ApiKeySuccess | { type: 'failed' }
138
151
  async function requestText(
139
152
  urlString: string,
140
153
  options: {
141
- method: string
142
- headers?: Record<string, string>
143
- body?: string
154
+ method: string;
155
+ headers?: Record<string, string>;
156
+ body?: string;
144
157
  },
145
158
  ): Promise<string> {
146
159
  return new Promise((resolve, reject) => {
@@ -149,11 +162,11 @@ async function requestText(
149
162
  headers: options.headers,
150
163
  method: options.method,
151
164
  url: urlString,
152
- })
165
+ });
153
166
  const child = spawn(
154
- 'node',
167
+ "node",
155
168
  [
156
- '-e',
169
+ "-e",
157
170
  `
158
171
  const input = JSON.parse(process.argv[1]);
159
172
  (async () => {
@@ -176,82 +189,96 @@ const input = JSON.parse(process.argv[1]);
176
189
  payload,
177
190
  ],
178
191
  {
179
- stdio: ['ignore', 'pipe', 'pipe'],
192
+ stdio: ["ignore", "pipe", "pipe"],
180
193
  },
181
- )
194
+ );
182
195
 
183
- let stdout = ''
184
- let stderr = ''
196
+ let stdout = "";
197
+ let stderr = "";
185
198
  const timeout = setTimeout(() => {
186
- child.kill()
187
- reject(new Error(`Request timed out. url=${urlString}`))
188
- }, 30_000)
189
-
190
- child.stdout.on('data', (chunk) => {
191
- stdout += String(chunk)
192
- })
193
- child.stderr.on('data', (chunk) => {
194
- stderr += String(chunk)
195
- })
196
-
197
- child.on('error', (error) => {
198
- clearTimeout(timeout)
199
- reject(error)
200
- })
201
-
202
- child.on('close', (code) => {
203
- clearTimeout(timeout)
199
+ child.kill();
200
+ reject(new Error(`Request timed out. url=${urlString}`));
201
+ }, 30_000);
202
+
203
+ child.stdout.on("data", (chunk) => {
204
+ stdout += String(chunk);
205
+ });
206
+ child.stderr.on("data", (chunk) => {
207
+ stderr += String(chunk);
208
+ });
209
+
210
+ child.on("error", (error) => {
211
+ clearTimeout(timeout);
212
+ reject(error);
213
+ });
214
+
215
+ child.on("close", (code) => {
216
+ clearTimeout(timeout);
204
217
  if (code !== 0) {
205
- let details = stderr.trim()
218
+ let details = stderr.trim();
206
219
  try {
207
- const parsed = JSON.parse(details) as { status?: number; body?: string }
208
- if (typeof parsed.status === 'number') {
209
- reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ''}`))
210
- return
220
+ const parsed = JSON.parse(details) as {
221
+ status?: number;
222
+ body?: string;
223
+ };
224
+ if (typeof parsed.status === "number") {
225
+ reject(
226
+ new Error(
227
+ `HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`,
228
+ ),
229
+ );
230
+ return;
211
231
  }
212
232
  } catch {
213
233
  // fall back to raw stderr
214
234
  }
215
- reject(new Error(details || `Node helper exited with code ${code}`))
216
- return
235
+ reject(new Error(details || `Node helper exited with code ${code}`));
236
+ return;
217
237
  }
218
- resolve(stdout)
219
- })
220
- })
238
+ resolve(stdout);
239
+ });
240
+ });
221
241
  }
222
242
 
223
- async function postJson(url: string, body: Record<string, string | number>): Promise<unknown> {
224
- const requestBody = JSON.stringify(body)
243
+ async function postJson(
244
+ url: string,
245
+ body: Record<string, string | number>,
246
+ ): Promise<unknown> {
247
+ const requestBody = JSON.stringify(body);
225
248
  const responseText = await requestText(url, {
226
- method: 'POST',
249
+ method: "POST",
227
250
  headers: {
228
- Accept: 'application/json',
229
- 'Content-Length': String(Buffer.byteLength(requestBody)),
230
- 'Content-Type': 'application/json',
251
+ Accept: "application/json",
252
+ "Content-Length": String(Buffer.byteLength(requestBody)),
253
+ "Content-Type": "application/json",
231
254
  },
232
255
  body: requestBody,
233
- })
234
- return JSON.parse(responseText) as unknown
256
+ });
257
+ return JSON.parse(responseText) as unknown;
235
258
  }
236
259
 
237
- const pendingRefresh = new Map<string, Promise<OAuthStored>>()
260
+ const pendingRefresh = new Map<string, Promise<OAuthStored>>();
238
261
 
239
262
  // --- OAuth token exchange & refresh ---
240
263
 
241
264
  function parseTokenResponse(json: unknown): {
242
- access_token: string
243
- refresh_token: string
244
- expires_in: number
265
+ access_token: string;
266
+ refresh_token: string;
267
+ expires_in: number;
245
268
  } {
246
- const data = json as { access_token: string; refresh_token: string; expires_in: number }
269
+ const data = json as {
270
+ access_token: string;
271
+ refresh_token: string;
272
+ expires_in: number;
273
+ };
247
274
  if (!data.access_token || !data.refresh_token) {
248
- throw new Error(`Invalid token response: ${JSON.stringify(json)}`)
275
+ throw new Error(`Invalid token response: ${JSON.stringify(json)}`);
249
276
  }
250
- return data
277
+ return data;
251
278
  }
252
279
 
253
280
  function tokenExpiry(expiresIn: number) {
254
- return Date.now() + expiresIn * 1000 - 5 * 60 * 1000
281
+ return Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
255
282
  }
256
283
 
257
284
  async function exchangeAuthorizationCode(
@@ -261,134 +288,140 @@ async function exchangeAuthorizationCode(
261
288
  redirectUri: string,
262
289
  ): Promise<OAuthSuccess> {
263
290
  const json = await postJson(TOKEN_URL, {
264
- grant_type: 'authorization_code',
291
+ grant_type: "authorization_code",
265
292
  client_id: CLIENT_ID,
266
293
  code,
267
294
  state,
268
295
  redirect_uri: redirectUri,
269
296
  code_verifier: verifier,
270
- })
271
- const data = parseTokenResponse(json)
297
+ });
298
+ const data = parseTokenResponse(json);
272
299
  return {
273
- type: 'success',
300
+ type: "success",
274
301
  refresh: data.refresh_token,
275
302
  access: data.access_token,
276
303
  expires: tokenExpiry(data.expires_in),
277
- }
304
+ };
278
305
  }
279
306
 
280
- async function refreshAnthropicToken(refreshToken: string): Promise<OAuthStored> {
307
+ async function refreshAnthropicToken(
308
+ refreshToken: string,
309
+ ): Promise<OAuthStored> {
281
310
  const json = await postJson(TOKEN_URL, {
282
- grant_type: 'refresh_token',
311
+ grant_type: "refresh_token",
283
312
  client_id: CLIENT_ID,
284
313
  refresh_token: refreshToken,
285
- })
286
- const data = parseTokenResponse(json)
314
+ });
315
+ const data = parseTokenResponse(json);
287
316
  return {
288
- type: 'oauth',
317
+ type: "oauth",
289
318
  refresh: data.refresh_token,
290
319
  access: data.access_token,
291
320
  expires: tokenExpiry(data.expires_in),
292
- }
321
+ };
293
322
  }
294
323
 
295
324
  async function createApiKey(accessToken: string): Promise<ApiKeySuccess> {
296
325
  const responseText = await requestText(CREATE_API_KEY_URL, {
297
- method: 'POST',
326
+ method: "POST",
298
327
  headers: {
299
- Accept: 'application/json',
328
+ Accept: "application/json",
300
329
  authorization: `Bearer ${accessToken}`,
301
- 'Content-Type': 'application/json',
330
+ "Content-Type": "application/json",
302
331
  },
303
- })
304
- const json = JSON.parse(responseText) as { raw_key: string }
305
- return { type: 'success', key: json.raw_key }
332
+ });
333
+ const json = JSON.parse(responseText) as { raw_key: string };
334
+ return { type: "success", key: json.raw_key };
306
335
  }
307
336
 
308
337
  async function fetchAnthropicAccountIdentity(accessToken: string) {
309
- const urls = [CLIENT_DATA_URL, PROFILE_URL]
338
+ const urls = [CLIENT_DATA_URL, PROFILE_URL];
310
339
  for (const url of urls) {
311
340
  const responseText = await requestText(url, {
312
- method: 'GET',
341
+ method: "GET",
313
342
  headers: {
314
- Accept: 'application/json',
343
+ Accept: "application/json",
315
344
  authorization: `Bearer ${accessToken}`,
316
- 'user-agent': process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
317
- 'x-app': 'cli',
345
+ "user-agent":
346
+ process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
347
+ `claude-cli/${CLAUDE_CODE_VERSION}`,
348
+ "x-app": "cli",
318
349
  },
319
350
  }).catch(() => {
320
- return undefined
321
- })
322
- if (!responseText) continue
323
- const parsed = JSON.parse(responseText) as unknown
324
- const identity = extractAnthropicAccountIdentity(parsed)
325
- if (identity) return identity
351
+ return undefined;
352
+ });
353
+ if (!responseText) continue;
354
+ const parsed = JSON.parse(responseText) as unknown;
355
+ const identity = extractAnthropicAccountIdentity(parsed);
356
+ if (identity) return identity;
326
357
  }
327
- return undefined
358
+ return undefined;
328
359
  }
329
360
 
330
361
  // --- Localhost callback server ---
331
362
 
332
- type CallbackResult = { code: string; state: string }
363
+ type CallbackResult = { code: string; state: string };
333
364
 
334
365
  async function startCallbackServer(expectedState: string) {
335
366
  return new Promise<{
336
- server: Server
337
- cancelWait: () => void
338
- waitForCode: () => Promise<CallbackResult | null>
367
+ server: Server;
368
+ cancelWait: () => void;
369
+ waitForCode: () => Promise<CallbackResult | null>;
339
370
  }>((resolve, reject) => {
340
- let settle: ((value: CallbackResult | null) => void) | undefined
341
- let settled = false
371
+ let settle: ((value: CallbackResult | null) => void) | undefined;
372
+ let settled = false;
342
373
  const waitPromise = new Promise<CallbackResult | null>((res) => {
343
374
  settle = (v) => {
344
- if (settled) return
345
- settled = true
346
- res(v)
347
- }
348
- })
375
+ if (settled) return;
376
+ settled = true;
377
+ res(v);
378
+ };
379
+ });
349
380
 
350
381
  const server = createServer((req, res) => {
351
382
  try {
352
- const url = new URL(req.url || '', 'http://localhost')
383
+ const url = new URL(req.url || "", "http://localhost");
353
384
  if (url.pathname !== CALLBACK_PATH) {
354
- res.writeHead(404).end('Not found')
355
- return
385
+ res.writeHead(404).end("Not found");
386
+ return;
356
387
  }
357
- const code = url.searchParams.get('code')
358
- const state = url.searchParams.get('state')
359
- const error = url.searchParams.get('error')
388
+ const code = url.searchParams.get("code");
389
+ const state = url.searchParams.get("state");
390
+ const error = url.searchParams.get("error");
360
391
  if (error || !code || !state || state !== expectedState) {
361
- res.writeHead(400).end('Authentication failed: ' + (error || 'missing code/state'))
362
- return
392
+ res
393
+ .writeHead(400)
394
+ .end("Authentication failed: " + (error || "missing code/state"));
395
+ return;
363
396
  }
364
397
  res
365
- .writeHead(200, { 'Content-Type': 'text/plain' })
366
- .end('Authentication successful. You can close this window.')
367
- settle?.({ code, state })
398
+ .writeHead(200, { "Content-Type": "text/plain" })
399
+ .end("Authentication successful. You can close this window.");
400
+ settle?.({ code, state });
368
401
  } catch {
369
- res.writeHead(500).end('Internal error')
402
+ res.writeHead(500).end("Internal error");
370
403
  }
371
- })
404
+ });
372
405
 
373
- server.once('error', reject)
374
- server.listen(CALLBACK_PORT, '127.0.0.1', () => {
406
+ server.once("error", reject);
407
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
375
408
  resolve({
376
409
  server,
377
410
  cancelWait: () => {
378
- settle?.(null)
411
+ settle?.(null);
379
412
  },
380
413
  waitForCode: () => waitPromise,
381
- })
382
- })
383
- })
414
+ });
415
+ });
416
+ });
384
417
  }
385
418
 
386
419
  function closeServer(server: Server) {
387
420
  return new Promise<void>((resolve) => {
388
421
  server.close(() => {
389
- resolve()
390
- })
391
- })
422
+ resolve();
423
+ });
424
+ });
392
425
  }
393
426
 
394
427
  // --- Authorization flow ---
@@ -396,25 +429,25 @@ function closeServer(server: Server) {
396
429
  // then waitForCallback handles both auto (localhost) and manual (pasted code) paths.
397
430
 
398
431
  async function beginAuthorizationFlow() {
399
- const pkce = await generatePKCE()
400
- const callbackServer = await startCallbackServer(pkce.verifier)
432
+ const pkce = await generatePKCE();
433
+ const callbackServer = await startCallbackServer(pkce.verifier);
401
434
 
402
435
  const authParams = new URLSearchParams({
403
- code: 'true',
436
+ code: "true",
404
437
  client_id: CLIENT_ID,
405
- response_type: 'code',
438
+ response_type: "code",
406
439
  redirect_uri: REDIRECT_URI,
407
440
  scope: SCOPES,
408
441
  code_challenge: pkce.challenge,
409
- code_challenge_method: 'S256',
442
+ code_challenge_method: "S256",
410
443
  state: pkce.verifier,
411
- })
444
+ });
412
445
 
413
446
  return {
414
447
  url: `https://claude.ai/oauth/authorize?${authParams.toString()}`,
415
448
  verifier: pkce.verifier,
416
449
  callbackServer,
417
- }
450
+ };
418
451
  }
419
452
 
420
453
  async function waitForCallback(
@@ -427,16 +460,16 @@ async function waitForCallback(
427
460
  callbackServer.waitForCode(),
428
461
  new Promise<null>((r) => {
429
462
  setTimeout(() => {
430
- r(null)
431
- }, 50)
463
+ r(null);
464
+ }, 50);
432
465
  }),
433
- ])
434
- if (quick?.code) return quick
466
+ ]);
467
+ if (quick?.code) return quick;
435
468
 
436
469
  // If manual input was provided, parse it
437
- const trimmed = manualInput?.trim()
470
+ const trimmed = manualInput?.trim();
438
471
  if (trimmed) {
439
- return parseManualInput(trimmed)
472
+ return parseManualInput(trimmed);
440
473
  }
441
474
 
442
475
  // Wait for localhost callback with timeout
@@ -444,108 +477,111 @@ async function waitForCallback(
444
477
  callbackServer.waitForCode(),
445
478
  new Promise<null>((r) => {
446
479
  setTimeout(() => {
447
- r(null)
448
- }, OAUTH_TIMEOUT_MS)
480
+ r(null);
481
+ }, OAUTH_TIMEOUT_MS);
449
482
  }),
450
- ])
483
+ ]);
451
484
  if (!result?.code) {
452
- throw new Error('Timed out waiting for OAuth callback')
485
+ throw new Error("Timed out waiting for OAuth callback");
453
486
  }
454
- return result
487
+ return result;
455
488
  } finally {
456
- callbackServer.cancelWait()
457
- await closeServer(callbackServer.server)
489
+ callbackServer.cancelWait();
490
+ await closeServer(callbackServer.server);
458
491
  }
459
492
  }
460
493
 
461
494
  function parseManualInput(input: string): CallbackResult {
462
495
  try {
463
- const url = new URL(input)
464
- const code = url.searchParams.get('code')
465
- const state = url.searchParams.get('state')
466
- if (code) return { code, state: state || '' }
496
+ const url = new URL(input);
497
+ const code = url.searchParams.get("code");
498
+ const state = url.searchParams.get("state");
499
+ if (code) return { code, state: state || "" };
467
500
  } catch {
468
501
  // not a URL
469
502
  }
470
- if (input.includes('#')) {
471
- const [code = '', state = ''] = input.split('#', 2)
472
- return { code, state }
503
+ if (input.includes("#")) {
504
+ const [code = "", state = ""] = input.split("#", 2);
505
+ return { code, state };
473
506
  }
474
- if (input.includes('code=')) {
475
- const params = new URLSearchParams(input)
476
- const code = params.get('code')
477
- if (code) return { code, state: params.get('state') || '' }
507
+ if (input.includes("code=")) {
508
+ const params = new URLSearchParams(input);
509
+ const code = params.get("code");
510
+ if (code) return { code, state: params.get("state") || "" };
478
511
  }
479
- return { code: input, state: '' }
512
+ return { code: input, state: "" };
480
513
  }
481
514
 
482
515
  // Unified authorize handler: returns either OAuth tokens or an API key,
483
516
  // for both auto and remote-first modes.
484
- function buildAuthorizeHandler(mode: 'oauth' | 'apikey') {
517
+ function buildAuthorizeHandler(mode: "oauth" | "apikey") {
485
518
  return async () => {
486
- const auth = await beginAuthorizationFlow()
487
- const isRemote = Boolean(process.env.KIMAKI)
488
- let pendingAuthResult: Promise<AuthResult> | undefined
519
+ const auth = await beginAuthorizationFlow();
520
+ const isRemote = Boolean(process.env.KIMAKI);
521
+ let pendingAuthResult: Promise<AuthResult> | undefined;
489
522
 
490
523
  const finalize = async (result: CallbackResult): Promise<AuthResult> => {
491
- const verifier = auth.verifier
524
+ const verifier = auth.verifier;
492
525
  const creds = await exchangeAuthorizationCode(
493
526
  result.code,
494
527
  result.state || verifier,
495
528
  verifier,
496
529
  REDIRECT_URI,
497
- )
498
- if (mode === 'apikey') {
499
- return createApiKey(creds.access)
530
+ );
531
+ if (mode === "apikey") {
532
+ return createApiKey(creds.access);
500
533
  }
501
- const identity = await fetchAnthropicAccountIdentity(creds.access)
502
- await rememberAnthropicOAuth({
503
- type: 'oauth',
504
- refresh: creds.refresh,
505
- access: creds.access,
506
- expires: creds.expires,
507
- }, identity)
508
- return creds
509
- }
534
+ const identity = await fetchAnthropicAccountIdentity(creds.access);
535
+ await rememberAnthropicOAuth(
536
+ {
537
+ type: "oauth",
538
+ refresh: creds.refresh,
539
+ access: creds.access,
540
+ expires: creds.expires,
541
+ },
542
+ identity,
543
+ );
544
+ return creds;
545
+ };
510
546
 
511
547
  if (!isRemote) {
512
548
  return {
513
549
  url: auth.url,
514
550
  instructions:
515
- 'Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.',
516
- method: 'auto' as const,
551
+ "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
552
+ method: "auto" as const,
517
553
  callback: async (): Promise<AuthResult> => {
518
554
  pendingAuthResult ??= (async () => {
519
555
  try {
520
- const result = await waitForCallback(auth.callbackServer)
521
- return await finalize(result)
556
+ const result = await waitForCallback(auth.callbackServer);
557
+ return await finalize(result);
522
558
  } catch {
523
- return { type: 'failed' }
559
+ return { type: "failed" };
524
560
  }
525
- })()
526
- return pendingAuthResult
561
+ })();
562
+ return pendingAuthResult;
527
563
  },
528
- }
564
+ };
529
565
  }
530
566
 
531
567
  return {
532
568
  url: auth.url,
533
569
  instructions:
534
- 'Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.',
535
- method: 'code' as const,
570
+ "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
571
+ method: "code" as const,
536
572
  callback: async (input: string): Promise<AuthResult> => {
537
573
  pendingAuthResult ??= (async () => {
538
574
  try {
539
- const result = await waitForCallback(auth.callbackServer, input)
540
- return await finalize(result)
575
+ const result = await waitForCallback(auth.callbackServer, input);
576
+ return await finalize(result);
541
577
  } catch {
542
- return { type: 'failed' }
578
+ return { type: "failed" };
543
579
  }
544
- })()
545
- return pendingAuthResult
580
+ })();
581
+ return pendingAuthResult;
546
582
  },
547
- }
548
- }
583
+ };
584
+ };
549
585
  }
550
586
 
551
587
  // --- Request/response rewriting ---
@@ -553,399 +589,484 @@ function buildAuthorizeHandler(mode: 'oauth' | 'apikey') {
553
589
  // and reverses the mapping in streamed responses.
554
590
 
555
591
  function toClaudeCodeToolName(name: string) {
556
- return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name
592
+ return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name;
557
593
  }
558
594
 
559
- function sanitizeSystemText(text: string, onError?: (msg: string) => void) {
560
- const startIdx = text.indexOf(OPENCODE_IDENTITY)
561
- if (startIdx === -1) return text
562
- // to find the last heading to match readhttps://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/anthropic.txt
563
- // it contains the opencode injected prompt. you must keep the codeRefsMarker updated with that package
564
- const codeRefsMarker = '# Code References'
565
- const endIdx = text.indexOf(codeRefsMarker, startIdx)
595
+ function sanitizeAnthropicSystemText(
596
+ text: string,
597
+ onError?: (msg: string) => void,
598
+ ) {
599
+ const startIdx = text.indexOf(OPENCODE_IDENTITY);
600
+ if (startIdx === -1) return text;
601
+
602
+ // Keep the marker aligned with the current OpenCode Anthropic prompt.
603
+ const endIdx = text.indexOf(ANTHROPIC_PROMPT_MARKER, startIdx);
566
604
  if (endIdx === -1) {
567
- onError?.(`sanitizeSystemText: could not find '# Code References' after OpenCode identity`)
568
- return text
605
+ onError?.(
606
+ "sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity",
607
+ );
608
+ return text;
569
609
  }
570
- // Remove everything from the OpenCode identity up to (but not including) '# Code References'
571
- return text.slice(0, startIdx) + text.slice(endIdx)
610
+
611
+ return (text.slice(0, startIdx) + text.slice(endIdx)).replaceAll(
612
+ "opencode",
613
+ "openc0de",
614
+ );
572
615
  }
573
616
 
574
- function prependClaudeCodeIdentity(system: unknown, onError?: (msg: string) => void) {
575
- const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY }
617
+ function mapSystemTextPart(
618
+ part: unknown,
619
+ onError?: (msg: string) => void,
620
+ ): unknown {
621
+ if (typeof part === "string") {
622
+ return { type: "text", text: sanitizeAnthropicSystemText(part, onError) };
623
+ }
576
624
 
577
- if (typeof system === 'undefined') return [identityBlock]
625
+ if (
626
+ part &&
627
+ typeof part === "object" &&
628
+ "type" in part &&
629
+ part.type === "text" &&
630
+ "text" in part &&
631
+ typeof part.text === "string"
632
+ ) {
633
+ return {
634
+ ...part,
635
+ text: sanitizeAnthropicSystemText(part.text, onError),
636
+ };
637
+ }
638
+
639
+ return part;
640
+ }
578
641
 
579
- if (typeof system === 'string') {
580
- const sanitized = sanitizeSystemText(system, onError)
581
- if (sanitized === CLAUDE_CODE_IDENTITY) return [identityBlock]
582
- return [identityBlock, { type: 'text', text: sanitized }]
642
+
643
+ function prependClaudeCodeIdentity(
644
+ system: unknown,
645
+ onError?: (msg: string) => void,
646
+ ) {
647
+ const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
648
+
649
+ if (typeof system === "undefined") return [identityBlock];
650
+
651
+ if (typeof system === "string") {
652
+ const sanitized = sanitizeAnthropicSystemText(system, onError);
653
+ if (sanitized === CLAUDE_CODE_IDENTITY) return [identityBlock];
654
+ return [identityBlock, { type: "text", text: sanitized }];
583
655
  }
584
656
 
585
- if (!Array.isArray(system)) return [identityBlock, system]
657
+ if (!Array.isArray(system)) return [identityBlock, system];
586
658
 
587
659
  const sanitized = system.map((item) => {
588
- if (typeof item === 'string') return { type: 'text', text: sanitizeSystemText(item, onError) }
589
- if (item && typeof item === 'object' && (item as { type?: unknown }).type === 'text') {
590
- const text = (item as { text?: unknown }).text
591
- if (typeof text === 'string') {
592
- return { ...(item as Record<string, unknown>), text: sanitizeSystemText(text, onError) }
593
- }
594
- }
595
- return item
596
- })
660
+ return mapSystemTextPart(item, onError);
661
+ });
597
662
 
598
- const first = sanitized[0]
663
+ const first = sanitized[0];
599
664
  if (
600
665
  first &&
601
- typeof first === 'object' &&
602
- (first as { type?: unknown }).type === 'text' &&
603
- (first as { text?: unknown }).text === CLAUDE_CODE_IDENTITY
666
+ typeof first === "object" &&
667
+ "type" in first &&
668
+ first.type === "text" &&
669
+ "text" in first &&
670
+ first.text === CLAUDE_CODE_IDENTITY
604
671
  ) {
605
- return sanitized
672
+ return sanitized;
606
673
  }
607
- return [identityBlock, ...sanitized]
674
+ return [identityBlock, ...sanitized];
608
675
  }
609
676
 
610
- function rewriteRequestPayload(body: string | undefined, onError?: (msg: string) => void) {
611
- if (!body) return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
677
+ function rewriteRequestPayload(
678
+ body: string | undefined,
679
+ onError?: (msg: string) => void,
680
+ ) {
681
+ if (!body)
682
+ return {
683
+ body,
684
+ modelId: undefined,
685
+ reverseToolNameMap: new Map<string, string>(),
686
+ };
612
687
 
613
688
  try {
614
- const payload = JSON.parse(body) as Record<string, unknown>
615
- const reverseToolNameMap = new Map<string, string>()
616
- const modelId = typeof payload.model === 'string' ? payload.model : undefined
689
+ const payload = JSON.parse(body) as Record<string, unknown>;
690
+ const reverseToolNameMap = new Map<string, string>();
691
+ const modelId =
692
+ typeof payload.model === "string" ? payload.model : undefined;
617
693
 
618
694
  // Build reverse map and rename tools
619
695
  if (Array.isArray(payload.tools)) {
620
696
  payload.tools = payload.tools.map((tool) => {
621
- if (!tool || typeof tool !== 'object') return tool
622
- const name = (tool as { name?: unknown }).name
623
- if (typeof name !== 'string') return tool
624
- const mapped = toClaudeCodeToolName(name)
625
- reverseToolNameMap.set(mapped, name)
626
- return { ...(tool as Record<string, unknown>), name: mapped }
627
- })
697
+ if (!tool || typeof tool !== "object") return tool;
698
+ const name = (tool as { name?: unknown }).name;
699
+ if (typeof name !== "string") return tool;
700
+ const mapped = toClaudeCodeToolName(name);
701
+ reverseToolNameMap.set(mapped, name);
702
+ return { ...(tool as Record<string, unknown>), name: mapped };
703
+ });
628
704
  }
629
705
 
630
706
  // Rename system prompt
631
- payload.system = prependClaudeCodeIdentity(payload.system, onError)
707
+ payload.system = prependClaudeCodeIdentity(payload.system, onError);
632
708
 
633
709
  // Rename tool_choice
634
710
  if (
635
711
  payload.tool_choice &&
636
- typeof payload.tool_choice === 'object' &&
637
- (payload.tool_choice as { type?: unknown }).type === 'tool'
712
+ typeof payload.tool_choice === "object" &&
713
+ (payload.tool_choice as { type?: unknown }).type === "tool"
638
714
  ) {
639
- const name = (payload.tool_choice as { name?: unknown }).name
640
- if (typeof name === 'string') {
715
+ const name = (payload.tool_choice as { name?: unknown }).name;
716
+ if (typeof name === "string") {
641
717
  payload.tool_choice = {
642
718
  ...(payload.tool_choice as Record<string, unknown>),
643
719
  name: toClaudeCodeToolName(name),
644
- }
720
+ };
645
721
  }
646
722
  }
647
723
 
648
724
  // Rename tool_use blocks in messages
649
725
  if (Array.isArray(payload.messages)) {
650
726
  payload.messages = payload.messages.map((message) => {
651
- if (!message || typeof message !== 'object') return message
652
- const content = (message as { content?: unknown }).content
653
- if (!Array.isArray(content)) return message
727
+ if (!message || typeof message !== "object") return message;
728
+ const content = (message as { content?: unknown }).content;
729
+ if (!Array.isArray(content)) return message;
654
730
  return {
655
731
  ...(message as Record<string, unknown>),
656
732
  content: content.map((block) => {
657
- if (!block || typeof block !== 'object') return block
658
- const b = block as { type?: unknown; name?: unknown }
659
- if (b.type !== 'tool_use' || typeof b.name !== 'string') return block
660
- return { ...(block as Record<string, unknown>), name: toClaudeCodeToolName(b.name) }
733
+ if (!block || typeof block !== "object") return block;
734
+ const b = block as { type?: unknown; name?: unknown };
735
+ if (b.type !== "tool_use" || typeof b.name !== "string")
736
+ return block;
737
+ return {
738
+ ...(block as Record<string, unknown>),
739
+ name: toClaudeCodeToolName(b.name),
740
+ };
661
741
  }),
662
- }
663
- })
742
+ };
743
+ });
664
744
  }
665
745
 
666
- return { body: JSON.stringify(payload), modelId, reverseToolNameMap }
746
+ return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
667
747
  } catch {
668
- return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
748
+ return {
749
+ body,
750
+ modelId: undefined,
751
+ reverseToolNameMap: new Map<string, string>(),
752
+ };
669
753
  }
670
754
  }
671
755
 
672
- function wrapResponseStream(response: Response, reverseToolNameMap: Map<string, string>) {
673
- if (!response.body || reverseToolNameMap.size === 0) return response
756
+ function wrapResponseStream(
757
+ response: Response,
758
+ reverseToolNameMap: Map<string, string>,
759
+ ) {
760
+ if (!response.body || reverseToolNameMap.size === 0) return response;
674
761
 
675
- const reader = response.body.getReader()
676
- const decoder = new TextDecoder()
677
- const encoder = new TextEncoder()
678
- let carry = ''
762
+ const reader = response.body.getReader();
763
+ const decoder = new TextDecoder();
764
+ const encoder = new TextEncoder();
765
+ let carry = "";
679
766
 
680
767
  const transform = (text: string) => {
681
768
  return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name: string) => {
682
- const original = reverseToolNameMap.get(name)
683
- return original ? full.replace(`"${name}"`, `"${original}"`) : full
684
- })
685
- }
769
+ const original = reverseToolNameMap.get(name);
770
+ return original ? full.replace(`"${name}"`, `"${original}"`) : full;
771
+ });
772
+ };
686
773
 
687
774
  const stream = new ReadableStream<Uint8Array>({
688
775
  async pull(controller) {
689
- const { done, value } = await reader.read()
776
+ const { done, value } = await reader.read();
690
777
  if (done) {
691
- const finalText = carry + decoder.decode()
692
- if (finalText) controller.enqueue(encoder.encode(transform(finalText)))
693
- controller.close()
694
- return
778
+ const finalText = carry + decoder.decode();
779
+ if (finalText) controller.enqueue(encoder.encode(transform(finalText)));
780
+ controller.close();
781
+ return;
695
782
  }
696
- carry += decoder.decode(value, { stream: true })
783
+ carry += decoder.decode(value, { stream: true });
697
784
  // Buffer 256 chars to avoid splitting JSON keys across chunks
698
- if (carry.length <= 256) return
699
- const output = carry.slice(0, -256)
700
- carry = carry.slice(-256)
701
- controller.enqueue(encoder.encode(transform(output)))
785
+ if (carry.length <= 256) return;
786
+ const output = carry.slice(0, -256);
787
+ carry = carry.slice(-256);
788
+ controller.enqueue(encoder.encode(transform(output)));
702
789
  },
703
790
  async cancel(reason) {
704
- await reader.cancel(reason)
791
+ await reader.cancel(reason);
705
792
  },
706
- })
793
+ });
707
794
 
708
795
  return new Response(stream, {
709
796
  status: response.status,
710
797
  statusText: response.statusText,
711
798
  headers: response.headers,
712
- })
799
+ });
713
800
  }
714
801
 
715
802
  function appendToastSessionMarker({
716
803
  message,
717
804
  sessionId,
718
805
  }: {
719
- message: string
720
- sessionId: string | undefined
806
+ message: string;
807
+ sessionId: string | undefined;
721
808
  }) {
722
809
  if (!sessionId) {
723
- return message
810
+ return message;
724
811
  }
725
- return `${message} ${sessionId}`
812
+ return `${message} ${sessionId}`;
726
813
  }
727
814
 
728
815
  // --- Beta headers ---
729
816
 
730
817
  function getRequiredBetas(modelId: string | undefined) {
731
- const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA]
818
+ const betas = [
819
+ CLAUDE_CODE_BETA,
820
+ OAUTH_BETA,
821
+ FINE_GRAINED_TOOL_STREAMING_BETA,
822
+ ];
732
823
  const isAdaptive =
733
- modelId?.includes('opus-4-6') ||
734
- modelId?.includes('opus-4.6') ||
735
- modelId?.includes('sonnet-4-6') ||
736
- modelId?.includes('sonnet-4.6')
737
- if (!isAdaptive) betas.push(INTERLEAVED_THINKING_BETA)
738
- return betas
824
+ modelId?.includes("opus-4-6") ||
825
+ modelId?.includes("opus-4.6") ||
826
+ modelId?.includes("sonnet-4-6") ||
827
+ modelId?.includes("sonnet-4.6");
828
+ if (!isAdaptive) betas.push(INTERLEAVED_THINKING_BETA);
829
+ return betas;
739
830
  }
740
831
 
741
832
  function mergeBetas(existing: string | null, required: string[]) {
742
833
  return [
743
834
  ...new Set([
744
835
  ...required,
745
- ...(existing || '')
746
- .split(',')
836
+ ...(existing || "")
837
+ .split(",")
747
838
  .map((s) => s.trim())
748
839
  .filter(Boolean),
749
840
  ]),
750
- ].join(',')
841
+ ].join(",");
751
842
  }
752
843
 
753
844
  // --- Token refresh with dedup ---
754
845
 
755
846
  function isOAuthStored(auth: { type: string }): auth is OAuthStored {
756
- return auth.type === 'oauth'
847
+ return auth.type === "oauth";
757
848
  }
758
849
 
759
850
  async function getFreshOAuth(
760
851
  getAuth: () => Promise<OAuthStored | { type: string }>,
761
- client: Parameters<Plugin>[0]['client'],
852
+ client: Parameters<Plugin>[0]["client"],
762
853
  ) {
763
- const auth = await getAuth()
764
- if (!isOAuthStored(auth)) return undefined
765
- if (auth.access && auth.expires > Date.now()) return auth
854
+ const auth = await getAuth();
855
+ if (!isOAuthStored(auth)) return undefined;
856
+ if (auth.access && auth.expires > Date.now()) return auth;
766
857
 
767
- const pending = pendingRefresh.get(auth.refresh)
858
+ const pending = pendingRefresh.get(auth.refresh);
768
859
  if (pending) {
769
- return pending
860
+ return pending;
770
861
  }
771
862
 
772
863
  const refreshPromise = withAuthStateLock(async () => {
773
- const latest = await getAuth()
864
+ const latest = await getAuth();
774
865
  if (!isOAuthStored(latest)) {
775
- throw new Error('Anthropic OAuth credentials disappeared during refresh')
866
+ throw new Error("Anthropic OAuth credentials disappeared during refresh");
776
867
  }
777
- if (latest.access && latest.expires > Date.now()) return latest
868
+ if (latest.access && latest.expires > Date.now()) return latest;
778
869
 
779
- const refreshed = await refreshAnthropicToken(latest.refresh)
780
- await setAnthropicAuth(refreshed, client)
781
- const store = await loadAccountStore()
870
+ const refreshed = await refreshAnthropicToken(latest.refresh);
871
+ await setAnthropicAuth(refreshed, client);
872
+ const store = await loadAccountStore();
782
873
  if (store.accounts.length > 0) {
783
874
  const identity: AnthropicAccountIdentity | undefined = (() => {
784
875
  const currentIndex = store.accounts.findIndex((account) => {
785
- return account.refresh === latest.refresh || account.access === latest.access
786
- })
787
- const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined
788
- if (!current) return undefined
876
+ return (
877
+ account.refresh === latest.refresh ||
878
+ account.access === latest.access
879
+ );
880
+ });
881
+ const current =
882
+ currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
883
+ if (!current) return undefined;
789
884
  return {
790
885
  ...(current.email ? { email: current.email } : {}),
791
886
  ...(current.accountId ? { accountId: current.accountId } : {}),
792
- }
793
- })()
794
- upsertAccount(store, { ...refreshed, ...identity })
795
- await saveAccountStore(store)
887
+ };
888
+ })();
889
+ upsertAccount(store, { ...refreshed, ...identity });
890
+ await saveAccountStore(store);
796
891
  }
797
- return refreshed
798
- })
799
- pendingRefresh.set(auth.refresh, refreshPromise)
892
+ return refreshed;
893
+ });
894
+ pendingRefresh.set(auth.refresh, refreshPromise);
800
895
  return refreshPromise.finally(() => {
801
- pendingRefresh.delete(auth.refresh)
802
- })
896
+ pendingRefresh.delete(auth.refresh);
897
+ });
803
898
  }
804
899
 
805
- // --- Plugin export ---
806
-
807
900
  const AnthropicAuthPlugin: Plugin = async ({ client }) => {
808
901
  return {
809
- 'chat.headers': async (input, output) => {
810
- if (input.model.providerID !== 'anthropic') {
811
- return
902
+ "chat.headers": async (input, output) => {
903
+ if (input.model.providerID !== "anthropic") {
904
+ return;
812
905
  }
813
- output.headers[TOAST_SESSION_HEADER] = input.sessionID
906
+ output.headers[TOAST_SESSION_HEADER] = input.sessionID;
814
907
  },
815
908
  auth: {
816
- provider: 'anthropic',
909
+ provider: "anthropic",
817
910
  async loader(
818
911
  getAuth: () => Promise<OAuthStored | { type: string }>,
819
912
  provider: { models: Record<string, { cost?: unknown }> },
820
913
  ) {
821
- const auth = await getAuth()
822
- if (auth.type !== 'oauth') return {}
914
+ const auth = await getAuth();
915
+ if (auth.type !== "oauth") return {};
823
916
 
824
917
  // Zero out costs for OAuth users (Claude Pro/Max subscription)
825
918
  for (const model of Object.values(provider.models)) {
826
- model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
919
+ model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
827
920
  }
828
921
 
829
922
  return {
830
- apiKey: '',
923
+ apiKey: "",
831
924
  async fetch(input: Request | string | URL, init?: RequestInit) {
832
925
  const url = (() => {
833
926
  try {
834
- return new URL(input instanceof Request ? input.url : input.toString())
927
+ return new URL(
928
+ input instanceof Request ? input.url : input.toString(),
929
+ );
835
930
  } catch {
836
- return null
931
+ return null;
837
932
  }
838
- })()
839
- if (!url || !ANTHROPIC_HOSTS.has(url.hostname)) return fetch(input, init)
933
+ })();
934
+ if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
935
+ return fetch(input, init);
840
936
 
841
937
  const originalBody =
842
- typeof init?.body === 'string'
938
+ typeof init?.body === "string"
843
939
  ? init.body
844
940
  : input instanceof Request
845
941
  ? await input
846
942
  .clone()
847
943
  .text()
848
944
  .catch(() => undefined)
849
- : undefined
945
+ : undefined;
850
946
 
851
- const headers = new Headers(init?.headers)
947
+ const headers = new Headers(init?.headers);
852
948
  if (input instanceof Request) {
853
949
  input.headers.forEach((v, k) => {
854
- if (!headers.has(k)) headers.set(k, v)
855
- })
950
+ if (!headers.has(k)) headers.set(k, v);
951
+ });
856
952
  }
857
- const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined
953
+ const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
858
954
 
859
955
  const rewritten = rewriteRequestPayload(originalBody, (msg) => {
860
- client.tui.showToast({
861
- body: {
862
- message: appendToastSessionMarker({ message: msg, sessionId }),
863
- variant: 'error',
864
- },
865
- }).catch(() => {})
866
- })
867
- const betas = getRequiredBetas(rewritten.modelId)
956
+ client.tui
957
+ .showToast({
958
+ body: {
959
+ message: appendToastSessionMarker({
960
+ message: msg,
961
+ sessionId,
962
+ }),
963
+ variant: "error",
964
+ },
965
+ })
966
+ .catch(() => {});
967
+ });
968
+ const betas = getRequiredBetas(rewritten.modelId);
868
969
 
869
970
  const runRequest = async (auth: OAuthStored) => {
870
- const requestHeaders = new Headers(headers)
871
- requestHeaders.delete(TOAST_SESSION_HEADER)
872
- requestHeaders.set('accept', 'application/json')
971
+ const requestHeaders = new Headers(headers);
972
+ requestHeaders.delete(TOAST_SESSION_HEADER);
973
+ requestHeaders.set("accept", "application/json");
974
+ requestHeaders.set(
975
+ "anthropic-beta",
976
+ mergeBetas(requestHeaders.get("anthropic-beta"), betas),
977
+ );
873
978
  requestHeaders.set(
874
- 'anthropic-beta',
875
- mergeBetas(requestHeaders.get('anthropic-beta'), betas),
876
- )
877
- requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true')
878
- requestHeaders.set('authorization', `Bearer ${auth.access}`)
979
+ "anthropic-dangerous-direct-browser-access",
980
+ "true",
981
+ );
982
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
879
983
  requestHeaders.set(
880
- 'user-agent',
881
- process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
882
- )
883
- requestHeaders.set('x-app', 'cli')
884
- requestHeaders.delete('x-api-key')
984
+ "user-agent",
985
+ process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
986
+ `claude-cli/${CLAUDE_CODE_VERSION}`,
987
+ );
988
+ requestHeaders.set("x-app", "cli");
989
+ requestHeaders.delete("x-api-key");
885
990
 
886
991
  return fetch(input, {
887
992
  ...(init ?? {}),
888
993
  body: rewritten.body,
889
994
  headers: requestHeaders,
890
- })
891
- }
995
+ });
996
+ };
892
997
 
893
- const freshAuth = await getFreshOAuth(getAuth, client)
894
- if (!freshAuth) return fetch(input, init)
998
+ const freshAuth = await getFreshOAuth(getAuth, client);
999
+ if (!freshAuth) return fetch(input, init);
895
1000
 
896
- let response = await runRequest(freshAuth)
1001
+ let response = await runRequest(freshAuth);
897
1002
  if (!response.ok) {
898
1003
  const bodyText = await response
899
1004
  .clone()
900
1005
  .text()
901
- .catch(() => '')
1006
+ .catch(() => "");
902
1007
  if (shouldRotateAuth(response.status, bodyText)) {
903
- const rotated = await rotateAnthropicAccount(freshAuth, client)
1008
+ const rotated = await rotateAnthropicAccount(freshAuth, client);
904
1009
  if (rotated) {
905
1010
  // Show toast notification so Discord thread shows the rotation
906
- client.tui.showToast({
907
- body: {
908
- message: appendToastSessionMarker({
909
- message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
910
- sessionId,
911
- }),
912
- variant: 'info',
913
- },
914
-
915
- }).catch(() => {})
916
- const retryAuth = await getFreshOAuth(getAuth, client)
1011
+ client.tui
1012
+ .showToast({
1013
+ body: {
1014
+ message: appendToastSessionMarker({
1015
+ message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
1016
+ sessionId,
1017
+ }),
1018
+ variant: "info",
1019
+ },
1020
+ })
1021
+ .catch(() => {});
1022
+ const retryAuth = await getFreshOAuth(getAuth, client);
917
1023
  if (retryAuth) {
918
- response = await runRequest(retryAuth)
1024
+ response = await runRequest(retryAuth);
919
1025
  }
920
1026
  }
921
1027
  }
922
1028
  }
923
1029
 
924
- return wrapResponseStream(response, rewritten.reverseToolNameMap)
1030
+ return wrapResponseStream(response, rewritten.reverseToolNameMap);
925
1031
  },
926
- }
1032
+ };
927
1033
  },
928
1034
  methods: [
929
1035
  {
930
- label: 'Claude Pro/Max',
931
- type: 'oauth',
932
- authorize: buildAuthorizeHandler('oauth'),
1036
+ label: "Claude Pro/Max",
1037
+ type: "oauth",
1038
+ authorize: buildAuthorizeHandler("oauth"),
933
1039
  },
934
1040
  {
935
- label: 'Create an API Key',
936
- type: 'oauth',
937
- authorize: buildAuthorizeHandler('apikey'),
1041
+ label: "Create an API Key",
1042
+ type: "oauth",
1043
+ authorize: buildAuthorizeHandler("apikey"),
938
1044
  },
939
1045
  {
940
- provider: 'anthropic',
941
- label: 'Manually enter API Key',
942
- type: 'api',
1046
+ provider: "anthropic",
1047
+ label: "Manually enter API Key",
1048
+ type: "api",
943
1049
  },
944
1050
  ],
945
1051
  },
946
- }
947
- }
1052
+ };
1053
+ };
948
1054
 
949
- export {
950
- AnthropicAuthPlugin as anthropicAuthPlugin,
951
- }
1055
+ const replacer: Plugin = async () => {
1056
+ return {
1057
+ "experimental.chat.system.transform": (async (input, output) => {
1058
+ if (input.model.providerID !== "anthropic") return;
1059
+ const textIndex = output.system.findIndex((x) =>
1060
+ x.includes(OPENCODE_IDENTITY),
1061
+ );
1062
+ const text = output.system[textIndex];
1063
+ if (!text) {
1064
+ return;
1065
+ }
1066
+
1067
+ output.system[textIndex] = sanitizeAnthropicSystemText(text);
1068
+ }) satisfies SystemTransformHook,
1069
+ };
1070
+ };
1071
+
1072
+ export { replacer, AnthropicAuthPlugin as anthropicAuthPlugin };