@jer-y/copilot-proxy 0.1.5 → 0.2.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.
package/dist/main.js CHANGED
@@ -1,44 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import { PATHS, ensurePaths } from "./paths-CA6OZ0WA.js";
3
+ import { loadDaemonConfig } from "./config-D1kMGXKU.js";
4
+ import { isDaemonRunning, isProcessRunning, readPid, removePidFile } from "./pid-uKNpN4v-.js";
5
+ import { daemonStart } from "./start-Dcv2sypx.js";
2
6
  import { defineCommand, runMain } from "citty";
3
7
  import consola from "consola";
4
8
  import fs from "node:fs/promises";
5
9
  import os from "node:os";
6
- import path from "node:path";
7
10
  import { randomUUID } from "node:crypto";
8
11
  import process from "node:process";
12
+ import fs$1 from "node:fs";
13
+ import { Buffer } from "node:buffer";
14
+ import { execSync } from "node:child_process";
9
15
  import clipboard from "clipboardy";
10
16
  import { serve } from "srvx";
11
17
  import invariant from "tiny-invariant";
12
18
  import { getProxyForUrl } from "proxy-from-env";
13
19
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
14
- import { execSync } from "node:child_process";
15
20
  import { Hono } from "hono";
16
21
  import { cors } from "hono/cors";
17
22
  import { logger } from "hono/logger";
18
23
  import { streamSSE } from "hono/streaming";
19
24
  import { events } from "fetch-event-stream";
20
25
 
21
- //#region src/lib/paths.ts
22
- const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-proxy");
23
- const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
24
- const PATHS = {
25
- APP_DIR,
26
- GITHUB_TOKEN_PATH
27
- };
28
- async function ensurePaths() {
29
- await fs.mkdir(PATHS.APP_DIR, { recursive: true });
30
- await ensureFile(PATHS.GITHUB_TOKEN_PATH);
31
- }
32
- async function ensureFile(filePath) {
33
- try {
34
- await fs.access(filePath, fs.constants.W_OK);
35
- } catch {
36
- await fs.writeFile(filePath, "");
37
- await fs.chmod(filePath, 384);
38
- }
39
- }
40
-
41
- //#endregion
42
26
  //#region src/lib/state.ts
43
27
  const state = {
44
28
  accountType: "individual",
@@ -163,20 +147,21 @@ async function getGitHubUser() {
163
147
  //#endregion
164
148
  //#region src/services/copilot/get-models.ts
165
149
  async function getModels() {
150
+ const headers = copilotHeaders(state);
151
+ const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers });
152
+ if (response.ok) return await response.json();
153
+ consola.warn(`vscode-chat models request failed (${response.status} ${response.statusText}), falling back to copilot-developer-cli`);
166
154
  if (state.githubToken) try {
167
155
  const cliHeaders = copilotHeaders(state);
168
156
  cliHeaders.Authorization = `Bearer ${state.githubToken}`;
169
157
  cliHeaders["copilot-integration-id"] = "copilot-developer-cli";
170
- const response$1 = await fetch(`${copilotBaseUrl(state)}/models`, { headers: cliHeaders });
171
- if (response$1.ok) return await response$1.json();
172
- consola.warn(`copilot-developer-cli models request failed (${response$1.status} ${response$1.statusText}), falling back to standard auth`);
158
+ const cliResponse = await fetch(`${copilotBaseUrl(state)}/models`, { headers: cliHeaders });
159
+ if (cliResponse.ok) return await cliResponse.json();
160
+ consola.warn(`copilot-developer-cli fallback also failed (${cliResponse.status} ${cliResponse.statusText})`);
173
161
  } catch (e) {
174
- consola.warn("copilot-developer-cli models request error, falling back:", e);
162
+ consola.warn("copilot-developer-cli fallback error:", e);
175
163
  }
176
- const headers = copilotHeaders(state);
177
- const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers });
178
- if (!response.ok) throw new HTTPError("Failed to get models", response);
179
- return await response.json();
164
+ throw new HTTPError("Failed to get models", response);
180
165
  }
