@otto-assistant/bridge 0.4.97 → 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.
Files changed (39) hide show
  1. package/dist/agent-model.e2e.test.js +7 -1
  2. package/dist/anthropic-auth-plugin.js +227 -176
  3. package/dist/cli-send-thread.e2e.test.js +4 -7
  4. package/dist/cli.js +2 -2
  5. package/dist/commands/login.js +6 -4
  6. package/dist/commands/screenshare.js +1 -1
  7. package/dist/commands/screenshare.test.js +2 -2
  8. package/dist/commands/vscode.js +269 -0
  9. package/dist/context-awareness-plugin.js +8 -38
  10. package/dist/db.js +1 -0
  11. package/dist/discord-command-registration.js +5 -0
  12. package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
  13. package/dist/interaction-handler.js +4 -0
  14. package/dist/kimaki-opencode-plugin.js +3 -1
  15. package/dist/memory-overview-plugin.js +126 -0
  16. package/dist/system-message.js +23 -22
  17. package/dist/system-message.test.js +23 -22
  18. package/dist/system-prompt-drift-plugin.js +41 -11
  19. package/dist/utils.js +1 -1
  20. package/package.json +1 -1
  21. package/src/agent-model.e2e.test.ts +8 -1
  22. package/src/anthropic-auth-plugin.ts +574 -451
  23. package/src/cli-send-thread.e2e.test.ts +6 -7
  24. package/src/cli.ts +2 -2
  25. package/src/commands/login.ts +6 -4
  26. package/src/commands/screenshare.test.ts +2 -2
  27. package/src/commands/screenshare.ts +1 -1
  28. package/src/commands/vscode.ts +342 -0
  29. package/src/context-awareness-plugin.ts +11 -42
  30. package/src/db.ts +1 -0
  31. package/src/discord-command-registration.ts +7 -0
  32. package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
  33. package/src/interaction-handler.ts +5 -0
  34. package/src/kimaki-opencode-plugin.ts +3 -1
  35. package/src/memory-overview-plugin.ts +161 -0
  36. package/src/system-message.test.ts +23 -22
  37. package/src/system-message.ts +23 -22
  38. package/src/system-prompt-drift-plugin.ts +48 -12
  39. package/src/utils.ts +1 -1
@@ -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,397 +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
- const codeRefsMarker = '# Code References'
563
- 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);
564
604
  if (endIdx === -1) {
565
- onError?.(`sanitizeSystemText: could not find '# Code References' after OpenCode identity`)
566
- return text
605
+ onError?.(
606
+ "sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity",
607
+ );
608
+ return text;
567
609
  }
568
- // Remove everything from the OpenCode identity up to (but not including) '# Code References'
569
- return text.slice(0, startIdx) + text.slice(endIdx)
610
+
611
+ return (text.slice(0, startIdx) + text.slice(endIdx)).replaceAll(
612
+ "opencode",
613
+ "openc0de",
614
+ );
570
615
  }
571
616
 
572
- function prependClaudeCodeIdentity(system: unknown, onError?: (msg: string) => void) {
573
- 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
+ }
574
624
 
575
- 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
+ }
576
641
 
577
- if (typeof system === 'string') {
578
- const sanitized = sanitizeSystemText(system, onError)
579
- if (sanitized === CLAUDE_CODE_IDENTITY) return [identityBlock]
580
- 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 }];
581
655
  }
582
656
 
583
- if (!Array.isArray(system)) return [identityBlock, system]
657
+ if (!Array.isArray(system)) return [identityBlock, system];
584
658
 
585
659
  const sanitized = system.map((item) => {
586
- if (typeof item === 'string') return { type: 'text', text: sanitizeSystemText(item, onError) }
587
- if (item && typeof item === 'object' && (item as { type?: unknown }).type === 'text') {
588
- const text = (item as { text?: unknown }).text
589
- if (typeof text === 'string') {
590
- return { ...(item as Record<string, unknown>), text: sanitizeSystemText(text, onError) }
591
- }
592
- }
593
- return item
594
- })
660
+ return mapSystemTextPart(item, onError);
661
+ });
595
662
 
