@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.
- package/dist/agent-model.e2e.test.js +7 -1
- package/dist/anthropic-auth-plugin.js +227 -176
- package/dist/cli-send-thread.e2e.test.js +4 -7
- package/dist/cli.js +2 -2
- package/dist/commands/login.js +6 -4
- package/dist/commands/screenshare.js +1 -1
- package/dist/commands/screenshare.test.js +2 -2
- package/dist/commands/vscode.js +269 -0
- package/dist/context-awareness-plugin.js +8 -38
- package/dist/db.js +1 -0
- package/dist/discord-command-registration.js +5 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
- package/dist/interaction-handler.js +4 -0
- package/dist/kimaki-opencode-plugin.js +3 -1
- package/dist/memory-overview-plugin.js +126 -0
- package/dist/system-message.js +23 -22
- package/dist/system-message.test.js +23 -22
- package/dist/system-prompt-drift-plugin.js +41 -11
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/src/agent-model.e2e.test.ts +8 -1
- package/src/anthropic-auth-plugin.ts +574 -451
- package/src/cli-send-thread.e2e.test.ts +6 -7
- package/src/cli.ts +2 -2
- package/src/commands/login.ts +6 -4
- package/src/commands/screenshare.test.ts +2 -2
- package/src/commands/screenshare.ts +1 -1
- package/src/commands/vscode.ts +342 -0
- package/src/context-awareness-plugin.ts +11 -42
- package/src/db.ts +1 -0
- package/src/discord-command-registration.ts +7 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
- package/src/interaction-handler.ts +5 -0
- package/src/kimaki-opencode-plugin.ts +3 -1
- package/src/memory-overview-plugin.ts +161 -0
- package/src/system-message.test.ts +23 -22
- package/src/system-message.ts +23 -22
- package/src/system-prompt-drift-plugin.ts +48 -12
- 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
|
|
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,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
|
|
463
|
+
function sanitizeAnthropicSystemText(text, onError) {
|
|
460
464
|
const startIdx = text.indexOf(OPENCODE_IDENTITY);
|
|
461
465
|
if (startIdx === -1)
|
|
462
466
|
return text;
|
|
463
|
-
|
|
464
|
-
const endIdx = text.indexOf(
|
|
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?.(
|
|
470
|
+
onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
|
|
467
471
|
return text;
|
|
468
472
|
}
|
|
469
|
-
|
|
470
|
-
|
|
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:
|
|
474
|
-
if (typeof system ===
|
|
493
|
+
const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
|
|
494
|
+
if (typeof system === "undefined")
|
|
475
495
|
return [identityBlock];
|
|
476
|
-
if (typeof system ===
|
|
477
|
-
const sanitized =
|
|
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:
|
|
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
|
-
|
|
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 ===
|
|
498
|
-
|
|
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 {
|
|
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 ===
|
|
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 !==
|
|
532
|
+
if (!tool || typeof tool !== "object")
|
|
515
533
|
return tool;
|
|
516
534
|
const name = tool.name;
|
|
517
|
-
if (typeof name !==
|
|
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 ===
|
|
529
|
-
payload.tool_choice.type ===
|
|
546
|
+
typeof payload.tool_choice === "object" &&
|
|
547
|
+
payload.tool_choice.type === "tool") {
|
|
530
548
|
const name = payload.tool_choice.name;
|
|
531
|
-
if (typeof name ===
|
|
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 !==
|
|
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 !==
|
|
567
|
+
if (!block || typeof block !== "object")
|
|
550
568
|
return block;
|
|
551
569
|
const b = block;
|
|
552
|
-
if (b.type !==
|
|
570
|
+
if (b.type !== "tool_use" || typeof b.name !== "string")
|
|
553
571
|
return block;
|
|
554
|
-
return {
|
|
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 {
|
|
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 = [
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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 ===
|
|
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(
|
|
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 ||
|
|
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
|
-
|
|
685
|
-
if (input.model.providerID !==
|
|
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:
|
|
720
|
+
provider: "anthropic",
|
|
692
721
|
async loader(getAuth, provider) {
|
|
693
722
|
const auth = await getAuth();
|
|
694
|
-
if (auth.type !==
|
|
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 ===
|
|
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
|
|
759
|
+
client.tui
|
|
760
|
+
.showToast({
|
|
731
761
|
body: {
|
|
732
|
-
message: appendToastSessionMarker({
|
|
733
|
-
|
|
762
|
+
message: appendToastSessionMarker({
|
|
763
|
+
message: msg,
|
|
764
|
+
sessionId,
|
|
765
|
+
}),
|
|
766
|
+
variant: "error",
|
|
734
767
|
},
|
|
735
|
-
})
|
|
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(
|
|
742
|
-
requestHeaders.set(
|
|
743
|
-
requestHeaders.set(
|
|
744
|
-
requestHeaders.set(
|
|
745
|
-
requestHeaders.set(
|
|
746
|
-
|
|
747
|
-
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");
|
|
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
|
|
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:
|
|
809
|
+
variant: "info",
|
|
774
810
|
},
|
|
775
|
-
})
|
|
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:
|
|
790
|
-
type:
|
|
791
|
-
authorize: buildAuthorizeHandler(
|
|
826
|
+
label: "Claude Pro/Max",
|
|
827
|
+
type: "oauth",
|
|
828
|
+
authorize: buildAuthorizeHandler("oauth"),
|
|
792
829
|
},
|
|
793
830
|
{
|
|
794
|
-
label:
|
|
795
|
-
type:
|
|
796
|
-
authorize: buildAuthorizeHandler(
|
|
831
|
+
label: "Create an API Key",
|
|
832
|
+
type: "oauth",
|
|
833
|
+
authorize: buildAuthorizeHandler("apikey"),
|
|
797
834
|
},
|
|
798
835
|
{
|
|
799
|
-
provider:
|
|
800
|
-
label:
|
|
801
|
-
type:
|
|
836
|
+
provider: "anthropic",
|
|
837
|
+
label: "Manually enter API Key",
|
|
838
|
+
type: "api",
|
|
802
839
|
},
|
|
803
840
|
],
|
|
804
841
|
},
|
|
805
842
|
};
|
|
806
843
|
};
|
|
807
|
-
|
|
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 };
|