181
166
 
182
167
  //#endregion
@@ -259,7 +244,8 @@ async function setupCopilotToken() {
259
244
  state.copilotToken = token;
260
245
  consola.debug("GitHub Copilot Token fetched successfully!");
261
246
  if (state.showToken) consola.info("Copilot token:", token);
262
- const refreshInterval = (refresh_in - 60) * 1e3;
247
+ const rawInterval = (refresh_in - 60) * 1e3;
248
+ const refreshInterval = Number.isFinite(rawInterval) ? Math.min(Math.max(rawInterval, 6e4), 1440 * 60 * 1e3) : 6e4;
263
249
  setInterval(async () => {
264
250
  consola.debug("Refreshing Copilot token");
265
251
  try {
@@ -268,8 +254,7 @@ async function setupCopilotToken() {
268
254
  consola.debug("Copilot token refreshed");
269
255
  if (state.showToken) consola.info("Refreshed Copilot token:", token$1);
270
256
  } catch (error) {
271
- consola.error("Failed to refresh Copilot token:", error);
272
- throw error;
257
+ consola.error("Failed to refresh Copilot token, will retry next cycle:", error);
273
258
  }
274
259
  }, refreshInterval);
275
260
  }
@@ -387,6 +372,228 @@ const checkUsage = defineCommand({
387
372
  }
388
373
  });
389
374
 
