@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
@@ -22,71 +22,72 @@
22
22
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/anthropic.ts
23
23
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
24
24
  */
25
- import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from './anthropic-auth-state.js';
26
- import { extractAnthropicAccountIdentity, } from './anthropic-account-identity.js';
25
+ import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
26
+ import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
27
27
  // PKCE (Proof Key for Code Exchange) using Web Crypto API.
28
28
  // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
29
29
  function base64urlEncode(bytes) {
30
- let binary = '';
30
+ let binary = "";
31
31
  for (const byte of bytes) {
32
32
  binary += String.fromCharCode(byte);
33
33
  }
34
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
34
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
35
35
  }
36
36
  async function generatePKCE() {
37
37
  const verifierBytes = new Uint8Array(32);
38
38
  crypto.getRandomValues(verifierBytes);
39
39
  const verifier = base64urlEncode(verifierBytes);
40
40
  const data = new TextEncoder().encode(verifier);
41
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
41
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
42
42
  const challenge = base64urlEncode(new Uint8Array(hashBuffer));
43
43
  return { verifier, challenge };
44
44
  }
45
- import { spawn } from 'node:child_process';
46
- import { createServer } from 'node:http';
45
+ import { spawn } from "node:child_process";
46
+ import { createServer } from "node:http";
47
47
  // --- Constants ---
48
48
  const CLIENT_ID = (() => {
49
- const encoded = 'OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl';
50
- return typeof atob === 'function'
49
+ const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
50
+ return typeof atob === "function"
51
51
  ? atob(encoded)
52
- : Buffer.from(encoded, 'base64').toString('utf8');
52
+ : Buffer.from(encoded, "base64").toString("utf8");
53
53
  })();
54
- const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
55
- const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key';
56
- const CLIENT_DATA_URL = 'https://api.anthropic.com/api/oauth/claude_cli/client_data';
57
- const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
54
+ const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
55
+ const CREATE_API_KEY_URL = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key";
56
+ const CLIENT_DATA_URL = "https://api.anthropic.com/api/oauth/claude_cli/client_data";
57
+ const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
58
58
  const CALLBACK_PORT = 53692;
59
- const CALLBACK_PATH = '/callback';
59
+ const CALLBACK_PATH = "/callback";
60
60
  const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
61
- const SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
61
+ const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
62
62
  const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
63
- const CLAUDE_CODE_VERSION = '2.1.75';
63
+ const CLAUDE_CODE_VERSION = "2.1.75";
64
64
  const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
65
- const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
66
- const CLAUDE_CODE_BETA = 'claude-code-20250219';
67
- const OAUTH_BETA = 'oauth-2025-04-20';
68
- const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14';
69
- const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
70
- const TOAST_SESSION_HEADER = 'x-kimaki-session-id';
65
+ const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
66
+ const ANTHROPIC_PROMPT_MARKER = "Skills provide specialized instructions";
67
+ const CLAUDE_CODE_BETA = "claude-code-20250219";
68
+ const OAUTH_BETA = "oauth-2025-04-20";
69
+ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
70
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
71
+ const TOAST_SESSION_HEADER = "x-kimaki-session-id";
71
72
  const ANTHROPIC_HOSTS = new Set([
72
- 'api.anthropic.com',
73
- 'claude.ai',
74
- 'console.anthropic.com',
75
- 'platform.claude.com',
73
+ "api.anthropic.com",
74
+ "claude.ai",
75
+ "console.anthropic.com",
76
+ "platform.claude.com",
76
77
  ]);
77
78
  const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME = {
78
- bash: 'Bash',
79
- edit: 'Edit',
80
- glob: 'Glob',
81
- grep: 'Grep',
82
- question: 'AskUserQuestion',
83
- read: 'Read',
84
- skill: 'Skill',
85
- task: 'Task',
86
- todowrite: 'TodoWrite',
87
- webfetch: 'WebFetch',
88
- websearch: 'WebSearch',
89
- write: 'Write',
79
+ bash: "Bash",
80
+ edit: "Edit",
81
+ glob: "Glob",
82
+ grep: "Grep",
83
+ question: "AskUserQuestion",
84
+ read: "Read",
85
+ skill: "Skill",
86
+ task: "Task",
87
+ todowrite: "TodoWrite",
88
+ webfetch: "WebFetch",
89
+ websearch: "WebSearch",
90
+ write: "Write",
90
91
  };
