@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.
- package/dist/anthropic-auth-plugin.js +227 -178
- package/dist/commands/login.js +6 -4
- package/dist/context-awareness-plugin.js +8 -38
- package/dist/kimaki-opencode-plugin.js +3 -1
- package/dist/memory-overview-plugin.js +126 -0
- package/package.json +1 -1
- package/src/anthropic-auth-plugin.ts +574 -453
- package/src/commands/login.ts +6 -4
- package/src/context-awareness-plugin.ts +11 -42
- package/src/kimaki-opencode-plugin.ts +3 -1
- package/src/memory-overview-plugin.ts +161 -0
|
@@ -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
|
|
26
|
-
import { extractAnthropicAccountIdentity, } from
|
|
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,
|
|
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(
|
|
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
|
|
46
|
-
import { createServer } from
|
|
45
|
+
import { spawn } from "node:child_process";
|
|
46
|
+
import { createServer } from "node:http";
|
|
47
47
|
// --- Constants ---
|
|
48
48
|
const CLIENT_ID = (() => {
|
|
49
|
-
const encoded =
|
|
50
|
-
return typeof atob ===
|
|
49
|
+
const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
|
|
50
|
+
return typeof atob === "function"
|
|
51
51
|
? atob(encoded)
|
|
52
|
-
: Buffer.from(encoded,
|
|
52
|
+
: Buffer.from(encoded, "base64").toString("utf8");
|
|
53
53
|
})();
|
|
54
|
-
const TOKEN_URL =
|
|
55
|
-
const CREATE_API_KEY_URL =
|
|
56
|
-
const CLIENT_DATA_URL =
|
|
57
|
-
const PROFILE_URL =
|
|
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 =
|
|
59
|
+
const CALLBACK_PATH = "/callback";
|
|
60
60
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
61
|
-
const SCOPES =
|
|
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 =
|
|
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 =
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
79
|
-
edit:
|
|
80
|
-
glob:
|
|
81
|
-
grep:
|
|
82
|
-
question:
|
|
83
|
-
read:
|
|
84
|
-
skill:
|
|
85
|
-
task:
|
|
86
|
-
todowrite:
|
|
87
|
-
webfetch:
|
|
88
|
-
websearch:
|
|
89
|
-
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(
|
|
105
|
-
|
|
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: [
|
|
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(
|
|
136
|
+
child.stdout.on("data", (chunk) => {
|
|
136
137
|
stdout += String(chunk);
|
|
137
138
|
});
|
|
138
|
-
child.stderr.on(
|
|
139
|
+
child.stderr.on("data", (chunk) => {
|
|
139
140
|
stderr += String(chunk);
|
|
140
141
|
});
|
|
141
|
-
child.on(
|
|
142
|
+
child.on("error", (error) => {
|
|
142
143
|
clearTimeout(timeout);
|
|
143
144
|
reject(error);
|
|
144
145
|
});
|
|
145
|
-
child.on(
|
|
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 ===
|
|
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:
|
|
170
|
+
method: "POST",
|
|
170
171
|
headers: {
|
|
171
|
-
Accept:
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
225
|
+
method: "POST",
|
|
225
226
|
headers: {
|
|
226
|
-
Accept:
|
|
227
|
+
Accept: "application/json",
|
|
227
228
|
authorization: `Bearer ${accessToken}`,
|
|
228
|
-
|
|
229
|
+
"Content-Type": "application/json",
|
|
229
230
|
},
|
|
230
231
|
});
|
|
231
232
|
const json = JSON.parse(responseText);
|
|
232
|
-
return { type:
|
|
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:
|
|
239
|
+
method: "GET",
|
|
239
240
|
headers: {
|
|
240
|
-
Accept:
|
|
241
|
+
Accept: "application/json",
|
|
241
242
|
authorization: `Bearer ${accessToken}`,
|
|
242
|
-
|
|
243
|
-
|
|
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 ||
|
|
273
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
272
274
|
if (url.pathname !== CALLBACK_PATH) {
|
|
273
|
-
res.writeHead(404).end(
|
|
275
|
+
res.writeHead(404).end("Not found");
|
|
274
276
|
return;
|
|
275
277
|
}
|
|
276
|
-
const code = url.searchParams.get(
|
|
277
|
-
const state = url.searchParams.get(
|
|
278
|
-
const error = url.searchParams.get(
|
|
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
|
|
282
|
+
res
|
|
283
|
+
.writeHead(400)
|
|
284
|
+
.end("Authentication failed: " + (error || "missing code/state"));
|
|
281
285
|
return;
|
|
282
286
|
}
|
|
283
287
|
res
|
|
284
|
-
.writeHead(200, {
|
|
285
|
-
.end(
|
|
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(
|
|
293
|
+
res.writeHead(500).end("Internal error");
|
|
290
294
|
}
|
|
291
295
|
});
|
|
292
|
-
server.once(
|
|
293
|
-
server.listen(CALLBACK_PORT,
|
|
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:
|
|
322
|
+
code: "true",
|
|
319
323
|
client_id: CLIENT_ID,
|
|
320
|
-
response_type:
|
|
324
|
+
response_type: "code",
|
|
321
325
|
redirect_uri: REDIRECT_URI,
|
|
322
326
|
scope: SCOPES,
|
|
323
327
|
code_challenge: pkce.challenge,
|
|
324
|
-
code_challenge_method:
|
|
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(
|
|
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(
|
|
374
|
-
const state = url.searchParams.get(
|
|
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 =
|
|
385
|
+
if (input.includes("#")) {
|
|
386
|
+
const [code = "", state = ""] = input.split("#", 2);
|
|
383
387
|
return { code, state };
|
|
384
388
|
}
|
|
385
|
-
if (input.includes(
|
|
389
|
+
if (input.includes("code=")) {
|
|
386
390
|
const params = new URLSearchParams(input);
|
|
387
|
-
const code = params.get(
|
|
391
|
+
const code = params.get("code");
|
|
388
392
|
if (code)
|
|
389
|
-
return { code, state: params.get(
|
|
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 ===
|
|
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:
|
|
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:
|
|
419
|
-
method:
|
|
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:
|
|
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:
|
|
437
|
-
method:
|
|
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:
|
|
449
|
+
return { type: "failed" };
|
|
446
450
|
}
|
|
447
451
|
})();
|
|
448
452
|
return pendingAuthResult;
|
|
@@ -456,48 +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
|
|
463
|
+
function sanitizeAnthropicSystemText(text, onError) {
|
|
460
464
|
const startIdx = text.indexOf(OPENCODE_IDENTITY);
|
|
461
465
|
if (startIdx === -1)
|
|
462
466
|
return text;
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
const codeRefsMarker = '# Code References';
|
|
466
|
-
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);
|
|
467
469
|
if (endIdx === -1) {
|
|
468
|
-
onError?.(
|
|
470
|
+
onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
|
|
469
471
|
return text;
|
|
470
472
|
}
|
|
471
|
-
|
|
472
|
-
|
|
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;
|
|
473
491
|
}
|
|
474
492
|
function prependClaudeCodeIdentity(system, onError) {
|
|
475
|
-
const identityBlock = { type:
|
|
476
|
-
if (typeof system ===
|
|
493
|
+
const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
|
|
494
|
+
if (typeof system === "undefined")
|
|
477
495
|
return [identityBlock];
|
|
478
|
-
if (typeof system ===
|
|
479
|
-
const sanitized =
|
|
496
|
+
if (typeof system === "string") {
|
|
497
|
+
const sanitized = sanitizeAnthropicSystemText(system, onError);
|
|
480
498
|
if (sanitized === CLAUDE_CODE_IDENTITY)
|
|
481
499
|
return [identityBlock];
|
|
482
|
-
return [identityBlock, { type:
|
|
500
|
+
return [identityBlock, { type: "text", text: sanitized }];
|
|
483
501
|
}
|
|
484
502
|
if (!Array.isArray(system))
|
|
485
503
|
return [identityBlock, system];
|
|
486
504
|
const sanitized = system.map((item) => {
|
|
487
|
-
|
|
488
|
-
return { type: 'text', text: sanitizeSystemText(item, onError) };
|
|
489
|
-
if (item && typeof item === 'object' && item.type === 'text') {
|
|
490
|
-
const text = item.text;
|
|
491
|
-
if (typeof text === 'string') {
|
|
492
|
-
return { ...item, text: sanitizeSystemText(text, onError) };
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return item;
|
|
505
|
+
return mapSystemTextPart(item, onError);
|
|
496
506
|
});
|
|
497
507
|
const first = sanitized[0];
|
|
498
508
|
if (first &&
|
|
499
|
-
typeof first ===
|
|
500
|
-
|
|
509
|
+
typeof first === "object" &&
|
|
510
|
+
"type" in first &&
|
|
511
|
+
first.type === "text" &&
|
|
512
|
+
"text" in first &&
|
|
501
513
|
first.text === CLAUDE_CODE_IDENTITY) {
|
|
502
514
|
return sanitized;
|
|
503
515
|
}
|
|
@@ -505,18 +517,22 @@ function prependClaudeCodeIdentity(system, onError) {
|
|
|
505
517
|
}
|
|
506
518
|
function rewriteRequestPayload(body, onError) {
|
|
507
519
|
if (!body)
|
|
508
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
body,
|
|
522
|
+
modelId: undefined,
|
|
523
|
+
reverseToolNameMap: new Map(),
|
|
524
|
+
};
|
|
509
525
|
try {
|
|
510
526
|
const payload = JSON.parse(body);
|
|
511
527
|
const reverseToolNameMap = new Map();
|
|
512
|
-
const modelId = typeof payload.model ===
|
|
528
|
+
const modelId = typeof payload.model === "string" ? payload.model : undefined;
|
|
513
529
|
// Build reverse map and rename tools
|
|
514
530
|
if (Array.isArray(payload.tools)) {
|
|
515
531
|
payload.tools = payload.tools.map((tool) => {
|
|
516
|
-
if (!tool || typeof tool !==
|
|
532
|
+
if (!tool || typeof tool !== "object")
|
|
517
533
|
return tool;
|
|
518
534
|
const name = tool.name;
|
|
519
|
-
if (typeof name !==
|
|
535
|
+
if (typeof name !== "string")
|
|
520
536
|
return tool;
|
|
521
537
|
const mapped = toClaudeCodeToolName(name);
|
|
522
538
|
reverseToolNameMap.set(mapped, name);
|
|
@@ -527,10 +543,10 @@ function rewriteRequestPayload(body, onError) {
|
|
|
527
543
|
payload.system = prependClaudeCodeIdentity(payload.system, onError);
|
|
528
544
|
// Rename tool_choice
|
|
529
545
|
if (payload.tool_choice &&
|
|
530
|
-
typeof payload.tool_choice ===
|
|
531
|
-
payload.tool_choice.type ===
|
|
546
|
+
typeof payload.tool_choice === "object" &&
|
|
547
|
+
payload.tool_choice.type === "tool") {
|
|
532
548
|
const name = payload.tool_choice.name;
|
|
533
|
-
if (typeof name ===
|
|
549
|
+
if (typeof name === "string") {
|
|
534
550
|
payload.tool_choice = {
|
|
535
551
|
...payload.tool_choice,
|
|
536
552
|
name: toClaudeCodeToolName(name),
|
|
@@ -540,7 +556,7 @@ function rewriteRequestPayload(body, onError) {
|
|
|
540
556
|
// Rename tool_use blocks in messages
|
|
541
557
|
if (Array.isArray(payload.messages)) {
|
|
542
558
|
payload.messages = payload.messages.map((message) => {
|
|
543
|
-
if (!message || typeof message !==
|
|
559
|
+
if (!message || typeof message !== "object")
|
|
544
560
|
return message;
|
|
545
561
|
const content = message.content;
|
|
546
562
|
if (!Array.isArray(content))
|
|
@@ -548,12 +564,15 @@ function rewriteRequestPayload(body, onError) {
|
|
|
548
564
|
return {
|
|
549
565
|
...message,
|
|
550
566
|
content: content.map((block) => {
|
|
551
|
-
if (!block || typeof block !==
|
|
567
|
+
if (!block || typeof block !== "object")
|
|
552
568
|
return block;
|
|
553
569
|
const b = block;
|
|
554
|
-
if (b.type !==
|
|
570
|
+
if (b.type !== "tool_use" || typeof b.name !== "string")
|
|
555
571
|
return block;
|
|
556
|
-
return {
|
|
572
|
+
return {
|
|
573
|
+
...block,
|
|
574
|
+
name: toClaudeCodeToolName(b.name),
|
|
575
|
+
};
|
|
557
576
|
}),
|
|
558
577
|
};
|
|
559
578
|
});
|
|
@@ -561,7 +580,11 @@ function rewriteRequestPayload(body, onError) {
|
|
|
561
580
|
return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
|
|
562
581
|
}
|
|
563
582
|
catch {
|
|
564
|
-
return {
|
|
583
|
+
return {
|
|
584
|
+
body,
|
|
585
|
+
modelId: undefined,
|
|
586
|
+
reverseToolNameMap: new Map(),
|
|
587
|
+
};
|
|
565
588
|
}
|
|
566
589
|
}
|
|
567
590
|
function wrapResponseStream(response, reverseToolNameMap) {
|
|
@@ -570,7 +593,7 @@ function wrapResponseStream(response, reverseToolNameMap) {
|
|
|
570
593
|
const reader = response.body.getReader();
|
|
571
594
|
const decoder = new TextDecoder();
|
|
572
595
|
const encoder = new TextEncoder();
|
|
573
|
-
let carry =
|
|
596
|
+
let carry = "";
|
|
574
597
|
const transform = (text) => {
|
|
575
598
|
return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name) => {
|
|
576
599
|
const original = reverseToolNameMap.get(name);
|
|
@@ -613,11 +636,15 @@ function appendToastSessionMarker({ message, sessionId, }) {
|
|
|
613
636
|
}
|
|
614
637
|
// --- Beta headers ---
|
|
615
638
|
function getRequiredBetas(modelId) {
|
|
616
|
-
const betas = [
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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");
|
|
621
648
|
if (!isAdaptive)
|
|
622
649
|
betas.push(INTERLEAVED_THINKING_BETA);
|
|
623
650
|
return betas;
|
|
@@ -626,16 +653,16 @@ function mergeBetas(existing, required) {
|
|
|
626
653
|
return [
|
|
627
654
|
...new Set([
|
|
628
655
|
...required,
|
|
629
|
-
...(existing ||
|
|
630
|
-
.split(
|
|
656
|
+
...(existing || "")
|
|
657
|
+
.split(",")
|
|
631
658
|
.map((s) => s.trim())
|
|
632
659
|
.filter(Boolean),
|
|
633
660
|
]),
|
|
634
|
-
].join(
|
|
661
|
+
].join(",");
|
|
635
662
|
}
|
|
636
663
|
// --- Token refresh with dedup ---
|
|
637
664
|
function isOAuthStored(auth) {
|
|
638
|
-
return auth.type ===
|
|
665
|
+
return auth.type === "oauth";
|
|
639
666
|
}
|
|
640
667
|
async function getFreshOAuth(getAuth, client) {
|
|
641
668
|
const auth = await getAuth();
|
|
@@ -650,7 +677,7 @@ async function getFreshOAuth(getAuth, client) {
|
|
|
650
677
|
const refreshPromise = withAuthStateLock(async () => {
|
|
651
678
|
const latest = await getAuth();
|
|
652
679
|
if (!isOAuthStored(latest)) {
|
|
653
|
-
throw new Error(
|
|
680
|
+
throw new Error("Anthropic OAuth credentials disappeared during refresh");
|
|
654
681
|
}
|
|
655
682
|
if (latest.access && latest.expires > Date.now())
|
|
656
683
|
return latest;
|
|
@@ -660,7 +687,8 @@ async function getFreshOAuth(getAuth, client) {
|
|
|
660
687
|
if (store.accounts.length > 0) {
|
|
661
688
|
const identity = (() => {
|
|
662
689
|
const currentIndex = store.accounts.findIndex((account) => {
|
|
663
|
-
return account.refresh === latest.refresh ||
|
|
690
|
+
return (account.refresh === latest.refresh ||
|
|
691
|
+
account.access === latest.access);
|
|
664
692
|
});
|
|
665
693
|
const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
|
|
666
694
|
if (!current)
|
|
@@ -680,27 +708,26 @@ async function getFreshOAuth(getAuth, client) {
|
|
|
680
708
|
pendingRefresh.delete(auth.refresh);
|
|
681
709
|
});
|
|
682
710
|
}
|
|
683
|
-
// --- Plugin export ---
|
|
684
711
|
const AnthropicAuthPlugin = async ({ client }) => {
|
|
685
712
|
return {
|
|
686
|
-
|
|
687
|
-
if (input.model.providerID !==
|
|
713
|
+
"chat.headers": async (input, output) => {
|
|
714
|
+
if (input.model.providerID !== "anthropic") {
|
|
688
715
|
return;
|
|
689
716
|
}
|
|
690
717
|
output.headers[TOAST_SESSION_HEADER] = input.sessionID;
|
|
691
718
|
},
|
|
692
719
|
auth: {
|
|
693
|
-
provider:
|
|
720
|
+
provider: "anthropic",
|
|
694
721
|
async loader(getAuth, provider) {
|
|
695
722
|
const auth = await getAuth();
|
|
696
|
-
if (auth.type !==
|
|
723
|
+
if (auth.type !== "oauth")
|
|
697
724
|
return {};
|
|
698
725
|
// Zero out costs for OAuth users (Claude Pro/Max subscription)
|
|
699
726
|
for (const model of Object.values(provider.models)) {
|
|
700
727
|
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
|
|
701
728
|
}
|
|
702
729
|
return {
|
|
703
|
-
apiKey:
|
|
730
|
+
apiKey: "",
|
|
704
731
|
async fetch(input, init) {
|
|
705
732
|
const url = (() => {
|
|
706
733
|
try {
|
|
@@ -712,7 +739,7 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
712
739
|
})();
|
|
713
740
|
if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
|
|
714
741
|
return fetch(input, init);
|
|
715
|
-
const originalBody = typeof init?.body ===
|
|
742
|
+
const originalBody = typeof init?.body === "string"
|
|
716
743
|
? init.body
|
|
717
744
|
: input instanceof Request
|
|
718
745
|
? await input
|
|
@@ -729,24 +756,30 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
729
756
|
}
|
|
730
757
|
const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
|
|
731
758
|
const rewritten = rewriteRequestPayload(originalBody, (msg) => {
|
|
732
|
-
client.tui
|
|
759
|
+
client.tui
|
|
760
|
+
.showToast({
|
|
733
761
|
body: {
|
|
734
|
-
message: appendToastSessionMarker({
|
|
735
|
-
|
|
762
|
+
message: appendToastSessionMarker({
|
|
763
|
+
message: msg,
|
|
764
|
+
sessionId,
|
|
765
|
+
}),
|
|
766
|
+
variant: "error",
|
|
736
767
|
},
|
|
737
|
-
})
|
|
768
|
+
})
|
|
769
|
+
.catch(() => { });
|
|
738
770
|
});
|
|
739
771
|
const betas = getRequiredBetas(rewritten.modelId);
|
|
740
772
|
const runRequest = async (auth) => {
|
|
741
773
|
const requestHeaders = new Headers(headers);
|
|
742
774
|
requestHeaders.delete(TOAST_SESSION_HEADER);
|
|
743
|
-
requestHeaders.set(
|
|
744
|
-
requestHeaders.set(
|
|
745
|
-
requestHeaders.set(
|
|
746
|
-
requestHeaders.set(
|
|
747
|
-
requestHeaders.set(
|
|
748
|
-
|
|
749
|
-
requestHeaders.
|
|
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");
|
|
750
783
|
return fetch(input, {
|
|
751
784
|
...(init ?? {}),
|
|
752
785
|
body: rewritten.body,
|
|
@@ -761,20 +794,22 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
761
794
|
const bodyText = await response
|
|
762
795
|
.clone()
|
|
763
796
|
.text()
|
|
764
|
-
.catch(() =>
|
|
797
|
+
.catch(() => "");
|
|
765
798
|
if (shouldRotateAuth(response.status, bodyText)) {
|
|
766
799
|
const rotated = await rotateAnthropicAccount(freshAuth, client);
|
|
767
800
|
if (rotated) {
|
|
768
801
|
// Show toast notification so Discord thread shows the rotation
|
|
769
|
-
client.tui
|
|
802
|
+
client.tui
|
|
803
|
+
.showToast({
|
|
770
804
|
body: {
|
|
771
805
|
message: appendToastSessionMarker({
|
|
772
806
|
message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
|
|
773
807
|
sessionId,
|
|
774
808
|
}),
|
|
775
|
-
variant:
|
|
809
|
+
variant: "info",
|
|
776
810
|
},
|
|
777
|
-
})
|
|
811
|
+
})
|
|
812
|
+
.catch(() => { });
|
|
778
813
|
const retryAuth = await getFreshOAuth(getAuth, client);
|
|
779
814
|
if (retryAuth) {
|
|
780
815
|
response = await runRequest(retryAuth);
|
|
@@ -788,22 +823,36 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
788
823
|
},
|
|
789
824
|
methods: [
|
|
790
825
|
{
|
|
791
|
-
label:
|
|
792
|
-
type:
|
|
793
|
-
authorize: buildAuthorizeHandler(
|
|
826
|
+
label: "Claude Pro/Max",
|
|
827
|
+
type: "oauth",
|
|
828
|
+
authorize: buildAuthorizeHandler("oauth"),
|
|
794
829
|
},
|
|
795
830
|
{
|
|
796
|
-
label:
|
|
797
|
-
type:
|
|
798
|
-
authorize: buildAuthorizeHandler(
|
|
831
|
+
label: "Create an API Key",
|
|
832
|
+
type: "oauth",
|
|
833
|
+
authorize: buildAuthorizeHandler("apikey"),
|
|
799
834
|
},
|
|
800
835
|
{
|
|
801
|
-
provider:
|
|
802
|
-
label:
|
|
803
|
-
type:
|
|
836
|
+
provider: "anthropic",
|
|
837
|
+
label: "Manually enter API Key",
|
|
838
|
+
type: "api",
|
|
804
839
|
},
|
|
805
840
|
],
|
|
806
841
|
},
|
|
807
842
|
};
|
|
808
843
|
};
|
|
809
|
-
|
|
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 };
|