375
+ //#endregion
376
+ //#region src/daemon/disable.ts
377
+ const disable = defineCommand({
378
+ meta: {
379
+ name: "disable",
380
+ description: "Remove auto-start service"
381
+ },
382
+ async run() {
383
+ const { platform } = process;
384
+ let success = true;
385
+ if (platform === "linux") {
386
+ const { uninstallAutoStart } = await import("./linux-CX0xETja.js");
387
+ success = await uninstallAutoStart();
388
+ } else if (platform === "darwin") {
389
+ const { uninstallAutoStart } = await import("./darwin-BVmd1DeO.js");
390
+ success = await uninstallAutoStart();
391
+ } else if (platform === "win32") {
392
+ const { uninstallAutoStart } = await import("./win32-D1-MlKl7.js");
393
+ success = await uninstallAutoStart();
394
+ } else {
395
+ consola.error(`Unsupported platform: ${platform}`);
396
+ process.exit(1);
397
+ }
398
+ if (!success) process.exit(1);
399
+ }
400
+ });
401
+
402
+ //#endregion
403
+ //#region src/daemon/enable.ts
404
+ const enable = defineCommand({
405
+ meta: {
406
+ name: "enable",
407
+ description: "Register as auto-start service"
408
+ },
409
+ async run() {
410
+ if (!loadDaemonConfig()) {
411
+ consola.error("No daemon config found. Start the daemon first with `start -d`");
412
+ process.exit(1);
413
+ }
414
+ const execPath = process.argv[0];
415
+ const args = [
416
+ process.argv[1],
417
+ "start",
418
+ "--_supervisor"
419
+ ];
420
+ let success = false;
421
+ const { platform } = process;
422
+ if (platform === "linux") {
423
+ const { installAutoStart } = await import("./linux-CX0xETja.js");
424
+ success = await installAutoStart(execPath, args);
425
+ } else if (platform === "darwin") {
426
+ const { installAutoStart } = await import("./darwin-BVmd1DeO.js");
427
+ success = await installAutoStart(execPath, args);
428
+ } else if (platform === "win32") {
429
+ const { installAutoStart } = await import("./win32-D1-MlKl7.js");
430
+ success = await installAutoStart(execPath, args);
431
+ } else {
432
+ consola.error(`Unsupported platform: ${platform}`);
433
+ process.exit(1);
434
+ }
435
+ if (!success) process.exit(1);
436
+ }
437
+ });
438
+
439
+ //#endregion
440
+ //#region src/daemon/logs.ts
441
+ const logs = defineCommand({
442
+ meta: {
443
+ name: "logs",
444
+ description: "Show daemon logs"
445
+ },
446
+ args: {
447
+ follow: {
448
+ alias: "f",
449
+ type: "boolean",
450
+ default: false,
451
+ description: "Follow log output"
452
+ },
453
+ lines: {
454
+ alias: "n",
455
+ type: "string",
456
+ default: "50",
457
+ description: "Number of lines to show"
458
+ }
459
+ },
460
+ run({ args }) {
461
+ if (!fs$1.existsSync(PATHS.DAEMON_LOG)) {
462
+ consola.info("No log file found");
463
+ return;
464
+ }
465
+ if (args.follow) followLogsWatch();
466
+ else {
467
+ const lines = fs$1.readFileSync(PATHS.DAEMON_LOG, "utf8").split("\n");
468
+ const count = Number.parseInt(args.lines, 10);
469
+ const output = lines.slice(-count).join("\n");
470
+ console.log(output);
471
+ }
472
+ }
473
+ });
474
+ function followLogsWatch() {
475
+ const content = fs$1.readFileSync(PATHS.DAEMON_LOG, "utf8");
476
+ process.stdout.write(content);
477
+ let position = Buffer.byteLength(content);
478
+ let currentIno = 0;
479
+ try {
480
+ currentIno = fs$1.statSync(PATHS.DAEMON_LOG).ino;
481
+ } catch {}
482
+ setInterval(() => {
483
+ try {
484
+ const stat = fs$1.statSync(PATHS.DAEMON_LOG);
485
+ if (stat.ino !== currentIno) {
486
+ currentIno = stat.ino;
487
+ position = 0;
488
+ }
489
+ if (stat.size < position) position = 0;
490
+ if (stat.size > position) {
491
+ const fd = fs$1.openSync(PATHS.DAEMON_LOG, "r");
492
+ const buffer = Buffer.alloc(stat.size - position);
493
+ fs$1.readSync(fd, buffer, 0, buffer.length, position);
494
+ fs$1.closeSync(fd);
495
+ process.stdout.write(buffer);
496
+ position = stat.size;
497
+ }
498
+ } catch {}
499
+ }, 500);
500
+ }
501
+
502
+ //#endregion
503
+ //#region src/daemon/stop.ts
504
+ /**
505
+ * Attempt to stop the daemon. Returns true if daemon was stopped or
506
+ * was not running. Returns false if the process could not be stopped.
507
+ */
508
+ function stopDaemon() {
509
+ const daemon = isDaemonRunning();
510
+ if (!daemon.running) {
511
+ consola.info("Daemon is not running");
512
+ removePidFile();
513
+ return true;
514
+ }
515
+ const { pid } = daemon;
516
+ consola.info(`Stopping daemon (PID: ${pid})...`);
517
+ try {
518
+ process.kill(pid, "SIGTERM");
519
+ } catch {
520
+ consola.error("Failed to send SIGTERM");
521
+ return false;
522
+ }
523
+ const deadline = Date.now() + 1e4;
524
+ while (isProcessRunning(pid) && Date.now() < deadline) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
525
+ if (isProcessRunning(pid)) {
526
+ consola.warn("Process did not exit in time, sending SIGKILL");
527
+ try {
528
+ process.kill(pid, "SIGKILL");
529
+ } catch {}
530
+ const killDeadline = Date.now() + 3e3;
531
+ while (isProcessRunning(pid) && Date.now() < killDeadline) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
532
+ if (isProcessRunning(pid)) {
533
+ consola.error(`Failed to kill process ${pid}`);
534
+ return false;
535
+ }
536
+ }
537
+ removePidFile();
538
+ consola.success("Daemon stopped");
539
+ return true;
540
+ }
541
+ const stop = defineCommand({
542
+ meta: {
543
+ name: "stop",
544
+ description: "Stop the background daemon"
545
+ },
546
+ run() {
547
+ if (!stopDaemon()) process.exit(1);
548
+ }
549
+ });
550
+
551
+ //#endregion
552
+ //#region src/daemon/restart.ts
553
+ const restart = defineCommand({
554
+ meta: {
555
+ name: "restart",
556
+ description: "Restart the background daemon"
557
+ },
558
+ run() {
559
+ const config = loadDaemonConfig();
560
+ if (!config) {
561
+ consola.error("No daemon config found. Start the daemon first with `start -d`");
562
+ process.exit(1);
563
+ }
564
+ if (isDaemonRunning().running) {
565
+ if (!stopDaemon()) {
566
+ consola.error("Cannot restart: failed to stop existing daemon");
567
+ process.exit(1);
568
+ }
569
+ }
570
+ daemonStart(config);
571
+ }
572
+ });
573
+
574
+ //#endregion
575
+ //#region src/daemon/status.ts
576
+ const status = defineCommand({
577
+ meta: {
578
+ name: "status",
579
+ description: "Show daemon status"
580
+ },
581
+ run() {
582
+ const daemon = isDaemonRunning();
583
+ if (!daemon.running) {
584
+ consola.info("Daemon is not running");
585
+ return;
586
+ }
587
+ const config = loadDaemonConfig();
588
+ const info = readPid();
589
+ const startedAt = info && info.startTime > 0 ? new Date(info.startTime).toLocaleString() : "unknown";
590
+ consola.info(`Daemon is running`);
591
+ consola.info(` PID: ${daemon.pid}`);
592
+ consola.info(` Port: ${config?.port ?? "unknown"}`);
593
+ consola.info(` Started: ${startedAt}`);
594
+ }
595
+ });
596
+
390
597
  //#endregion
