opencode-claude-code-wrapper 0.0.1 → 0.0.2
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/index.mjs +322 -54
- package/package.json +4 -1
package/index.mjs
CHANGED
|
@@ -1,18 +1,80 @@
|
|
|
1
|
+
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
1
2
|
import {
|
|
2
3
|
transformRequestToCLIArgs,
|
|
3
4
|
spawnClaudeCode,
|
|
4
|
-
generateId,
|
|
5
5
|
} from "./lib/cli-runner.mjs";
|
|
6
6
|
import {
|
|
7
7
|
JSONLToSSETransformer,
|
|
8
8
|
buildCompleteResponse,
|
|
9
9
|
} from "./lib/transformer.mjs";
|
|
10
10
|
|
|
11
|
+
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {"max" | "console"} mode
|
|
15
|
+
*/
|
|
16
|
+
async function authorize(mode) {
|
|
17
|
+
const pkce = await generatePKCE();
|
|
18
|
+
|
|
19
|
+
const url = new URL(
|
|
20
|
+
`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
|
|
21
|
+
import.meta.url
|
|
22
|
+
);
|
|
23
|
+
url.searchParams.set("code", "true");
|
|
24
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
25
|
+
url.searchParams.set("response_type", "code");
|
|
26
|
+
url.searchParams.set(
|
|
27
|
+
"redirect_uri",
|
|
28
|
+
"https://console.anthropic.com/oauth/code/callback"
|
|
29
|
+
);
|
|
30
|
+
url.searchParams.set(
|
|
31
|
+
"scope",
|
|
32
|
+
"org:create_api_key user:profile user:inference"
|
|
33
|
+
);
|
|
34
|
+
url.searchParams.set("code_challenge", pkce.challenge);
|
|
35
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
36
|
+
url.searchParams.set("state", pkce.verifier);
|
|
37
|
+
return {
|
|
38
|
+
url: url.toString(),
|
|
39
|
+
verifier: pkce.verifier,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} code
|
|
45
|
+
* @param {string} verifier
|
|
46
|
+
*/
|
|
47
|
+
async function exchange(code, verifier) {
|
|
48
|
+
const splits = code.split("#");
|
|
49
|
+
const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
code: splits[0],
|
|
56
|
+
state: splits[1],
|
|
57
|
+
grant_type: "authorization_code",
|
|
58
|
+
client_id: CLIENT_ID,
|
|
59
|
+
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
|
60
|
+
code_verifier: verifier,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
if (!result.ok)
|
|
64
|
+
return {
|
|
65
|
+
type: "failed",
|
|
66
|
+
};
|
|
67
|
+
const json = await result.json();
|
|
68
|
+
return {
|
|
69
|
+
type: "success",
|
|
70
|
+
refresh: json.refresh_token,
|
|
71
|
+
access: json.access_token,
|
|
72
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
11
76
|
/**
|
|
12
77
|
* Create an error response
|
|
13
|
-
* @param {Error|string} error - The error
|
|
14
|
-
* @param {number} statusCode - HTTP status code
|
|
15
|
-
* @returns {Response} Error response
|
|
16
78
|
*/
|
|
17
79
|
function createErrorResponse(error, statusCode = 500) {
|
|
18
80
|
return new Response(
|
|
@@ -32,9 +94,6 @@ function createErrorResponse(error, statusCode = 500) {
|
|
|
32
94
|
|
|
33
95
|
/**
|
|
34
96
|
* Create a streaming response from Claude Code process
|
|
35
|
-
* @param {ChildProcess} child - Claude Code child process
|
|
36
|
-
* @param {object} requestBody - Original request body
|
|
37
|
-
* @returns {Response} Streaming response
|
|
38
97
|
*/
|
|
39
98
|
function createStreamingResponse(child, requestBody) {
|
|
40
99
|
const transformer = new JSONLToSSETransformer(requestBody);
|
|
@@ -59,12 +118,10 @@ function createStreamingResponse(child, requestBody) {
|
|
|
59
118
|
});
|
|
60
119
|
|
|
61
120
|
child.stderr.on("data", (chunk) => {
|
|
62
|
-
// Log stderr for debugging but don't fail
|
|
63
121
|
console.error("[claude-code-wrapper] stderr:", chunk.toString());
|
|
64
122
|
});
|
|
65
123
|
|
|
66
124
|
child.on("close", (code) => {
|
|
67
|
-
// Process any remaining buffer
|
|
68
125
|
if (buffer.trim()) {
|
|
69
126
|
const sseEvents = transformer.transformLine(buffer);
|
|
70
127
|
if (sseEvents) {
|
|
@@ -72,7 +129,6 @@ function createStreamingResponse(child, requestBody) {
|
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
131
|
|
|
75
|
-
// Send final events if not already sent
|
|
76
132
|
if (!finalized) {
|
|
77
133
|
finalized = true;
|
|
78
134
|
const finalEvents = transformer.finalize();
|
|
@@ -105,12 +161,9 @@ function createStreamingResponse(child, requestBody) {
|
|
|
105
161
|
|
|
106
162
|
/**
|
|
107
163
|
* Create a non-streaming response from Claude Code process
|
|
108
|
-
* @param {ChildProcess} child - Claude Code child process
|
|
109
|
-
* @param {object} requestBody - Original request body
|
|
110
|
-
* @returns {Promise<Response>} Complete response
|
|
111
164
|
*/
|
|
112
165
|
async function createNonStreamingResponse(child, requestBody) {
|
|
113
|
-
return new Promise((resolve
|
|
166
|
+
return new Promise((resolve) => {
|
|
114
167
|
let stdout = "";
|
|
115
168
|
let stderr = "";
|
|
116
169
|
|
|
@@ -158,13 +211,9 @@ async function createNonStreamingResponse(child, requestBody) {
|
|
|
158
211
|
}
|
|
159
212
|
|
|
160
213
|
/**
|
|
161
|
-
* Handle
|
|
162
|
-
* @param {Request|string} input - Fetch input
|
|
163
|
-
* @param {RequestInit} init - Fetch init options
|
|
164
|
-
* @returns {Promise<Response>} Response
|
|
214
|
+
* Handle request via Claude Code CLI
|
|
165
215
|
*/
|
|
166
216
|
async function handleClaudeCodeRequest(input, init) {
|
|
167
|
-
// Parse the incoming request URL
|
|
168
217
|
let requestUrl;
|
|
169
218
|
try {
|
|
170
219
|
if (typeof input === "string" || input instanceof URL) {
|
|
@@ -176,12 +225,10 @@ async function handleClaudeCodeRequest(input, init) {
|
|
|
176
225
|
requestUrl = null;
|
|
177
226
|
}
|
|
178
227
|
|
|
179
|
-
// Only intercept messages endpoint
|
|
180
228
|
if (!requestUrl || !requestUrl.pathname.includes("/v1/messages")) {
|
|
181
229
|
return fetch(input, init);
|
|
182
230
|
}
|
|
183
231
|
|
|
184
|
-
// Parse request body
|
|
185
232
|
let requestBody;
|
|
186
233
|
try {
|
|
187
234
|
let bodyStr = "";
|
|
@@ -198,13 +245,8 @@ async function handleClaudeCodeRequest(input, init) {
|
|
|
198
245
|
});
|
|
199
246
|
}
|
|
200
247
|
|
|
201
|
-
// Check if streaming is requested
|
|
202
248
|
const isStreaming = requestBody.stream === true;
|
|
203
|
-
|
|
204
|
-
// Transform request to CLI args
|
|
205
249
|
const cliArgs = transformRequestToCLIArgs(requestBody);
|
|
206
|
-
|
|
207
|
-
// Spawn Claude Code process
|
|
208
250
|
const child = spawnClaudeCode(cliArgs, { streaming: isStreaming });
|
|
209
251
|
|
|
210
252
|
if (isStreaming) {
|
|
@@ -215,7 +257,7 @@ async function handleClaudeCodeRequest(input, init) {
|
|
|
215
257
|
}
|
|
216
258
|
|
|
217
259
|
/**
|
|
218
|
-
* OpenCode plugin that wraps Claude Code CLI
|
|
260
|
+
* OpenCode plugin that wraps Claude Code CLI with full auth options
|
|
219
261
|
* @type {import('@opencode-ai/plugin').Plugin}
|
|
220
262
|
*/
|
|
221
263
|
export async function ClaudeCodeWrapperPlugin({ client }) {
|
|
@@ -223,46 +265,272 @@ export async function ClaudeCodeWrapperPlugin({ client }) {
|
|
|
223
265
|
auth: {
|
|
224
266
|
provider: "anthropic",
|
|
225
267
|
async loader(getAuth, provider) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
268
|
+
const auth = await getAuth();
|
|
269
|
+
|
|
270
|
+
// Claude Code CLI auth - zero cost, use CLI
|
|
271
|
+
if (auth.type === "claude-code") {
|
|
272
|
+
for (const model of Object.values(provider.models)) {
|
|
273
|
+
model.cost = {
|
|
274
|
+
input: 0,
|
|
275
|
+
output: 0,
|
|
276
|
+
cache: { read: 0, write: 0 },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
apiKey: "",
|
|
282
|
+
async fetch(input, init) {
|
|
283
|
+
try {
|
|
284
|
+
return await handleClaudeCodeRequest(input, init);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("[claude-code-wrapper] error:", error);
|
|
287
|
+
return createErrorResponse(error);
|
|
288
|
+
}
|
|
234
289
|
},
|
|
235
290
|
};
|
|
236
291
|
}
|
|
237
292
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
// OAuth auth (Claude Pro/Max)
|
|
294
|
+
if (auth.type === "oauth") {
|
|
295
|
+
for (const model of Object.values(provider.models)) {
|
|
296
|
+
model.cost = {
|
|
297
|
+
input: 0,
|
|
298
|
+
output: 0,
|
|
299
|
+
cache: { read: 0, write: 0 },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
apiKey: "",
|
|
305
|
+
async fetch(input, init) {
|
|
306
|
+
const auth = await getAuth();
|
|
307
|
+
if (auth.type !== "oauth") return fetch(input, init);
|
|
308
|
+
|
|
309
|
+
// Refresh token if expired
|
|
310
|
+
if (!auth.access || auth.expires < Date.now()) {
|
|
311
|
+
const response = await fetch(
|
|
312
|
+
"https://console.anthropic.com/v1/oauth/token",
|
|
313
|
+
{
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: { "Content-Type": "application/json" },
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
grant_type: "refresh_token",
|
|
318
|
+
refresh_token: auth.refresh,
|
|
319
|
+
client_id: CLIENT_ID,
|
|
320
|
+
}),
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
325
|
+
}
|
|
326
|
+
const json = await response.json();
|
|
327
|
+
await client.auth.set({
|
|
328
|
+
path: { id: "anthropic" },
|
|
329
|
+
body: {
|
|
330
|
+
type: "oauth",
|
|
331
|
+
refresh: json.refresh_token,
|
|
332
|
+
access: json.access_token,
|
|
333
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
auth.access = json.access_token;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const requestInit = init ?? {};
|
|
340
|
+
const requestHeaders = new Headers();
|
|
341
|
+
|
|
342
|
+
if (input instanceof Request) {
|
|
343
|
+
input.headers.forEach((value, key) => {
|
|
344
|
+
requestHeaders.set(key, value);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (requestInit.headers) {
|
|
348
|
+
if (requestInit.headers instanceof Headers) {
|
|
349
|
+
requestInit.headers.forEach((value, key) => {
|
|
350
|
+
requestHeaders.set(key, value);
|
|
351
|
+
});
|
|
352
|
+
} else if (Array.isArray(requestInit.headers)) {
|
|
353
|
+
for (const [key, value] of requestInit.headers) {
|
|
354
|
+
if (typeof value !== "undefined") {
|
|
355
|
+
requestHeaders.set(key, String(value));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
for (const [key, value] of Object.entries(
|
|
360
|
+
requestInit.headers
|
|
361
|
+
)) {
|
|
362
|
+
if (typeof value !== "undefined") {
|
|
363
|
+
requestHeaders.set(key, String(value));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
370
|
+
const incomingBetasList = incomingBeta
|
|
371
|
+
.split(",")
|
|
372
|
+
.map((b) => b.trim())
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
|
|
375
|
+
const includeClaudeCode = incomingBetasList.includes(
|
|
376
|
+
"claude-code-20250219"
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const mergedBetas = [
|
|
380
|
+
"oauth-2025-04-20",
|
|
381
|
+
"interleaved-thinking-2025-05-14",
|
|
382
|
+
...(includeClaudeCode ? ["claude-code-20250219"] : []),
|
|
383
|
+
].join(",");
|
|
384
|
+
|
|
385
|
+
requestHeaders.set("authorization", `Bearer ${auth.access}`);
|
|
386
|
+
requestHeaders.set("anthropic-beta", mergedBetas);
|
|
387
|
+
requestHeaders.set(
|
|
388
|
+
"user-agent",
|
|
389
|
+
"claude-cli/2.1.2 (external, cli)"
|
|
390
|
+
);
|
|
391
|
+
requestHeaders.delete("x-api-key");
|
|
392
|
+
|
|
393
|
+
const TOOL_PREFIX = "oc_";
|
|
394
|
+
let body = requestInit.body;
|
|
395
|
+
if (body && typeof body === "string") {
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(body);
|
|
398
|
+
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
399
|
+
parsed.tools = parsed.tools.map((tool) => ({
|
|
400
|
+
...tool,
|
|
401
|
+
name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name,
|
|
402
|
+
}));
|
|
403
|
+
body = JSON.stringify(parsed);
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {
|
|
406
|
+
// ignore parse errors
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let requestInput = input;
|
|
411
|
+
let requestUrl = null;
|
|
412
|
+
try {
|
|
413
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
414
|
+
requestUrl = new URL(input.toString());
|
|
415
|
+
} else if (input instanceof Request) {
|
|
416
|
+
requestUrl = new URL(input.url);
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
requestUrl = null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (
|
|
423
|
+
requestUrl &&
|
|
424
|
+
requestUrl.pathname === "/v1/messages" &&
|
|
425
|
+
!requestUrl.searchParams.has("beta")
|
|
426
|
+
) {
|
|
427
|
+
requestUrl.searchParams.set("beta", "true");
|
|
428
|
+
requestInput =
|
|
429
|
+
input instanceof Request
|
|
430
|
+
? new Request(requestUrl.toString(), input)
|
|
431
|
+
: requestUrl;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const response = await fetch(requestInput, {
|
|
435
|
+
...requestInit,
|
|
436
|
+
body,
|
|
437
|
+
headers: requestHeaders,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Transform streaming response to rename tools back
|
|
441
|
+
if (response.body) {
|
|
442
|
+
const reader = response.body.getReader();
|
|
443
|
+
const decoder = new TextDecoder();
|
|
444
|
+
const encoder = new TextEncoder();
|
|
445
|
+
|
|
446
|
+
const stream = new ReadableStream({
|
|
447
|
+
async pull(controller) {
|
|
448
|
+
const { done, value } = await reader.read();
|
|
449
|
+
if (done) {
|
|
450
|
+
controller.close();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let text = decoder.decode(value, { stream: true });
|
|
455
|
+
text = text.replace(
|
|
456
|
+
/"name"\s*:\s*"oc_([^"]+)"/g,
|
|
457
|
+
'"name": "$1"'
|
|
458
|
+
);
|
|
459
|
+
controller.enqueue(encoder.encode(text));
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return new Response(stream, {
|
|
464
|
+
status: response.status,
|
|
465
|
+
statusText: response.statusText,
|
|
466
|
+
headers: response.headers,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return response;
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Default - no special handling
|
|
476
|
+
return {};
|
|
254
477
|
},
|
|
255
478
|
methods: [
|
|
256
479
|
{
|
|
257
480
|
label: "Claude Code CLI",
|
|
481
|
+
type: "claude-code",
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
label: "Claude Pro/Max",
|
|
485
|
+
type: "oauth",
|
|
486
|
+
authorize: async () => {
|
|
487
|
+
const { url, verifier } = await authorize("max");
|
|
488
|
+
return {
|
|
489
|
+
url: url,
|
|
490
|
+
instructions: "Paste the authorization code here: ",
|
|
491
|
+
method: "code",
|
|
492
|
+
callback: async (code) => {
|
|
493
|
+
const credentials = await exchange(code, verifier);
|
|
494
|
+
return credentials;
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
label: "Create an API Key",
|
|
501
|
+
type: "oauth",
|
|
502
|
+
authorize: async () => {
|
|
503
|
+
const { url, verifier } = await authorize("console");
|
|
504
|
+
return {
|
|
505
|
+
url: url,
|
|
506
|
+
instructions: "Paste the authorization code here: ",
|
|
507
|
+
method: "code",
|
|
508
|
+
callback: async (code) => {
|
|
509
|
+
const credentials = await exchange(code, verifier);
|
|
510
|
+
if (credentials.type === "failed") return credentials;
|
|
511
|
+
const result = await fetch(
|
|
512
|
+
`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`,
|
|
513
|
+
{
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: {
|
|
516
|
+
"Content-Type": "application/json",
|
|
517
|
+
authorization: `Bearer ${credentials.access}`,
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
).then((r) => r.json());
|
|
521
|
+
return { type: "success", key: result.raw_key };
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
provider: "anthropic",
|
|
528
|
+
label: "Manually enter API Key",
|
|
258
529
|
type: "api",
|
|
259
|
-
// Simple passthrough - Claude Code handles auth via its own config
|
|
260
|
-
// Users should run 'claude setup-token' or set ANTHROPIC_API_KEY
|
|
261
530
|
},
|
|
262
531
|
],
|
|
263
532
|
},
|
|
264
533
|
};
|
|
265
534
|
}
|
|
266
535
|
|
|
267
|
-
// Default export for convenience
|
|
268
536
|
export default ClaudeCodeWrapperPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-code-wrapper",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "OpenCode plugin that wraps Claude Code CLI for API-like access",
|
|
5
5
|
"main": "./index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"type": "git",
|
|
26
26
|
"url": "https://github.com/elad12390/opencode-claude-code-cli-wrapper"
|
|
27
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@openauthjs/openauth": "^0.4.3"
|
|
30
|
+
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@opencode-ai/plugin": "^0.4.45"
|
|
30
33
|
}
|