596
- const first = sanitized[0]
663
+ const first = sanitized[0];
597
664
  if (
598
665
  first &&
599
- typeof first === 'object' &&
600
- (first as { type?: unknown }).type === 'text' &&
601
- (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
602
671
  ) {
603
- return sanitized
672
+ return sanitized;
604
673
  }
605
- return [identityBlock, ...sanitized]
674
+ return [identityBlock, ...sanitized];
606
675
  }
607
676
 
608
- function rewriteRequestPayload(body: string | undefined, onError?: (msg: string) => void) {
609
- 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
+ };
610
687
 
611
688
  try {
612
- const payload = JSON.parse(body) as Record<string, unknown>
613
- const reverseToolNameMap = new Map<string, string>()
614
- 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;
615
693
 
616
694
  // Build reverse map and rename tools
617
695
  if (Array.isArray(payload.tools)) {
618
696
  payload.tools = payload.tools.map((tool) => {
619
- if (!tool || typeof tool !== 'object') return tool
620
- const name = (tool as { name?: unknown }).name
621
- if (typeof name !== 'string') return tool
622
- const mapped = toClaudeCodeToolName(name)
623
- reverseToolNameMap.set(mapped, name)
624
- return { ...(tool as Record<string, unknown>), name: mapped }
625
- })
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
+ });
626
704
  }
627
705
 
628
706
  // Rename system prompt
629
- payload.system = prependClaudeCodeIdentity(payload.system, onError)
707
+ payload.system = prependClaudeCodeIdentity(payload.system, onError);
630
708
 
631
709
  // Rename tool_choice
632
710
  if (
633
711
  payload.tool_choice &&
634
- typeof payload.tool_choice === 'object' &&
635
- (payload.tool_choice as { type?: unknown }).type === 'tool'
712
+ typeof payload.tool_choice === "object" &&
713
+ (payload.tool_choice as { type?: unknown }).type === "tool"
636
714
  ) {
637
- const name = (payload.tool_choice as { name?: unknown }).name
638
- if (typeof name === 'string') {
715
+ const name = (payload.tool_choice as { name?: unknown }).name;
716
+ if (typeof name === "string") {
639
717
  payload.tool_choice = {
640
718
  ...(payload.tool_choice as Record<string, unknown>),
641
719
  name: toClaudeCodeToolName(name),
642
- }
720
+ };
643
721
  }
644
722
  }
645
723
 
646
724
  // Rename tool_use blocks in messages
647
725
  if (Array.isArray(payload.messages)) {
648
726
  payload.messages = payload.messages.map((message) => {
649
- if (!message || typeof message !== 'object') return message
650
- const content = (message as { content?: unknown }).content
651
- 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;
652
730
  return {
653
731
  ...(message as Record<string, unknown>),
654
732
  content: content.map((block) => {
655
- if (!block || typeof block !== 'object') return block
656
- const b = block as { type?: unknown; name?: unknown }
657
- if (b.type !== 'tool_use' || typeof b.name !== 'string') return block
658
- 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
+ };
659
741
  }),
660
- }
661
- })
742
+ };
743
+ });
662
744
  }
663
745
 
664
- return { body: JSON.stringify(payload), modelId, reverseToolNameMap }
746
+ return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
665
747
  } catch {
666
- return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
748
+ return {
749
+ body,
750
+ modelId: undefined,
751
+ reverseToolNameMap: new Map<string, string>(),
752
+ };
667
753
  }
668
754
  }
669
755
 
670
- function wrapResponseStream(response: Response, reverseToolNameMap: Map<string, string>) {
671
- 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;
672
761
 
673
- const reader = response.body.getReader()
674
- const decoder = new TextDecoder()
675
- const encoder = new TextEncoder()
676
- let carry = ''
762
+ const reader = response.body.getReader();
763
+ const decoder = new TextDecoder();
764
+ const encoder = new TextEncoder();
765
+ let carry = "";
677
766
 
678
767
  const transform = (text: string) => {
679
768
  return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name: string) => {
680
- const original = reverseToolNameMap.get(name)
681
- return original ? full.replace(`"${name}"`, `"${original}"`) : full
682
- })
683
- }
769
+ const original = reverseToolNameMap.get(name);
770
+ return original ? full.replace(`"${name}"`, `"${original}"`) : full;
771
+ });
772
+ };
684
773
 