391
598
  //#region src/debug.ts
392
599
  async function getPackageVersion() {
@@ -996,8 +1203,29 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
996
1203
 
997
1204
  //#endregion
998
1205
  //#region src/routes/messages/non-stream-translation.ts
999
- function translateToOpenAI(payload) {
1000
- const model = translateModelName(payload.model);
1206
+ /** Models that support variant suffixes (e.g. -fast, -1m) */
1207
+ const MODEL_VARIANTS = { "claude-opus-4.6": new Set(["fast", "1m"]) };
1208
+ /** Parse comma-separated anthropic-beta header into a Set of feature names */
1209
+ function parseBetaFeatures(anthropicBeta) {
1210
+ if (!anthropicBeta) return /* @__PURE__ */ new Set();
1211
+ return new Set(anthropicBeta.split(",").map((s) => s.trim()).filter(Boolean));
1212
+ }
1213
+ /** Apply model variant suffix based on speed field and beta header signals */
1214
+ function applyModelVariant(model, payload, anthropicBeta) {
1215
+ const normalizedModel = translateModelName(model);
1216
+ const variants = MODEL_VARIANTS[normalizedModel];
1217
+ if (!variants) return normalizedModel;
1218
+ const betaFeatures = parseBetaFeatures(anthropicBeta);
1219
+ if (variants.has("fast")) {
1220
+ if (payload.speed === "fast" || betaFeatures.has("fast-mode-2026-02-01")) return `${normalizedModel}-fast`;
1221
+ }
1222
+ if (variants.has("1m")) {
1223
+ if (betaFeatures.has("context-1m-2025-08-07")) return `${normalizedModel}-1m`;
1224
+ }
1225
+ return normalizedModel;
1226
+ }
1227
+ function translateToOpenAI(payload, options) {
1228
+ const model = applyModelVariant(payload.model, payload, options?.anthropicBeta);
1001
1229
  const modelConfig = getModelConfig(model);
1002
1230
  const enableCacheControl = modelConfig.enableCacheControl === true;
1003
1231
  const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system);
@@ -1207,14 +1435,32 @@ function getAnthropicToolUseBlocks(toolCalls) {
1207
1435
  //#endregion
1208
1436
  //#region src/routes/messages/count-tokens-handler.ts
1209
1437
  /**
1438
+ * Find a model in the models list, falling back to the base model
1439
+ * when a variant suffix (-fast, -1m) doesn't have its own entry.
1440
+ */
1441
+ function findModelWithFallback(modelId, models) {
1442
+ if (!models) return;
1443
+ const exact = models.find((m) => m.id === modelId);
1444
+ if (exact) return exact;
1445
+ const baseModel = modelId.replace(/-(fast|1m)$/, "");
1446
+ if (baseModel !== modelId) return models.find((m) => m.id === baseModel);
1447
+ }
1448
+ /**
1449
+ * Determine if the request is from Claude Code based on anthropic-beta tokens.
1450
+ * Order-independent: works regardless of token position in the header.
1451
+ */
1452
+ function isClaudeCodeRequest(anthropicBeta) {
1453
+ return [...parseBetaFeatures(anthropicBeta)].some((f) => f.startsWith("claude-code"));
1454
+ }
1455
+ /**
1210
1456
  * Handles token counting for Anthropic messages
1211
1457
  */
1212
1458
  async function handleCountTokens(c) {
1213
1459
  try {
1214
1460
  const anthropicBeta = c.req.header("anthropic-beta");
1215
1461
  const anthropicPayload = await c.req.json();
1216
- const openAIPayload = translateToOpenAI(anthropicPayload);
1217
- const selectedModel = state.models?.data.find((model) => model.id === openAIPayload.model);
1462
+ const openAIPayload = translateToOpenAI(anthropicPayload, { anthropicBeta });
1463
+ const selectedModel = findModelWithFallback(openAIPayload.model, state.models?.data);
1218
1464
  if (!selectedModel) {
1219
1465
  consola.warn("Model not found, returning default token count");
1220
1466
  return c.json({ input_tokens: 1 });
@@ -1222,7 +1468,7 @@ async function handleCountTokens(c) {
1222
1468
  const tokenCount = await getTokenCount(openAIPayload, selectedModel);
1223
1469
  if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
1224
1470
  let mcpToolExist = false;
1225
- if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
1471
+ if (isClaudeCodeRequest(anthropicBeta)) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
1226
1472
  if (!mcpToolExist) {
1227
1473
  if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
1228
1474
  else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
@@ -1367,9 +1613,10 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1367
1613
  //#region src/routes/messages/handler.ts
1368
1614
  async function handleCompletion(c) {
1369
1615
  await checkRateLimit(state);
1616
+ const anthropicBeta = c.req.header("anthropic-beta");
1370
1617
  const anthropicPayload = await c.req.json();
1371
1618
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
1372
- const openAIPayload = translateToOpenAI(anthropicPayload);
1619
+ const openAIPayload = translateToOpenAI(anthropicPayload, { anthropicBeta });
1373
1620
  consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
1374
1621
  if (state.manualApprove) await awaitApproval();
1375
1622
  const response = await createChatCompletions(openAIPayload);
@@ -1624,6 +1871,7 @@ async function runServer(options) {
1624
1871
  fetch: server.fetch,
1625
1872
  port: options.port
1626
1873
  });
1874
+ await new Promise(() => {});
1627
1875
  }
1628
1876
  const start = defineCommand({
1629
1877
  meta: {
@@ -1685,13 +1933,83 @@ const start = defineCommand({
1685
1933
  type: "boolean",
1686
1934
  default: false,
1687
1935
  description: "Initialize proxy from environment variables"
1936
+ },
1937
+ "daemon": {
1938
+ alias: "d",
1939
+ type: "boolean",
1940
+ default: false,
1941
+ description: "Run as a background daemon"
1942
+ },
1943
+ "_supervisor": {
1944
+ type: "boolean",
1945
+ default: false,
1946
+ description: "Internal: run as supervisor (do not use directly)"
1688
1947
  }
1689
1948
  },
1690
- run({ args }) {
1949
+ async run({ args }) {
1950
+ const port = Number.parseInt(args.port, 10);
1951
+ if (Number.isNaN(port) || port <= 0 || port > 65535 || String(port) !== args.port) {
1952
+ consola.error(`Invalid port: ${args.port}`);
1953
+ process.exit(1);
1954
+ }
1691
1955
  const rateLimitRaw = args["rate-limit"];
1692
1956
  const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
1957
+ if (rateLimitRaw !== void 0 && (Number.isNaN(rateLimit) || rateLimit <= 0 || rateLimit > 86400 || String(rateLimit) !== rateLimitRaw)) {
1958
+ consola.error(`Invalid rate-limit: ${rateLimitRaw} (must be 1-86400)`);
1959
+ process.exit(1);
1960
+ }
1961
+ const validAccountTypes = [
1962
+ "individual",
1963
+ "business",
1964
+ "enterprise"
1965
+ ];
1966
+ if (!validAccountTypes.includes(args["account-type"])) {
1967
+ consola.error(`Invalid account-type: ${args["account-type"]} (must be one of: ${validAccountTypes.join(", ")})`);
1968
+ process.exit(1);
1969
+ }
1970
+ if (args._supervisor) {
1971
+ const { loadDaemonConfig: loadDaemonConfig$1 } = await import("./config-ixm2Pm96.js");
1972
+ const config = loadDaemonConfig$1();
1973
+ if (!config) {
1974
+ consola.error("Supervisor mode: daemon config not found");
1975
+ process.exit(1);
1976
+ }
1977
+ const { runAsSupervisor } = await import("./supervisor-BbH28zwT.js");
1978
+ const options = {
1979
+ port: config.port,
1980
+ verbose: config.verbose,
1981
+ accountType: config.accountType,
1982
+ manual: config.manual,
1983
+ rateLimit: config.rateLimit,
1984
+ rateLimitWait: config.rateLimitWait,
1985
+ githubToken: config.githubToken,
1986
+ claudeCode: false,
1987
+ showToken: config.showToken,
1988
+ proxyEnv: config.proxyEnv
1989
+ };
1990
+ return runAsSupervisor(() => runServer(options));
1991
+ }
1992
+ if (args.daemon) {
1993
+ if (args["claude-code"]) {
1994
+ consola.error("Cannot use --claude-code with --daemon (interactive mode)");
1995
+ process.exit(1);
1996
+ }
1997
+ const { daemonStart: daemonStart$1 } = await import("./start-C-0OcnXB.js");
1998
+ daemonStart$1({
1999
+ port,
2000
+ verbose: args.verbose,
2001
+ accountType: args["account-type"],
2002
+ manual: args.manual,
2003
+ rateLimit,
2004
+ rateLimitWait: args.wait,
2005
+ githubToken: args["github-token"],
2006
+ showToken: args["show-token"],
2007
+ proxyEnv: args["proxy-env"]
2008
+ });
2009
+ return;
2010
+ }
1693
2011
  return runServer({
1694
- port: Number.parseInt(args.port, 10),
2012
+ port,
1695
2013
  verbose: args.verbose,
1696
2014
  accountType: args["account-type"],
1697
2015
  manual: args.manual,
@@ -1716,7 +2034,13 @@ const main = defineCommand({
1716
2034
  auth,
1717
2035
  start,
1718
2036
  "check-usage": checkUsage,
1719
- debug
2037
+ debug,
2038
+ stop,
2039
+ status,
2040
+ logs,
2041
+ restart,
2042
+ enable,
2043
+ disable
1720
2044
  }
1721
2045
  });
1722
2046
  await runMain(main);