cccproxy 1.0.0

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 (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +673 -0
  3. package/package.json +30 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruslan Yakushev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "util";
5
+ import { serve } from "@hono/node-server";
6
+
7
+ // src/auth/token.ts
8
+ import fs from "fs/promises";
9
+ import os from "os";
10
+ import path from "path";
11
+
12
+ // src/config.ts
13
+ import { randomUUID } from "crypto";
14
+ var GITHUB_BASE_URL = "https://github.com";
15
+ var GITHUB_API_BASE_URL = "https://api.github.com";
16
+ var GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
17
+ var GITHUB_APP_SCOPES = "read:user";
18
+ var COPILOT_VERSION = "0.26.7";
19
+ var EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
20
+ var USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
21
+ var API_VERSION = "2025-04-01";
22
+ var standardHeaders = () => ({
23
+ "content-type": "application/json",
24
+ accept: "application/json"
25
+ });
26
+ var copilotBaseUrl = (state2) => state2.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state2.accountType}.githubcopilot.com`;
27
+ var copilotHeaders = (state2, vision = false) => {
28
+ const headers = {
29
+ Authorization: `Bearer ${state2.copilotToken}`,
30
+ "content-type": "application/json",
31
+ "copilot-integration-id": "vscode-chat",
32
+ "editor-version": `vscode/${state2.vsCodeVersion}`,
33
+ "editor-plugin-version": EDITOR_PLUGIN_VERSION,
34
+ "user-agent": USER_AGENT,
35
+ "openai-intent": "conversation-panel",
36
+ "x-github-api-version": API_VERSION,
37
+ "x-request-id": randomUUID(),
38
+ "x-vscode-user-agent-library-version": "electron-fetch"
39
+ };
40
+ if (vision) headers["copilot-vision-request"] = "true";
41
+ return headers;
42
+ };
43
+ var githubHeaders = (state2) => ({
44
+ ...standardHeaders(),
45
+ authorization: `token ${state2.githubToken}`,
46
+ "editor-version": `vscode/${state2.vsCodeVersion}`,
47
+ "editor-plugin-version": EDITOR_PLUGIN_VERSION,
48
+ "user-agent": USER_AGENT,
49
+ "x-github-api-version": API_VERSION,
50
+ "x-vscode-user-agent-library-version": "electron-fetch"
51
+ });
52
+
53
+ // src/auth/device-code.ts
54
+ async function getDeviceCode() {
55
+ const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
56
+ method: "POST",
57
+ headers: standardHeaders(),
58
+ body: JSON.stringify({
59
+ client_id: GITHUB_CLIENT_ID,
60
+ scope: GITHUB_APP_SCOPES
61
+ })
62
+ });
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to get device code: ${response.status}`);
65
+ }
66
+ return await response.json();
67
+ }
68
+ async function pollAccessToken(deviceCode) {
69
+ const sleepDuration = (deviceCode.interval + 1) * 1e3;
70
+ while (true) {
71
+ const response = await fetch(
72
+ `${GITHUB_BASE_URL}/login/oauth/access_token`,
73
+ {
74
+ method: "POST",
75
+ headers: standardHeaders(),
76
+ body: JSON.stringify({
77
+ client_id: GITHUB_CLIENT_ID,
78
+ device_code: deviceCode.device_code,
79
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
80
+ })
81
+ }
82
+ );
83
+ if (!response.ok) {
84
+ await new Promise((resolve) => setTimeout(resolve, sleepDuration));
85
+ continue;
86
+ }
87
+ const json = await response.json();
88
+ if (json.access_token) {
89
+ return json.access_token;
90
+ }
91
+ await new Promise((resolve) => setTimeout(resolve, sleepDuration));
92
+ }
93
+ }
94
+
95
+ // src/state.ts
96
+ var state = {
97
+ accountType: "individual",
98
+ vsCodeVersion: "1.104.3"
99
+ };
100
+
101
+ // src/auth/copilot-token.ts
102
+ async function getCopilotToken() {
103
+ const response = await fetch(
104
+ `${GITHUB_API_BASE_URL}/copilot_internal/v2/token`,
105
+ {
106
+ headers: githubHeaders(state)
107
+ }
108
+ );
109
+ if (!response.ok) {
110
+ throw new Error(`Failed to get Copilot token: ${response.status}`);
111
+ }
112
+ return await response.json();
113
+ }
114
+
115
+ // src/auth/token.ts
116
+ var APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
117
+ var GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
118
+ async function ensurePaths() {
119
+ await fs.mkdir(APP_DIR, { recursive: true });
120
+ try {
121
+ await fs.access(GITHUB_TOKEN_PATH, fs.constants.W_OK);
122
+ } catch {
123
+ await fs.writeFile(GITHUB_TOKEN_PATH, "");
124
+ await fs.chmod(GITHUB_TOKEN_PATH, 384);
125
+ }
126
+ }
127
+ async function readGithubToken() {
128
+ return fs.readFile(GITHUB_TOKEN_PATH, "utf8");
129
+ }
130
+ async function writeGithubToken(token) {
131
+ await fs.writeFile(GITHUB_TOKEN_PATH, token);
132
+ }
133
+ async function setupGitHubToken() {
134
+ await ensurePaths();
135
+ try {
136
+ const githubToken = await readGithubToken();
137
+ if (githubToken) {
138
+ state.githubToken = githubToken;
139
+ console.log("Using saved GitHub token");
140
+ return;
141
+ }
142
+ } catch {
143
+ }
144
+ console.log("Not logged in, starting authentication flow...");
145
+ const deviceCode = await getDeviceCode();
146
+ console.log(
147
+ `Please enter the code "${deviceCode.user_code}" at ${deviceCode.verification_uri}`
148
+ );
149
+ const token = await pollAccessToken(deviceCode);
150
+ await writeGithubToken(token);
151
+ state.githubToken = token;
152
+ console.log("GitHub authentication successful");
153
+ }
154
+ async function setupCopilotToken() {
155
+ const { token, refresh_in } = await getCopilotToken();
156
+ state.copilotToken = token;
157
+ console.log("Copilot token fetched successfully");
158
+ const refreshInterval = (refresh_in - 60) * 1e3;
159
+ setInterval(async () => {
160
+ try {
161
+ const { token: token2 } = await getCopilotToken();
162
+ state.copilotToken = token2;
163
+ console.log("Copilot token refreshed");
164
+ } catch (error) {
165
+ console.error("Failed to refresh Copilot token:", error);
166
+ }
167
+ }, refreshInterval);
168
+ }
169
+
170
+ // src/server.ts
171
+ import { Hono } from "hono";
172
+ import { cors } from "hono/cors";
173
+
174
+ // src/routes/messages.ts
175
+ import { streamSSE } from "hono/streaming";
176
+
177
+ // src/api/chat.ts
178
+ import { events } from "fetch-event-stream";
179
+ async function createChatCompletions(payload) {
180
+ if (!state.copilotToken) throw new Error("Copilot token not found");
181
+ const enableVision = payload.messages.some(
182
+ (x) => typeof x.content !== "string" && x.content?.some((x2) => x2.type === "image_url")
183
+ );
184
+ const isAgentCall = payload.messages.some(
185
+ (msg) => ["assistant", "tool"].includes(msg.role)
186
+ );
187
+ const headers = {
188
+ ...copilotHeaders(state, enableVision),
189
+ "X-Initiator": isAgentCall ? "agent" : "user"
190
+ };
191
+ const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
192
+ method: "POST",
193
+ headers,
194
+ body: JSON.stringify(payload)
195
+ });
196
+ if (!response.ok) {
197
+ const text = await response.text();
198
+ throw new Error(`Copilot API error ${response.status}: ${text}`);
199
+ }
200
+ if (payload.stream) {
201
+ return events(response);
202
+ }
203
+ return await response.json();
204
+ }
205
+
206
+ // src/routes/translate.ts
207
+ function mapStopReason(finishReason) {
208
+ if (finishReason === null) return null;
209
+ const map = {
210
+ stop: "end_turn",
211
+ length: "max_tokens",
212
+ tool_calls: "tool_use",
213
+ content_filter: "end_turn"
214
+ };
215
+ return map[finishReason];
216
+ }
217
+ function translateToOpenAI(payload) {
218
+ return {
219
+ model: translateModelName(payload.model),
220
+ messages: translateMessages(payload.messages, payload.system),
221
+ max_tokens: payload.max_tokens,
222
+ stop: payload.stop_sequences,
223
+ stream: payload.stream,
224
+ temperature: payload.temperature,
225
+ top_p: payload.top_p,
226
+ tools: translateTools(payload.tools),
227
+ tool_choice: translateToolChoice(payload.tool_choice)
228
+ };
229
+ }
230
+ function translateModelName(model) {
231
+ if (model.startsWith("claude-sonnet-4-")) {
232
+ return "claude-sonnet-4";
233
+ } else if (model.startsWith("claude-opus-4-")) {
234
+ return "claude-opus-4";
235
+ }
236
+ return model;
237
+ }
238
+ function translateMessages(messages, system) {
239
+ const systemMessages = handleSystemPrompt(system);
240
+ const otherMessages = messages.flatMap(
241
+ (message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message)
242
+ );
243
+ return [...systemMessages, ...otherMessages];
244
+ }
245
+ function handleSystemPrompt(system) {
246
+ if (!system) return [];
247
+ if (typeof system === "string") {
248
+ return [{ role: "system", content: system }];
249
+ }
250
+ const text = system.map((block) => block.text).join("\n\n");
251
+ return [{ role: "system", content: text }];
252
+ }
253
+ function handleUserMessage(message) {
254
+ const result = [];
255
+ if (Array.isArray(message.content)) {
256
+ const toolResultBlocks = message.content.filter(
257
+ (block) => block.type === "tool_result"
258
+ );
259
+ const otherBlocks = message.content.filter(
260
+ (block) => block.type !== "tool_result"
261
+ );
262
+ for (const block of toolResultBlocks) {
263
+ result.push({
264
+ role: "tool",
265
+ tool_call_id: block.tool_use_id,
266
+ content: block.content
267
+ });
268
+ }
269
+ if (otherBlocks.length > 0) {
270
+ result.push({
271
+ role: "user",
272
+ content: mapContent(otherBlocks)
273
+ });
274
+ }
275
+ } else {
276
+ result.push({
277
+ role: "user",
278
+ content: message.content
279
+ });
280
+ }
281
+ return result;
282
+ }
283
+ function handleAssistantMessage(message) {
284
+ if (!Array.isArray(message.content)) {
285
+ return [{ role: "assistant", content: message.content }];
286
+ }
287
+ const toolUseBlocks = message.content.filter(
288
+ (block) => block.type === "tool_use"
289
+ );
290
+ const textBlocks = message.content.filter(
291
+ (block) => block.type === "text"
292
+ );
293
+ const thinkingBlocks = message.content.filter(
294
+ (block) => block.type === "thinking"
295
+ );
296
+ const allTextContent = [
297
+ ...textBlocks.map((b) => b.text),
298
+ ...thinkingBlocks.map((b) => b.thinking)
299
+ ].join("\n\n");
300
+ if (toolUseBlocks.length > 0) {
301
+ return [
302
+ {
303
+ role: "assistant",
304
+ content: allTextContent || null,
305
+ tool_calls: toolUseBlocks.map((toolUse) => ({
306
+ id: toolUse.id,
307
+ type: "function",
308
+ function: {
309
+ name: toolUse.name,
310
+ arguments: JSON.stringify(toolUse.input)
311
+ }
312
+ }))
313
+ }
314
+ ];
315
+ }
316
+ return [{ role: "assistant", content: mapContent(message.content) }];
317
+ }
318
+ function mapContent(content) {
319
+ const hasImage = content.some((block) => block.type === "image");
320
+ if (!hasImage) {
321
+ return content.filter(
322
+ (block) => block.type === "text" || block.type === "thinking"
323
+ ).map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
324
+ }
325
+ const parts = [];
326
+ for (const block of content) {
327
+ if (block.type === "text") {
328
+ parts.push({ type: "text", text: block.text });
329
+ } else if (block.type === "thinking") {
330
+ parts.push({ type: "text", text: block.thinking });
331
+ } else if (block.type === "image") {
332
+ parts.push({
333
+ type: "image_url",
334
+ image_url: {
335
+ url: `data:${block.source.media_type};base64,${block.source.data}`
336
+ }
337
+ });
338
+ }
339
+ }
340
+ return parts;
341
+ }
342
+ function translateTools(tools) {
343
+ if (!tools) return void 0;
344
+ return tools.map((tool) => ({
345
+ type: "function",
346
+ function: {
347
+ name: tool.name,
348
+ description: tool.description,
349
+ parameters: tool.input_schema
350
+ }
351
+ }));
352
+ }
353
+ function translateToolChoice(choice) {
354
+ if (!choice) return void 0;
355
+ switch (choice.type) {
356
+ case "auto":
357
+ return "auto";
358
+ case "any":
359
+ return "required";
360
+ case "tool":
361
+ if (choice.name) {
362
+ return { type: "function", function: { name: choice.name } };
363
+ }
364
+ return void 0;
365
+ case "none":
366
+ return "none";
367
+ default:
368
+ return void 0;
369
+ }
370
+ }
371
+ function translateToAnthropic(response) {
372
+ const allTextBlocks = [];
373
+ const allToolUseBlocks = [];
374
+ let stopReason = response.choices[0]?.finish_reason ?? null;
375
+ for (const choice of response.choices) {
376
+ const textBlocks = getTextBlocks(choice.message.content);
377
+ const toolUseBlocks = getToolUseBlocks(choice.message.tool_calls);
378
+ allTextBlocks.push(...textBlocks);
379
+ allToolUseBlocks.push(...toolUseBlocks);
380
+ if (choice.finish_reason === "tool_calls" || stopReason === "stop") {
381
+ stopReason = choice.finish_reason;
382
+ }
383
+ }
384
+ return {
385
+ id: response.id,
386
+ type: "message",
387
+ role: "assistant",
388
+ model: response.model,
389
+ content: [...allTextBlocks, ...allToolUseBlocks],
390
+ stop_reason: mapStopReason(stopReason),
391
+ stop_sequence: null,
392
+ usage: {
393
+ input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
394
+ output_tokens: response.usage?.completion_tokens ?? 0,
395
+ ...response.usage?.prompt_tokens_details?.cached_tokens !== void 0 && {
396
+ cache_read_input_tokens: response.usage.prompt_tokens_details.cached_tokens
397
+ }
398
+ }
399
+ };
400
+ }
401
+ function getTextBlocks(content) {
402
+ if (typeof content === "string") {
403
+ return [{ type: "text", text: content }];
404
+ }
405
+ return [];
406
+ }
407
+ function getToolUseBlocks(toolCalls) {
408
+ if (!toolCalls) return [];
409
+ return toolCalls.map((tc) => ({
410
+ type: "tool_use",
411
+ id: tc.id,
412
+ name: tc.function.name,
413
+ input: JSON.parse(tc.function.arguments)
414
+ }));
415
+ }
416
+
417
+ // src/routes/stream.ts
418
+ function isToolBlockOpen(state2) {
419
+ if (!state2.contentBlockOpen) return false;
420
+ return Object.values(state2.toolCalls).some(
421
+ (tc) => tc.anthropicBlockIndex === state2.contentBlockIndex
422
+ );
423
+ }
424
+ function translateChunkToEvents(chunk, state2) {
425
+ const events2 = [];
426
+ if (chunk.choices.length === 0) return events2;
427
+ const choice = chunk.choices[0];
428
+ const { delta } = choice;
429
+ if (!state2.messageStartSent) {
430
+ events2.push({
431
+ type: "message_start",
432
+ message: {
433
+ id: chunk.id,
434
+ type: "message",
435
+ role: "assistant",
436
+ content: [],
437
+ model: chunk.model,
438
+ stop_reason: null,
439
+ stop_sequence: null,
440
+ usage: {
441
+ input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
442
+ output_tokens: 0,
443
+ ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && {
444
+ cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens
445
+ }
446
+ }
447
+ }
448
+ });
449
+ state2.messageStartSent = true;
450
+ }
451
+ if (delta.content) {
452
+ if (isToolBlockOpen(state2)) {
453
+ events2.push({
454
+ type: "content_block_stop",
455
+ index: state2.contentBlockIndex
456
+ });
457
+ state2.contentBlockIndex++;
458
+ state2.contentBlockOpen = false;
459
+ }
460
+ if (!state2.contentBlockOpen) {
461
+ events2.push({
462
+ type: "content_block_start",
463
+ index: state2.contentBlockIndex,
464
+ content_block: { type: "text", text: "" }
465
+ });
466
+ state2.contentBlockOpen = true;
467
+ }
468
+ events2.push({
469
+ type: "content_block_delta",
470
+ index: state2.contentBlockIndex,
471
+ delta: { type: "text_delta", text: delta.content }
472
+ });
473
+ }
474
+ if (delta.tool_calls) {
475
+ for (const toolCall of delta.tool_calls) {
476
+ if (toolCall.id && toolCall.function?.name) {
477
+ if (state2.contentBlockOpen) {
478
+ events2.push({
479
+ type: "content_block_stop",
480
+ index: state2.contentBlockIndex
481
+ });
482
+ state2.contentBlockIndex++;
483
+ state2.contentBlockOpen = false;
484
+ }
485
+ const anthropicBlockIndex = state2.contentBlockIndex;
486
+ state2.toolCalls[toolCall.index] = {
487
+ id: toolCall.id,
488
+ name: toolCall.function.name,
489
+ anthropicBlockIndex
490
+ };
491
+ events2.push({
492
+ type: "content_block_start",
493
+ index: anthropicBlockIndex,
494
+ content_block: {
495
+ type: "tool_use",
496
+ id: toolCall.id,
497
+ name: toolCall.function.name,
498
+ input: {}
499
+ }
500
+ });
501
+ state2.contentBlockOpen = true;
502
+ }
503
+ if (toolCall.function?.arguments) {
504
+ const toolCallInfo = state2.toolCalls[toolCall.index];
505
+ if (toolCallInfo) {
506
+ events2.push({
507
+ type: "content_block_delta",
508
+ index: toolCallInfo.anthropicBlockIndex,
509
+ delta: {
510
+ type: "input_json_delta",
511
+ partial_json: toolCall.function.arguments
512
+ }
513
+ });
514
+ }
515
+ }
516
+ }
517
+ }
518
+ if (choice.finish_reason) {
519
+ if (state2.contentBlockOpen) {
520
+ events2.push({
521
+ type: "content_block_stop",
522
+ index: state2.contentBlockIndex
523
+ });
524
+ state2.contentBlockOpen = false;
525
+ }
526
+ events2.push(
527
+ {
528
+ type: "message_delta",
529
+ delta: {
530
+ stop_reason: mapStopReason(choice.finish_reason),
531
+ stop_sequence: null
532
+ },
533
+ usage: {
534
+ input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
535
+ output_tokens: chunk.usage?.completion_tokens ?? 0,
536
+ ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && {
537
+ cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens
538
+ }
539
+ }
540
+ },
541
+ { type: "message_stop" }
542
+ );
543
+ }
544
+ return events2;
545
+ }
546
+
547
+ // src/routes/messages.ts
548
+ async function handleMessages(c) {
549
+ const anthropicPayload = await c.req.json();
550
+ const openAIPayload = translateToOpenAI(anthropicPayload);
551
+ const response = await createChatCompletions(openAIPayload);
552
+ if ("choices" in response) {
553
+ const anthropicResponse = translateToAnthropic(response);
554
+ return c.json(anthropicResponse);
555
+ }
556
+ return streamSSE(c, async (stream) => {
557
+ const state2 = {
558
+ messageStartSent: false,
559
+ contentBlockIndex: 0,
560
+ contentBlockOpen: false,
561
+ toolCalls: {}
562
+ };
563
+ for await (const rawEvent of response) {
564
+ if (rawEvent.data === "[DONE]") break;
565
+ if (!rawEvent.data) continue;
566
+ const chunk = JSON.parse(rawEvent.data);
567
+ const events2 = translateChunkToEvents(chunk, state2);
568
+ for (const event of events2) {
569
+ await stream.writeSSE({
570
+ event: event.type,
571
+ data: JSON.stringify(event)
572
+ });
573
+ }
574
+ }
575
+ });
576
+ }
577
+
578
+ // src/server.ts
579
+ var app = new Hono();
580
+ app.use(
581
+ "*",
582
+ cors({
583
+ origin: (origin) => {
584
+ if (!origin) return null;
585
+ const url = new URL(origin);
586
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
587
+ return origin;
588
+ }
589
+ return null;
590
+ }
591
+ })
592
+ );
593
+ app.post("/v1/messages", handleMessages);
594
+ app.get("/health", (c) => c.json({ status: "ok" }));
595
+ var server_default = app;
596
+
597
+ // src/index.ts
598
+ var { values } = parseArgs({
599
+ args: process.argv.slice(2),
600
+ options: {
601
+ port: {
602
+ type: "string",
603
+ short: "p",
604
+ default: "4141"
605
+ },
606
+ "account-type": {
607
+ type: "string",
608
+ short: "a",
609
+ default: "individual"
610
+ },
611
+ "github-token": {
612
+ type: "string",
613
+ short: "g"
614
+ },
615
+ help: {
616
+ type: "boolean",
617
+ short: "h",
618
+ default: false
619
+ }
620
+ }
621
+ });
622
+ if (values.help) {
623
+ console.log(`
624
+ Copilot API Proxy (minimal) - Claude Code backend using GitHub Copilot
625
+
626
+ Usage: npx cccproxy [options]
627
+
628
+ Options:
629
+ -p, --port <port> Port to listen on (default: 4141)
630
+ -a, --account-type <type> Account type: individual, business, enterprise (default: individual)
631
+ -g, --github-token <token> GitHub token (or use GH_TOKEN env var)
632
+ -h, --help Show this help message
633
+ `);
634
+ process.exit(0);
635
+ }
636
+ var port = parseInt(values.port, 10);
637
+ var accountType = values["account-type"];
638
+ async function main() {
639
+ console.log("Starting Copilot API Proxy (minimal)...");
640
+ state.accountType = accountType;
641
+ const githubToken = values["github-token"] || process.env.GH_TOKEN;
642
+ if (githubToken) {
643
+ state.githubToken = githubToken;
644
+ console.log("Using provided GitHub token");
645
+ } else {
646
+ await setupGitHubToken();
647
+ }
648
+ await setupCopilotToken();
649
+ console.log(`Server starting on port ${port}...`);
650
+ console.log(`Anthropic API endpoint: http://localhost:${port}/v1/messages`);
651
+ serve({
652
+ fetch: server_default.fetch,
653
+ port,
654
+ hostname: "127.0.0.1"
655
+ // Bind to localhost only for security
656
+ });
657
+ console.log(`
658
+ To use with Claude Code, set these environment variables:
659
+ ANTHROPIC_BASE_URL=http://localhost:${port}
660
+ ANTHROPIC_AUTH_TOKEN=dummy
661
+ ANTHROPIC_MODEL=claude-sonnet-4
662
+
663
+ Or create .claude/settings.json with:
664
+ {
665
+ "env": {
666
+ "ANTHROPIC_BASE_URL": "http://localhost:${port}",
667
+ "ANTHROPIC_AUTH_TOKEN": "dummy",
668
+ "ANTHROPIC_MODEL": "claude-sonnet-4"
669
+ }
670
+ }
671
+ `);
672
+ }
673
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "cccproxy",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "cccproxy": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format esm --target node18 --clean",
13
+ "prepublishOnly": "npm run build",
14
+ "start": "node dist/index.js",
15
+ "dev": "tsup src/index.ts --format esm --target node18 --watch"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.0.0"
19
+ },
20
+ "dependencies": {
21
+ "@hono/node-server": "^1.19.9",
22
+ "fetch-event-stream": "^0.1.5",
23
+ "hono": "^4.7.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.3.3",
27
+ "tsup": "^8.5.1",
28
+ "typescript": "^5.7.0"
29
+ }
30
+ }