685
774
  const stream = new ReadableStream<Uint8Array>({
686
775
  async pull(controller) {
687
- const { done, value } = await reader.read()
776
+ const { done, value } = await reader.read();
688
777
  if (done) {
689
- const finalText = carry + decoder.decode()
690
- if (finalText) controller.enqueue(encoder.encode(transform(finalText)))
691
- controller.close()
692
- return
778
+ const finalText = carry + decoder.decode();
779
+ if (finalText) controller.enqueue(encoder.encode(transform(finalText)));
780
+ controller.close();
781
+ return;
693
782
  }
694
- carry += decoder.decode(value, { stream: true })
783
+ carry += decoder.decode(value, { stream: true });
695
784
  // Buffer 256 chars to avoid splitting JSON keys across chunks
696
- if (carry.length <= 256) return
697
- const output = carry.slice(0, -256)
698
- carry = carry.slice(-256)
699
- 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)));
700
789
  },
701
790
  async cancel(reason) {
702
- await reader.cancel(reason)
791
+ await reader.cancel(reason);
703
792
  },
704
- })
793
+ });
705
794
 
706
795
  return new Response(stream, {
707
796
  status: response.status,
708
797
  statusText: response.statusText,
709
798
  headers: response.headers,
710
- })
799
+ });
711
800
  }
712
801
 
713
802
  function appendToastSessionMarker({
714
803
  message,
715
804
  sessionId,
716
805
  }: {
717
- message: string
718
- sessionId: string | undefined
806
+ message: string;
807
+ sessionId: string | undefined;
719
808
  }) {
720
809
  if (!sessionId) {
721
- return message
810
+ return message;
722
811
  }
723
- return `${message} ${sessionId}`
812
+ return `${message} ${sessionId}`;
724
813
  }
725
814
 
726
815
  // --- Beta headers ---
727
816
 
728
817
  function getRequiredBetas(modelId: string | undefined) {
729
- 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
+ ];
730
823
  const isAdaptive =
731
- modelId?.includes('opus-4-6') ||
732
- modelId?.includes('opus-4.6') ||
733
- modelId?.includes('sonnet-4-6') ||
734
- modelId?.includes('sonnet-4.6')
735
- if (!isAdaptive) betas.push(INTERLEAVED_THINKING_BETA)
736
- 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;
737
830
  }
738
831
 
739
832
  function mergeBetas(existing: string | null, required: string[]) {
740
833
  return [
741
834
  ...new Set([
742
835
  ...required,
743
- ...(existing || '')
744
- .split(',')
836
+ ...(existing || "")
837
+ .split(",")
745
838
  .map((s) => s.trim())
746
839
  .filter(Boolean),
747
840
  ]),
748
- ].join(',')
841
+ ].join(",");
749
842
  }
750
843
 
751
844
  // --- Token refresh with dedup ---
752
845
 
753
846
  function isOAuthStored(auth: { type: string }): auth is OAuthStored {
754
- return auth.type === 'oauth'
847
+ return auth.type === "oauth";
755
848
  }
756
849
 