91
92
  // --- HTTP helpers ---
92
93
  // Claude OAuth token exchange can 429 when this runs inside the opencode auth
@@ -101,8 +102,8 @@ async function requestText(urlString, options) {
101
102
  method: options.method,
102
103
  url: urlString,
103
104
  });
104
- const child = spawn('node', [
105
- '-e',
105
+ const child = spawn("node", [
106
+ "-e",
106
107
  `
107
108
  const input = JSON.parse(process.argv[1]);
108
109
  (async () => {
@@ -124,32 +125,32 @@ const input = JSON.parse(process.argv[1]);
124
125
  `.trim(),
125
126
  payload,
126
127
  ], {
127
- stdio: ['ignore', 'pipe', 'pipe'],
128
+ stdio: ["ignore", "pipe", "pipe"],
128
129
  });
129
- let stdout = '';
130
- let stderr = '';
130
+ let stdout = "";
131
+ let stderr = "";
131
132
  const timeout = setTimeout(() => {
132
133
  child.kill();
133
134
  reject(new Error(`Request timed out. url=${urlString}`));
134
135
  }, 30_000);
135
- child.stdout.on('data', (chunk) => {
136
+ child.stdout.on("data", (chunk) => {
136
137
  stdout += String(chunk);
137
138
  });
138
- child.stderr.on('data', (chunk) => {
139
+ child.stderr.on("data", (chunk) => {
139
140
  stderr += String(chunk);
140
141
  });
141
- child.on('error', (error) => {
142
+ child.on("error", (error) => {
142
143
  clearTimeout(timeout);
143
144
  reject(error);
144
145
  });
145
- child.on('close', (code) => {
146
+ child.on("close", (code) => {
146
147
  clearTimeout(timeout);
147
148
  if (code !== 0) {
148
149
  let details = stderr.trim();
149
150
  try {
150
151
  const parsed = JSON.parse(details);
151
- if (typeof parsed.status === 'number') {
152
- reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ''}`));
152
+ if (typeof parsed.status === "number") {
153
+ reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`));
153
154
  return;
154
155
  }
155
156
  }
@@ -166,11 +167,11 @@ const input = JSON.parse(process.argv[1]);
166
167
  async function postJson(url, body) {
167
168
  const requestBody = JSON.stringify(body);
168
169
  const responseText = await requestText(url, {
169
- method: 'POST',
170
+ method: "POST",
170
171
  headers: {
171
- Accept: 'application/json',
172
- 'Content-Length': String(Buffer.byteLength(requestBody)),
173
- 'Content-Type': 'application/json',
172
+ Accept: "application/json",
173
+ "Content-Length": String(Buffer.byteLength(requestBody)),
174
+ "Content-Type": "application/json",
174
175
  },
175
176
  body: requestBody,
176
177
  });
@@ -190,7 +191,7 @@ function tokenExpiry(expiresIn) {
190
191
  }
191
192
  async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
192
193
  const json = await postJson(TOKEN_URL, {
193
- grant_type: 'authorization_code',
194
+ grant_type: "authorization_code",
194
195
  client_id: CLIENT_ID,
195
196
  code,
196
197
  state,
@@ -199,7 +200,7 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
199
200
  });
200
201
  const data = parseTokenResponse(json);
201
202
  return {
202
- type: 'success',
203
+ type: "success",
203
204
  refresh: data.refresh_token,
204
205
  access: data.access_token,
205
206
  expires: tokenExpiry(data.expires_in),
@@ -207,13 +208,13 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
207
208
  }
208
209
  async function refreshAnthropicToken(refreshToken) {
209
210
  const json = await postJson(TOKEN_URL, {
210
- grant_type: 'refresh_token',
211
+ grant_type: "refresh_token",
211
212
  client_id: CLIENT_ID,
212
213
  refresh_token: refreshToken,
213
214
  });
214
215
  const data = parseTokenResponse(json);
215
216
  return {
216
- type: 'oauth',
217
+ type: "oauth",
217
218
  refresh: data.refresh_token,
218
219
  access: data.access_token,
219
220
  expires: tokenExpiry(data.expires_in),
@@ -221,26 +222,27 @@ async function refreshAnthropicToken(refreshToken) {
221
222
  }
222
223
  async function createApiKey(accessToken) {
223
224
  const responseText = await requestText(CREATE_API_KEY_URL, {
224
- method: 'POST',
225
+ method: "POST",
225
226
  headers: {
226
- Accept: 'application/json',
227
+ Accept: "application/json",
227
228
  authorization: `Bearer ${accessToken}`,
228
- 'Content-Type': 'application/json',
229
+ "Content-Type": "application/json",
229
230
  },
230
231
  });
231
232
  const json = JSON.parse(responseText);
232
- return { type: 'success', key: json.raw_key };
233
+ return { type: "success", key: json.raw_key };
233
234
  }
234
235
  async function fetchAnthropicAccountIdentity(accessToken) {
235
236
  const urls = [CLIENT_DATA_URL, PROFILE_URL];
236
237
  for (const url of urls) {
237
238
  const responseText = await requestText(url, {
238
- method: 'GET',
239
+ method: "GET",
239
240
  headers: {
240
- Accept: 'application/json',
241
+ Accept: "application/json",
241
242
  authorization: `Bearer ${accessToken}`,
242
- 'user-agent': process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
243
- 'x-app': 'cli',
243
+ "user-agent": process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
244
+ `claude-cli/${CLAUDE_CODE_VERSION}`,
245
+ "x-app": "cli",
244
246
  },
245
247
  }).catch(() => {
246
248
  return undefined;
@@ -268,29 +270,31 @@ async function startCallbackServer(expectedState) {
268
270
  });
269
271
  const server = createServer((req, res) => {
270
272
  try {
271
- const url = new URL(req.url || '', 'http://localhost');
273
+ const url = new URL(req.url || "", "http://localhost");
272
274
  if (url.pathname !== CALLBACK_PATH) {
273
- res.writeHead(404).end('Not found');
275
+ res.writeHead(404).end("Not found");
274
276
  return;
275
277
  }
276
- const code = url.searchParams.get('code');
277
- const state = url.searchParams.get('state');
278
- const error = url.searchParams.get('error');
278
+ const code = url.searchParams.get("code");
279
+ const state = url.searchParams.get("state");
280
+ const error = url.searchParams.get("error");
279
281
  if (error || !code || !state || state !== expectedState) {
280
- res.writeHead(400).end('Authentication failed: ' + (error || 'missing code/state'));
282
+ res
283
+ .writeHead(400)
284
+ .end("Authentication failed: " + (error || "missing code/state"));
281
285
  return;
282
286
  }
283
287
  res
284
- .writeHead(200, { 'Content-Type': 'text/plain' })
285
- .end('Authentication successful. You can close this window.');
288
+ .writeHead(200, { "Content-Type": "text/plain" })
289
+ .end("Authentication successful. You can close this window.");
286
290
  settle?.({ code, state });
287
291
  }
288
292
  catch {
289
- res.writeHead(500).end('Internal error');
293
+ res.writeHead(500).end("Internal error");
290
294
  }
291
295
  });
292
- server.once('error', reject);
293
- server.listen(CALLBACK_PORT, '127.0.0.1', () => {
296
+ server.once("error", reject);
297
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
294
298
  resolve({
295
299
  server,
296
300
  cancelWait: () => {
@@ -315,13 +319,13 @@ async function beginAuthorizationFlow() {
315
319
  const pkce = await generatePKCE();
316
320
  const callbackServer = await startCallbackServer(pkce.verifier);
317
321
  const authParams = new URLSearchParams({
318
- code: 'true',
322
+ code: "true",
319
323
  client_id: CLIENT_ID,
320
- response_type: 'code',
324
+ response_type: "code",
321
325
  redirect_uri: REDIRECT_URI,
322
326
  scope: SCOPES,
323
327
  code_challenge: pkce.challenge,
324
- code_challenge_method: 'S256',
328
+ code_challenge_method: "S256",
325
329
  state: pkce.verifier,
326
330
  });
327
331
  return {
@@ -358,7 +362,7 @@ async function waitForCallback(callbackServer, manualInput) {
358
362
  }),
359
363
  ]);
360
364
  if (!result?.code) {
361
- throw new Error('Timed out waiting for OAuth callback');
365
+ throw new Error("Timed out waiting for OAuth callback");
362
366
  }
363
367
  return result;
364
368
  }
@@ -370,25 +374,25 @@ async function waitForCallback(callbackServer, manualInput) {
370
374
  function parseManualInput(input) {
371
375
  try {
372
376
  const url = new URL(input);
373
- const code = url.searchParams.get('code');
374
- const state = url.searchParams.get('state');
377
+ const code = url.searchParams.get("code");
378
+ const state = url.searchParams.get("state");
375
379
  if (code)
376
- return { code, state: state || '' };
380
+ return { code, state: state || "" };
377
381
  }
378
382
  catch {
379
383
  // not a URL
380
384
  }
381
- if (input.includes('#')) {
382
- const [code = '', state = ''] = input.split('#', 2);
385
+ if (input.includes("#")) {
386
+ const [code = "", state = ""] = input.split("#", 2);
383
387
  return { code, state };
384
388
  }
385
- if (input.includes('code=')) {
389
+ if (input.includes("code=")) {
386
390
  const params = new URLSearchParams(input);
387
- const code = params.get('code');
391
+ const code = params.get("code");
388
392
  if (code)
389
- return { code, state: params.get('state') || '' };
393
+ return { code, state: params.get("state") || "" };
390
394
  }
391
- return { code: input, state: '' };
395
+ return { code: input, state: "" };
392
396
  }
393
397
  // Unified authorize handler: returns either OAuth tokens or an API key,
394
398
  // for both auto and remote-first modes.
@@ -400,12 +404,12 @@ function buildAuthorizeHandler(mode) {
400
404
  const finalize = async (result) => {
401
405
  const verifier = auth.verifier;
402
406
  const creds = await exchangeAuthorizationCode(result.code, result.state || verifier, verifier, REDIRECT_URI);
403
- if (mode === 'apikey') {
407
+ if (mode === "apikey") {
404
408
  return createApiKey(creds.access);
405
409
  }
406
410
  const identity = await fetchAnthropicAccountIdentity(creds.access);
407
411
  await rememberAnthropicOAuth({
408
- type: 'oauth',
412
+ type: "oauth",
409
413
  refresh: creds.refresh,
410
414
  access: creds.access,
411
415
  expires: creds.expires,
@@ -415,8 +419,8 @@ function buildAuthorizeHandler(mode) {
415
419
  if (!isRemote) {
416
420
  return {
417
421
  url: auth.url,
418
- instructions: 'Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.',
419
- method: 'auto',
422
+ instructions: "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
423
+ method: "auto",
420
424
  callback: async () => {
421
425
  pendingAuthResult ??= (async () => {
422
426
  try {
@@ -424,7 +428,7 @@ function buildAuthorizeHandler(mode) {
424
428
  return await finalize(result);
425
429
  }
426
430
  catch {
427
- return { type: 'failed' };
431
+ return { type: "failed" };
428
432
  }
429
433
  })();
430
434
  return pendingAuthResult;
@@ -433,8 +437,8 @@ function buildAuthorizeHandler(mode) {
433
437
  }
434
438
  return {
435
439
  url: auth.url,
436
- instructions: 'Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.',
437
- method: 'code',
440
+ instructions: "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
441
+ method: "code",
438
442
  callback: async (input) => {
439
443
  pendingAuthResult ??= (async () => {
440
444
  try {
@@ -442,7 +446,7 @@ function buildAuthorizeHandler(mode) {
442
446
  return await finalize(result);
443
447
  }
444
448
  catch {
445
- return { type: 'failed' };
449
+ return { type: "failed" };
446
450
  }
447
451
  })();
448
452
  return pendingAuthResult;
@@ -456,46 +460,56 @@ function buildAuthorizeHandler(mode) {
456
460
  function toClaudeCodeToolName(name) {
457
461
  return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name;
458
462
  }
459
- function sanitizeSystemText(text, onError) {
463
+ function sanitizeAnthropicSystemText(text, onError) {
460
464
  const startIdx = text.indexOf(OPENCODE_IDENTITY);
461
465
  if (startIdx === -1)
462
466
  return text;
463
- const codeRefsMarker = '# Code References';
464
- const endIdx = text.indexOf(codeRefsMarker, startIdx);
467
+ // Keep the marker aligned with the current OpenCode Anthropic prompt.
468
+ const endIdx = text.indexOf(ANTHROPIC_PROMPT_MARKER, startIdx);
465
469
  if (endIdx === -1) {
466
- onError?.(`sanitizeSystemText: could not find '# Code References' after OpenCode identity`);
470
+ onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
467
471
  return text;
468
472
  }
469
- // Remove everything from the OpenCode identity up to (but not including) '# Code References'
470
- return text.slice(0, startIdx) + text.slice(endIdx);
473
+ return (text.slice(0, startIdx) + text.slice(endIdx)).replaceAll("opencode", "openc0de");
474
+ }
475
+ function mapSystemTextPart(part, onError) {
476
+ if (typeof part === "string") {
477
+ return { type: "text", text: sanitizeAnthropicSystemText(part, onError) };
478
+ }
479
+ if (part &&
480
+ typeof part === "object" &&
481
+ "type" in part &&
482
+ part.type === "text" &&
483
+ "text" in part &&
484
+ typeof part.text === "string") {
485
+ return {
486
+ ...part,
487
+ text: sanitizeAnthropicSystemText(part.text, onError),
488
+ };
489
+ }
490
+ return part;
471
491
  }
472
492
  function prependClaudeCodeIdentity(system, onError) {
473
- const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY };
474
- if (typeof system === 'undefined')
493
+ const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
494
+ if (typeof system === "undefined")
475
495
  return [identityBlock];
476
- if (typeof system === 'string') {
477
- const sanitized = sanitizeSystemText(system, onError);
496
+ if (typeof system === "string") {
497
+ const sanitized = sanitizeAnthropicSystemText(system, onError);
478
498
  if (sanitized === CLAUDE_CODE_IDENTITY)
479
499
  return [identityBlock];
480
- return [identityBlock, { type: 'text', text: sanitized }];
500
+ return [identityBlock, { type: "text", text: sanitized }];
481
501
  }
482
502
  if (!Array.isArray(system))
483
503
  return [identityBlock, system];
484
504
  const sanitized = system.map((item) => {
485
- if (typeof item === 'string')
486
- return { type: 'text', text: sanitizeSystemText(item, onError) };
487
- if (item && typeof item === 'object' && item.type === 'text') {
488
- const text = item.text;
489
- if (typeof text === 'string') {
490
- return { ...item, text: sanitizeSystemText(text, onError) };
491
- }
492
- }
493
- return item;
505
+ return mapSystemTextPart(item, onError);
494
506
  });
495
507
  const first = sanitized[0];
496
508
  if (first &&
497
- typeof first === 'object' &&
498
- first.type === 'text' &&
509
+ typeof first === "object" &&
510
+ "type" in first &&
511
+ first.type === "text" &&
512
+ "text" in first &&
499
513
  first.text === CLAUDE_CODE_IDENTITY) {
500
514
  return sanitized;
501
515
  }
@@ -503,18 +517,22 @@ function prependClaudeCodeIdentity(system, onError) {
503
517
  }
504
518
  function rewriteRequestPayload(body, onError) {
505
519
  if (!body)
506
- return { body, modelId: undefined, reverseToolNameMap: new Map() };
520
+ return {
521
+ body,
522
+ modelId: undefined,
523
+ reverseToolNameMap: new Map(),
524
+ };
507
525
  try {
508
526
  const payload = JSON.parse(body);
509
527
  const reverseToolNameMap = new Map();
510
- const modelId = typeof payload.model === 'string' ? payload.model : undefined;
528
+ const modelId = typeof payload.model === "string" ? payload.model : undefined;
511
529
  // Build reverse map and rename tools
512
530
  if (Array.isArray(payload.tools)) {
513
531
  payload.tools = payload.tools.map((tool) => {
514
- if (!tool || typeof tool !== 'object')
532
+ if (!tool || typeof tool !== "object")
515
533
  return tool;
516
534
  const name = tool.name;
517
- if (typeof name !== 'string')
535
+ if (typeof name !== "string")
518
536
  return tool;
519
537
  const mapped = toClaudeCodeToolName(name);
520
538
  reverseToolNameMap.set(mapped, name);
@@ -525,10 +543,10 @@ function rewriteRequestPayload(body, onError) {
525
543
  payload.system = prependClaudeCodeIdentity(payload.system, onError);
526
544
  // Rename tool_choice
527
545
  if (payload.tool_choice &&
528
- typeof payload.tool_choice === 'object' &&
529
- payload.tool_choice.type === 'tool') {
546
+ typeof payload.tool_choice === "object" &&
547
+ payload.tool_choice.type === "tool") {
530
548
  const name = payload.tool_choice.name;
531
- if (typeof name === 'string') {
549
+ if (typeof name === "string") {
532
550
  payload.tool_choice = {
533
551
  ...payload.tool_choice,
534
552
  name: toClaudeCodeToolName(name),
@@ -538,7 +556,7 @@ function rewriteRequestPayload(body, onError) {
538
556
  // Rename tool_use blocks in messages
539
557
  if (Array.isArray(payload.messages)) {
540
558
  payload.messages = payload.messages.map((message) => {
541
- if (!message || typeof message !== 'object')
559
+ if (!message || typeof message !== "object")
542
560
  return message;
543
561
  const content = message.content;
544
562
  if (!Array.isArray(content))
@@ -546,12 +564,15 @@ function rewriteRequestPayload(body, onError) {
546
564
  return {
547
565
  ...message,
548
566
  content: content.map((block) => {
549
- if (!block || typeof block !== 'object')
567
+ if (!block || typeof block !== "object")
550
568
  return block;
551
569
  const b = block;
552
- if (b.type !== 'tool_use' || typeof b.name !== 'string')
570
+ if (b.type !== "tool_use" || typeof b.name !== "string")
553
571
  return block;
554
- return { ...block, name: toClaudeCodeToolName(b.name) };
572
+ return {
573
+ ...block,
574
+ name: toClaudeCodeToolName(b.name),
575
+ };
555
576
  }),
556
577
  };
557
578
  });
@@ -559,7 +580,11 @@ function rewriteRequestPayload(body, onError) {
559
580
  return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
560
581
  }
561
582
  catch {
562
- return { body, modelId: undefined, reverseToolNameMap: new Map() };
583
+ return {
584
+ body,
585
+ modelId: undefined,
586
+ reverseToolNameMap: new Map(),
587
+ };
563
588
  }
564
589
  }
565
590
  function wrapResponseStream(response, reverseToolNameMap) {
@@ -568,7 +593,7 @@ function wrapResponseStream(response, reverseToolNameMap) {
568
593
  const reader = response.body.getReader();
569
594
  const decoder = new TextDecoder();
570
595
  const encoder = new TextEncoder();
571
- let carry = '';
596
+ let carry = "";
572
597
  const transform = (text) => {
573
598
  return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name) => {
574
599
  const original = reverseToolNameMap.get(name);
@@ -611,11 +636,15 @@ function appendToastSessionMarker({ message, sessionId, }) {
611
636
  }
612
637
  // --- Beta headers ---
613
638
  function getRequiredBetas(modelId) {
614
- const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA];
615
- const isAdaptive = modelId?.includes('opus-4-6') ||
616
- modelId?.includes('opus-4.6') ||
617
- modelId?.includes('sonnet-4-6') ||
618
- modelId?.includes('sonnet-4.6');
639
+ const betas = [
640
+ CLAUDE_CODE_BETA,
641
+ OAUTH_BETA,
642
+ FINE_GRAINED_TOOL_STREAMING_BETA,
643
+ ];
644
+ const isAdaptive = modelId?.includes("opus-4-6") ||
645
+ modelId?.includes("opus-4.6") ||
646
+ modelId?.includes("sonnet-4-6") ||
647
+ modelId?.includes("sonnet-4.6");
619
648
  if (!isAdaptive)
620
649
  betas.push(INTERLEAVED_THINKING_BETA);
621
650
  return betas;
@@ -624,16 +653,16 @@ function mergeBetas(existing, required) {
624
653
  return [
625
654
  ...new Set([
626
655
  ...required,
627
- ...(existing || '')
628
- .split(',')
656
+ ...(existing || "")
657
+ .split(",")
629
658
  .map((s) => s.trim())
630
659
  .filter(Boolean),
631
660
  ]),
632
- ].join(',');
661
+ ].join(",");
633
662
  }
634
663
  // --- Token refresh with dedup ---
635
664
  function isOAuthStored(auth) {
636
- return auth.type === 'oauth';
665
+ return auth.type === "oauth";
637
666
  }
638
667
  async function getFreshOAuth(getAuth, client) {
639
668
  const auth = await getAuth();
@@ -648,7 +677,7 @@ async function getFreshOAuth(getAuth, client) {
648
677
  const refreshPromise = withAuthStateLock(async () => {
649
678
  const latest = await getAuth();
650
679
  if (!isOAuthStored(latest)) {
651
- throw new Error('Anthropic OAuth credentials disappeared during refresh');
680
+ throw new Error("Anthropic OAuth credentials disappeared during refresh");
652
681
  }
653
682
  if (latest.access && latest.expires > Date.now())
654
683
  return latest;
@@ -658,7 +687,8 @@ async function getFreshOAuth(getAuth, client) {
658
687
  if (store.accounts.length > 0) {
659
688
  const identity = (() => {
660
689
  const currentIndex = store.accounts.findIndex((account) => {
661
- return account.refresh === latest.refresh || account.access === latest.access;
690
+ return (account.refresh === latest.refresh ||
691
+ account.access === latest.access);
662
692
  });
663
693
  const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
664
694
  if (!current)
@@ -678,27 +708,26 @@ async function getFreshOAuth(getAuth, client) {
678
708
  pendingRefresh.delete(auth.refresh);
679
709
  });
680
710
  }
681
- // --- Plugin export ---
682
711
  const AnthropicAuthPlugin = async ({ client }) => {
683
712
  return {
684
- 'chat.headers': async (input, output) => {
685
- if (input.model.providerID !== 'anthropic') {
713
+ "chat.headers": async (input, output) => {
714
+ if (input.model.providerID !== "anthropic") {
686
715
  return;
687
716
  }
688
717
  output.headers[TOAST_SESSION_HEADER] = input.sessionID;
689
718
  },
690
719
  auth: {
691
- provider: 'anthropic',
720
+ provider: "anthropic",
692
721
  async loader(getAuth, provider) {
693
722
  const auth = await getAuth();
694
- if (auth.type !== 'oauth')
723
+ if (auth.type !== "oauth")
695
724
  return {};
696
725
  // Zero out costs for OAuth users (Claude Pro/Max subscription)
697
726
  for (const model of Object.values(provider.models)) {
698
727
  model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
699
728
  }
700
729
  return {
701
- apiKey: '',
730
+ apiKey: "",
702
731
  async fetch(input, init) {
703
732
  const url = (() => {
704
733
  try {
@@ -710,7 +739,7 @@ const AnthropicAuthPlugin = async ({ client }) => {
710
739
  })();
711
740
  if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
712
741
  return fetch(input, init);
713
- const originalBody = typeof init?.body === 'string'
742
+ const originalBody = typeof init?.body === "string"
714
743
  ? init.body
715
744
  : input instanceof Request
716
745
  ? await input
@@ -727,24 +756,30 @@ const AnthropicAuthPlugin = async ({ client }) => {
727
756
  }
728
757
  const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
729
758
  const rewritten = rewriteRequestPayload(originalBody, (msg) => {
730
- client.tui.showToast({
759
+ client.tui
760
+ .showToast({
731
761
  body: {
732
- message: appendToastSessionMarker({ message: msg, sessionId }),
733
- variant: 'error',
762
+ message: appendToastSessionMarker({
763
+ message: msg,
764
+ sessionId,
765
+ }),
766
+ variant: "error",
734
767
  },
735
- }).catch(() => { });
768
+ })
769
+ .catch(() => { });
736
770
  });
737
771
  const betas = getRequiredBetas(rewritten.modelId);
738
772
  const runRequest = async (auth) => {
739
773
  const requestHeaders = new Headers(headers);
740
774
  requestHeaders.delete(TOAST_SESSION_HEADER);
741
- requestHeaders.set('accept', 'application/json');
742
- requestHeaders.set('anthropic-beta', mergeBetas(requestHeaders.get('anthropic-beta'), betas));
743
- requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true');
744
- requestHeaders.set('authorization', `Bearer ${auth.access}`);
745
- requestHeaders.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`);
746
- requestHeaders.set('x-app', 'cli');
747
- requestHeaders.delete('x-api-key');
775
+ requestHeaders.set("accept", "application/json");
776
+ requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
777
+ requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
778
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
779
+ requestHeaders.set("user-agent", process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
780
+ `claude-cli/${CLAUDE_CODE_VERSION}`);
781
+ requestHeaders.set("x-app", "cli");
782
+ requestHeaders.delete("x-api-key");
748
783
  return fetch(input, {
749
784
  ...(init ?? {}),
750
785
  body: rewritten.body,
@@ -759,20 +794,22 @@ const AnthropicAuthPlugin = async ({ client }) => {
759
794
  const bodyText = await response
760
795
  .clone()
761
796
  .text()
762
- .catch(() => '');
797
+ .catch(() => "");
763
798
  if (shouldRotateAuth(response.status, bodyText)) {
764
799
  const rotated = await rotateAnthropicAccount(freshAuth, client);
765
800
  if (rotated) {
766
801
  // Show toast notification so Discord thread shows the rotation
767
- client.tui.showToast({
802
+ client.tui
803
+ .showToast({
768
804
  body: {
769
805
  message: appendToastSessionMarker({
770
806
  message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
771
807
  sessionId,
772
808
  }),
773
- variant: 'info',
809
+ variant: "info",
774
810
  },
775
- }).catch(() => { });
811
+ })
812
+ .catch(() => { });
776
813
  const retryAuth = await getFreshOAuth(getAuth, client);
777
814
  if (retryAuth) {
778
815
  response = await runRequest(retryAuth);
@@ -786,22 +823,36 @@ const AnthropicAuthPlugin = async ({ client }) => {
786
823
  },
787
824
  methods: [
788
825
  {
789
- label: 'Claude Pro/Max',
790
- type: 'oauth',
791
- authorize: buildAuthorizeHandler('oauth'),
826
+ label: "Claude Pro/Max",
827
+ type: "oauth",
828
+ authorize: buildAuthorizeHandler("oauth"),
792
829
  },
793
830
  {
794
- label: 'Create an API Key',
795
- type: 'oauth',
796
- authorize: buildAuthorizeHandler('apikey'),
831
+ label: "Create an API Key",
832
+ type: "oauth",
833
+ authorize: buildAuthorizeHandler("apikey"),
797
834
  },
798
835
  {
799
- provider: 'anthropic',
800
- label: 'Manually enter API Key',
801
- type: 'api',
836
+ provider: "anthropic",
837
+ label: "Manually enter API Key",
838
+ type: "api",
802
839
  },
803
840
  ],
804
841
  },
805
842
  };
806
843
  };
807
- export { AnthropicAuthPlugin as anthropicAuthPlugin, };
844
+ const replacer = async () => {
845
+ return {
846
+ "experimental.chat.system.transform": (async (input, output) => {
847
+ if (input.model.providerID !== "anthropic")
848
+ return;
849
+ const textIndex = output.system.findIndex((x) => x.includes(OPENCODE_IDENTITY));
850
+ const text = output.system[textIndex];
851
+ if (!text) {
852
+ return;
853
+ }
854
+ output.system[textIndex] = sanitizeAnthropicSystemText(text);
855
+ }),
856
+ };
857
+ };
858
+ export { replacer, AnthropicAuthPlugin as anthropicAuthPlugin };