757
850
  async function getFreshOAuth(
758
851
  getAuth: () => Promise<OAuthStored | { type: string }>,
759
- client: Parameters<Plugin>[0]['client'],
852
+ client: Parameters<Plugin>[0]["client"],
760
853
  ) {
761
- const auth = await getAuth()
762
- if (!isOAuthStored(auth)) return undefined
763
- 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;
764
857
 
765
- const pending = pendingRefresh.get(auth.refresh)
858
+ const pending = pendingRefresh.get(auth.refresh);
766
859
  if (pending) {
767
- return pending
860
+ return pending;
768
861
  }
769
862
 
770
863
  const refreshPromise = withAuthStateLock(async () => {
771
- const latest = await getAuth()
864
+ const latest = await getAuth();
772
865
  if (!isOAuthStored(latest)) {
773
- throw new Error('Anthropic OAuth credentials disappeared during refresh')
866
+ throw new Error("Anthropic OAuth credentials disappeared during refresh");
774
867
  }
775
- if (latest.access && latest.expires > Date.now()) return latest
868
+ if (latest.access && latest.expires > Date.now()) return latest;
776
869
 
777
- const refreshed = await refreshAnthropicToken(latest.refresh)
778
- await setAnthropicAuth(refreshed, client)
779
- const store = await loadAccountStore()
870
+ const refreshed = await refreshAnthropicToken(latest.refresh);
871
+ await setAnthropicAuth(refreshed, client);
872
+ const store = await loadAccountStore();
780
873
  if (store.accounts.length > 0) {
781
874
  const identity: AnthropicAccountIdentity | undefined = (() => {
782
875
  const currentIndex = store.accounts.findIndex((account) => {
783
- return account.refresh === latest.refresh || account.access === latest.access
784
- })
785
- const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined
786
- 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;
787
884
  return {
788
885
  ...(current.email ? { email: current.email } : {}),
789
886
  ...(current.accountId ? { accountId: current.accountId } : {}),
790
- }
791
- })()
792
- upsertAccount(store, { ...refreshed, ...identity })
793
- await saveAccountStore(store)
887
+ };
888
+ })();
889
+ upsertAccount(store, { ...refreshed, ...identity });
890
+ await saveAccountStore(store);
794
891
  }
795
- return refreshed
796
- })
797
- pendingRefresh.set(auth.refresh, refreshPromise)
892
+ return refreshed;
893
+ });
894
+ pendingRefresh.set(auth.refresh, refreshPromise);
798
895
  return refreshPromise.finally(() => {
799
- pendingRefresh.delete(auth.refresh)
800
- })
896
+ pendingRefresh.delete(auth.refresh);
897
+ });
801
898
  }
802
899
 
803
- // --- Plugin export ---
804
-
805
900
  const AnthropicAuthPlugin: Plugin = async ({ client }) => {
806
901
  return {
807
- 'chat.headers': async (input, output) => {
808
- if (input.model.providerID !== 'anthropic') {
809
- return
902
+ "chat.headers": async (input, output) => {
903
+ if (input.model.providerID !== "anthropic") {
904
+ return;
810
905
  }
811
- output.headers[TOAST_SESSION_HEADER] = input.sessionID
906
+ output.headers[TOAST_SESSION_HEADER] = input.sessionID;
812
907
  },
813
908
  auth: {
814
- provider: 'anthropic',
909
+ provider: "anthropic",
815
910
  async loader(
816
911
  getAuth: () => Promise<OAuthStored | { type: string }>,
817
912
  provider: { models: Record<string, { cost?: unknown }> },
818
913
  ) {
819
- const auth = await getAuth()
820
- if (auth.type !== 'oauth') return {}
914
+ const auth = await getAuth();
915
+ if (auth.type !== "oauth") return {};
821
916
 
822
917
  // Zero out costs for OAuth users (Claude Pro/Max subscription)
823
918
  for (const model of Object.values(provider.models)) {
824
- model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
919
+ model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
825
920
  }
826
921
 
827
922
  return {
828
- apiKey: '',
923
+ apiKey: "",
829
924
  async fetch(input: Request | string | URL, init?: RequestInit) {
830
925
  const url = (() => {
831
926
  try {
832
- return new URL(input instanceof Request ? input.url : input.toString())
927
+ return new URL(
928
+ input instanceof Request ? input.url : input.toString(),
929
+ );
833
930
  } catch {
834
- return null
931
+ return null;
835
932
  }
836
- })()
837
- 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);
838
936
 
839
937
  const originalBody =
840
- typeof init?.body === 'string'
938
+ typeof init?.body === "string"
841
939
  ? init.body
842
940
  : input instanceof Request
843
941
  ? await input
844
942
  .clone()
845
943
  .text()
846
944
  .catch(() => undefined)
847
- : undefined
945
+ : undefined;
848
946
 
849
- const headers = new Headers(init?.headers)
947
+ const headers = new Headers(init?.headers);
850
948
  if (input instanceof Request) {
851
949
  input.headers.forEach((v, k) => {
852
- if (!headers.has(k)) headers.set(k, v)
853
- })
950
+ if (!headers.has(k)) headers.set(k, v);
951
+ });
854
952
  }
855
- const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined
953
+ const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
856
954
 
857
955
  const rewritten = rewriteRequestPayload(originalBody, (msg) => {
858
- client.tui.showToast({
859
- body: {
860
- message: appendToastSessionMarker({ message: msg, sessionId }),
861
- variant: 'error',
862
- },
863
- }).catch(() => {})
864
- })
865
- 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);
866
969
 
867
970
  const runRequest = async (auth: OAuthStored) => {
868
- const requestHeaders = new Headers(headers)
869
- requestHeaders.delete(TOAST_SESSION_HEADER)
870
- 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
+ );
871
978
  requestHeaders.set(
872
- 'anthropic-beta',
873
- mergeBetas(requestHeaders.get('anthropic-beta'), betas),
874
- )
875
- requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true')
876
- requestHeaders.set('authorization', `Bearer ${auth.access}`)
979
+ "anthropic-dangerous-direct-browser-access",
980
+ "true",
981
+ );
982
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
877
983
  requestHeaders.set(
878
- 'user-agent',
879
- process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
880
- )
881
- requestHeaders.set('x-app', 'cli')
882
- 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");
883
990
 
884
991
  return fetch(input, {
885
992
  ...(init ?? {}),
886
993
  body: rewritten.body,
887
994
  headers: requestHeaders,
888
- })
889
- }
995
+ });
996
+ };
890
997
 
891
- const freshAuth = await getFreshOAuth(getAuth, client)
892
- if (!freshAuth) return fetch(input, init)
998
+ const freshAuth = await getFreshOAuth(getAuth, client);
999
+ if (!freshAuth) return fetch(input, init);
893
1000
 
894
- let response = await runRequest(freshAuth)
1001
+ let response = await runRequest(freshAuth);
895
1002
  if (!response.ok) {
896
1003
  const bodyText = await response
897
1004
  .clone()
898
1005
  .text()
899
- .catch(() => '')
1006
+ .catch(() => "");
900
1007
  if (shouldRotateAuth(response.status, bodyText)) {
901
- const rotated = await rotateAnthropicAccount(freshAuth, client)
1008
+ const rotated = await rotateAnthropicAccount(freshAuth, client);
902
1009
  if (rotated) {
903
1010
  // Show toast notification so Discord thread shows the rotation
904
- client.tui.showToast({
905
- body: {
906
- message: appendToastSessionMarker({
907
- message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
908
- sessionId,
909
- }),
910
- variant: 'info',
911
- },
912
-
913
- }).catch(() => {})
914
- 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);
915
1023
  if (retryAuth) {
916
- response = await runRequest(retryAuth)
1024
+ response = await runRequest(retryAuth);
917
1025
  }
918
1026
  }
919
1027
  }
920
1028
  }
921
1029
 
922
- return wrapResponseStream(response, rewritten.reverseToolNameMap)
1030
+ return wrapResponseStream(response, rewritten.reverseToolNameMap);
923
1031
  },
924
- }
1032
+ };
925
1033
  },
926
1034
  methods: [
927
1035
  {
928
- label: 'Claude Pro/Max',
929
- type: 'oauth',
930
- authorize: buildAuthorizeHandler('oauth'),
1036
+ label: "Claude Pro/Max",
1037
+ type: "oauth",
1038
+ authorize: buildAuthorizeHandler("oauth"),
931
1039
  },
932
1040
  {
933
- label: 'Create an API Key',
934
- type: 'oauth',
935
- authorize: buildAuthorizeHandler('apikey'),
1041
+ label: "Create an API Key",
1042
+ type: "oauth",
1043
+ authorize: buildAuthorizeHandler("apikey"),
936
1044
  },
937
1045
  {
938
- provider: 'anthropic',
939
- label: 'Manually enter API Key',
940
- type: 'api',
1046
+ provider: "anthropic",
1047
+ label: "Manually enter API Key",
1048
+ type: "api",
941
1049
  },
942
1050
  ],
943
1051
  },
944
- }
945
- }
1052
+ };
1053
+ };
946
1054
 
947
- export {
948
- AnthropicAuthPlugin as anthropicAuthPlugin,
949
- }
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 };