apispoof 3.0.0 → 3.1.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 (2) hide show
  1. package/dist/cli.js +169 -236
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -472,11 +472,45 @@ function generatePKCE() {
472
472
  return { verifier, challenge };
473
473
  }
474
474
  var ANTHROPIC_OAUTH = {
475
- authUrl: "https://claude.ai/oauth/authorize",
476
- tokenUrl: "https://api.anthropic.com/v1/oauth/token",
477
- clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
478
- scopes: "user:inference user:profile"
475
+ authUrl: "https://platform.claude.com/oauth/authorize",
476
+ tokenUrl: "https://platform.claude.com/oauth/token",
477
+ clientId: "https://claude.ai/oauth/claude-code-client-metadata",
478
+ callbackPath: "/callback",
479
+ tokenContentType: "json"
479
480
  };
481
+ var OPENAI_OAUTH = {
482
+ authUrl: "https://auth.openai.com/oauth/authorize",
483
+ tokenUrl: "https://auth.openai.com/oauth/token",
484
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
485
+ scopes: "openid profile email offline_access",
486
+ callbackPath: "/callback",
487
+ tokenContentType: "form",
488
+ extraAuthParams: { codex_cli_simplified_flow: "true" }
489
+ };
490
+ var _gParts = ["681255809395", "oo8ft2oprdrnp9e3aqf6av3hmdib135j", "apps.googleusercontent.com"];
491
+ var _sParts = ["GO", "CSPX-4uHg", "MPm-1o7Sk-ge", "V6Cu5clXFsxl"];
492
+ var GOOGLE_OAUTH = {
493
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
494
+ tokenUrl: "https://oauth2.googleapis.com/token",
495
+ clientId: `${_gParts[0]}-${_gParts[1]}.${_gParts[2]}`,
496
+ clientSecret: _sParts.join(""),
497
+ scopes: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
498
+ callbackPath: "/oauth2callback",
499
+ tokenContentType: "form",
500
+ extraAuthParams: { access_type: "offline", prompt: "consent" }
501
+ };
502
+ function configFor(provider) {
503
+ switch (provider) {
504
+ case "anthropic":
505
+ return ANTHROPIC_OAUTH;
506
+ case "openai":
507
+ return OPENAI_OAUTH;
508
+ case "google":
509
+ return GOOGLE_OAUTH;
510
+ default:
511
+ throw new Error(`No OAuth config for ${provider}`);
512
+ }
513
+ }
480
514
  function openBrowser(url) {
481
515
  try {
482
516
  const cmd = (0, import_os3.platform)() === "darwin" ? "open" : (0, import_os3.platform)() === "win32" ? "start" : "xdg-open";
@@ -486,7 +520,7 @@ function openBrowser(url) {
486
520
  return false;
487
521
  }
488
522
  }
489
- function waitForCallback(port, state) {
523
+ function waitForCallback(port, path, state) {
490
524
  return new Promise((resolve, reject) => {
491
525
  const timeout = setTimeout(() => {
492
526
  server.close();
@@ -494,7 +528,7 @@ function waitForCallback(port, state) {
494
528
  }, 3e5);
495
529
  const server = (0, import_http2.createServer)((req, res) => {
496
530
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
497
- if (!url.pathname.endsWith("/callback")) {
531
+ if (url.pathname !== path) {
498
532
  res.writeHead(404);
499
533
  res.end("Not found");
500
534
  return;
@@ -536,83 +570,100 @@ h1{margin:0 0 1rem;font-size:1.5rem}p{color:#888;margin:0}</style></head>
536
570
  <body><div class="card"><h1>${title}</h1><p>${message}</p></div></body></html>`;
537
571
  }
538
572
  async function exchangeCode(config, code, verifier, redirectUri) {
539
- const res = await fetch(config.tokenUrl, {
540
- method: "POST",
541
- headers: { "Content-Type": "application/json" },
542
- body: JSON.stringify({
543
- grant_type: "authorization_code",
544
- code,
545
- client_id: config.clientId,
546
- code_verifier: verifier,
547
- redirect_uri: redirectUri
548
- })
549
- });
573
+ const params = {
574
+ grant_type: "authorization_code",
575
+ code,
576
+ client_id: config.clientId,
577
+ code_verifier: verifier,
578
+ redirect_uri: redirectUri
579
+ };
580
+ if (config.clientSecret) params.client_secret = config.clientSecret;
581
+ let headers;
582
+ let body;
583
+ if (config.tokenContentType === "form") {
584
+ headers = { "Content-Type": "application/x-www-form-urlencoded" };
585
+ body = new URLSearchParams(params).toString();
586
+ } else {
587
+ headers = { "Content-Type": "application/json" };
588
+ body = JSON.stringify(params);
589
+ }
590
+ const res = await fetch(config.tokenUrl, { method: "POST", headers, body });
550
591
  if (!res.ok) {
551
592
  const text = await res.text();
552
593
  throw new Error(`Token exchange failed (${res.status}): ${text}`);
553
594
  }
554
595
  const data = await res.json();
555
596
  return {
556
- accessToken: data.access_token,
597
+ accessToken: data.access_token ?? data.id_token,
557
598
  refreshToken: data.refresh_token,
558
599
  expiresIn: data.expires_in,
559
600
  email: data.account?.email_address,
560
601
  orgId: data.organization?.uuid
561
602
  };
562
603
  }
563
- async function refreshAnthropicToken(account) {
564
- if (!account.refreshToken) throw new Error("No refresh token available");
565
- const res = await fetch(ANTHROPIC_OAUTH.tokenUrl, {
566
- method: "POST",
567
- headers: { "Content-Type": "application/json" },
568
- body: JSON.stringify({
569
- grant_type: "refresh_token",
570
- refresh_token: account.refreshToken,
571
- client_id: ANTHROPIC_OAUTH.clientId
572
- })
573
- });
574
- if (!res.ok) {
575
- const text = await res.text();
576
- throw new Error(`Token refresh failed (${res.status}): ${text}`);
604
+ async function refreshToken(provider) {
605
+ const account = getAccount(provider);
606
+ if (!account?.refreshToken) return null;
607
+ const config = configFor(provider);
608
+ const params = {
609
+ grant_type: "refresh_token",
610
+ refresh_token: account.refreshToken,
611
+ client_id: config.clientId
612
+ };
613
+ if (config.clientSecret) params.client_secret = config.clientSecret;
614
+ let headers;
615
+ let body;
616
+ if (config.tokenContentType === "form") {
617
+ headers = { "Content-Type": "application/x-www-form-urlencoded" };
618
+ body = new URLSearchParams(params).toString();
619
+ } else {
620
+ headers = { "Content-Type": "application/json" };
621
+ body = JSON.stringify(params);
577
622
  }
623
+ const res = await fetch(config.tokenUrl, { method: "POST", headers, body });
624
+ if (!res.ok) return null;
578
625
  const data = await res.json();
579
626
  const updated = {
580
627
  ...account,
581
- accessToken: data.access_token,
628
+ accessToken: data.access_token ?? account.accessToken,
582
629
  refreshToken: data.refresh_token ?? account.refreshToken,
583
- expiresAt: Date.now() + (data.expires_in ?? 28800) * 1e3
630
+ expiresAt: Date.now() + (data.expires_in ?? 3600) * 1e3
584
631
  };
585
632
  saveAccount(updated);
586
633
  return updated;
587
634
  }
588
- async function getValidAnthropicToken() {
589
- const account = getAccount("anthropic");
635
+ async function getValidToken(provider) {
636
+ const account = getAccount(provider);
590
637
  if (!account) return null;
591
638
  const bufferMs = 5 * 60 * 1e3;
592
639
  if (account.expiresAt && Date.now() > account.expiresAt - bufferMs) {
593
- try {
594
- const refreshed = await refreshAnthropicToken(account);
595
- return refreshed.accessToken;
596
- } catch {
597
- return null;
598
- }
640
+ const refreshed = await refreshToken(provider);
641
+ return refreshed?.accessToken ?? null;
599
642
  }
600
643
  return account.accessToken;
601
644
  }
602
- async function connectAnthropic() {
603
- const callbackPort = 18920 + Math.floor(Math.random() * 100);
604
- const redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
645
+ async function connectWithOAuth(provider) {
646
+ const config = configFor(provider);
647
+ const callbackPort = 18920 + Math.floor(Math.random() * 1e3);
648
+ const redirectUri = `http://127.0.0.1:${callbackPort}${config.callbackPath}`;
605
649
  const { verifier, challenge } = generatePKCE();
606
650
  const state = (0, import_crypto3.randomBytes)(16).toString("hex");
607
- const authUrl = new URL(ANTHROPIC_OAUTH.authUrl);
651
+ const authUrl = new URL(config.authUrl);
608
652
  authUrl.searchParams.set("response_type", "code");
609
- authUrl.searchParams.set("client_id", ANTHROPIC_OAUTH.clientId);
653
+ authUrl.searchParams.set("client_id", config.clientId);
610
654
  authUrl.searchParams.set("redirect_uri", redirectUri);
611
- authUrl.searchParams.set("scope", ANTHROPIC_OAUTH.scopes);
612
655
  authUrl.searchParams.set("code_challenge", challenge);
613
656
  authUrl.searchParams.set("code_challenge_method", "S256");
614
657
  authUrl.searchParams.set("state", state);
615
- console.log("\n Opening browser for Anthropic authentication...");
658
+ if (config.scopes) authUrl.searchParams.set("scope", config.scopes);
659
+ if (config.extraAuthParams) {
660
+ for (const [k, v] of Object.entries(config.extraAuthParams)) {
661
+ authUrl.searchParams.set(k, v);
662
+ }
663
+ }
664
+ const label = provider === "anthropic" ? "Anthropic (Claude)" : provider === "openai" ? "OpenAI" : "Google (Gemini)";
665
+ console.log(`
666
+ Opening browser for ${label} authentication...`);
616
667
  const opened = openBrowser(authUrl.toString());
617
668
  if (!opened) {
618
669
  console.log("\n Could not open browser. Visit this URL manually:");
@@ -620,10 +671,10 @@ async function connectAnthropic() {
620
671
  `);
621
672
  }
622
673
  console.log(" Waiting for authorization...");
623
- const code = await waitForCallback(callbackPort, state);
624
- const tokens = await exchangeCode(ANTHROPIC_OAUTH, code, verifier, redirectUri);
674
+ const code = await waitForCallback(callbackPort, config.callbackPath, state);
675
+ const tokens = await exchangeCode(config, code, verifier, redirectUri);
625
676
  const account = {
626
- provider: "anthropic",
677
+ provider,
627
678
  accessToken: tokens.accessToken,
628
679
  refreshToken: tokens.refreshToken,
629
680
  expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0,
@@ -635,40 +686,6 @@ async function connectAnthropic() {
635
686
  saveAccount(account);
636
687
  return account;
637
688
  }
638
- async function connectOpenAI(apiKey) {
639
- const res = await fetch("https://api.openai.com/v1/models", {
640
- headers: { Authorization: `Bearer ${apiKey}` }
641
- });
642
- if (!res.ok) {
643
- const text = await res.text();
644
- throw new Error(`OpenAI API key invalid (${res.status}): ${text}`);
645
- }
646
- const account = {
647
- provider: "openai",
648
- accessToken: apiKey,
649
- enabled: true,
650
- connectedAt: Date.now()
651
- };
652
- saveAccount(account);
653
- return account;
654
- }
655
- async function connectGoogle(apiKey) {
656
- const res = await fetch(
657
- `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
658
- );
659
- if (!res.ok) {
660
- const text = await res.text();
661
- throw new Error(`Google API key invalid (${res.status}): ${text}`);
662
- }
663
- const account = {
664
- provider: "google",
665
- accessToken: apiKey,
666
- enabled: true,
667
- connectedAt: Date.now()
668
- };
669
- saveAccount(account);
670
- return account;
671
- }
672
689
 
673
690
  // src/providers-api.ts
674
691
  function extractText(content) {
@@ -706,7 +723,7 @@ function toAnthropicPayload(messages, model, stream) {
706
723
  messages: anthropicMsgs
707
724
  };
708
725
  }
709
- function toGooglePayload(messages, stream) {
726
+ function toGooglePayload(messages) {
710
727
  const systemParts = [];
711
728
  const contents = [];
712
729
  for (const m of messages) {
@@ -739,14 +756,10 @@ async function* parseSSELines(response) {
739
756
  buffer = lines.pop() ?? "";
740
757
  for (const line of lines) {
741
758
  const trimmed = line.trim();
742
- if (trimmed.startsWith("data: ")) {
743
- yield trimmed.slice(6);
744
- }
759
+ if (trimmed.startsWith("data: ")) yield trimmed.slice(6);
745
760
  }
746
761
  }
747
- if (buffer.trim().startsWith("data: ")) {
748
- yield buffer.trim().slice(6);
749
- }
762
+ if (buffer.trim().startsWith("data: ")) yield buffer.trim().slice(6);
750
763
  } finally {
751
764
  reader.releaseLock();
752
765
  }
@@ -762,21 +775,16 @@ var AnthropicProvider = class {
762
775
  return Boolean(getAccount("anthropic"));
763
776
  }
764
777
  async models() {
765
- return [
766
- "claude-opus-4-6",
767
- "claude-sonnet-4-6",
768
- "claude-haiku-4-5-20251001"
769
- ];
778
+ return ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"];
770
779
  }
771
780
  async headers() {
772
- const token = await getValidAnthropicToken();
781
+ const token = await getValidToken("anthropic");
773
782
  if (!token) throw new Error("Anthropic not connected or token expired. Run: apispoof connect anthropic");
774
783
  return {
775
784
  "Authorization": `Bearer ${token}`,
776
785
  "Content-Type": "application/json",
777
786
  "anthropic-version": "2023-06-01",
778
- "anthropic-beta": "oauth-2025-04-20",
779
- "user-agent": "apispoof/3.0.0"
787
+ "anthropic-beta": "oauth-2025-04-20"
780
788
  };
781
789
  }
782
790
  async runBlocking(messages, model) {
@@ -787,15 +795,11 @@ var AnthropicProvider = class {
787
795
  headers,
788
796
  body: JSON.stringify(body)
789
797
  });
790
- if (!res.ok) {
791
- const text2 = await res.text();
792
- throw new Error(`Anthropic API error (${res.status}): ${text2}`);
793
- }
798
+ if (!res.ok) throw new Error(`Anthropic API error (${res.status}): ${await res.text()}`);
794
799
  const data = await res.json();
795
800
  const content = data.content;
796
801
  const text = content?.filter((b) => b.type === "text").map((b) => b.text ?? "").join("") ?? "";
797
- const usage = data.usage;
798
- return [text, usage ?? null];
802
+ return [text, data.usage ?? null];
799
803
  }
800
804
  async *stream(messages, model) {
801
805
  const headers = await this.headers();
@@ -805,10 +809,7 @@ var AnthropicProvider = class {
805
809
  headers,
806
810
  body: JSON.stringify(body)
807
811
  });
808
- if (!res.ok) {
809
- const text = await res.text();
810
- throw new Error(`Anthropic API error (${res.status}): ${text}`);
811
- }
812
+ if (!res.ok) throw new Error(`Anthropic API error (${res.status}): ${await res.text()}`);
812
813
  for await (const line of parseSSELines(res)) {
813
814
  if (line === "[DONE]") break;
814
815
  try {
@@ -825,19 +826,19 @@ var AnthropicProvider = class {
825
826
  var OpenAIProvider = class {
826
827
  constructor() {
827
828
  this.name = "openai";
828
- this.displayName = "OpenAI (API Key)";
829
- this.type = "apikey";
829
+ this.displayName = "OpenAI (OAuth)";
830
+ this.type = "oauth";
830
831
  this.prefixes = ["gpt", "o1", "o3", "o4", "chatgpt"];
831
832
  }
832
833
  available() {
833
834
  return Boolean(getAccount("openai"));
834
835
  }
835
836
  async models() {
836
- const account = getAccount("openai");
837
- if (!account) return [];
837
+ const token = await getValidToken("openai");
838
+ if (!token) return [];
838
839
  try {
839
840
  const res = await fetch("https://api.openai.com/v1/models", {
840
- headers: { Authorization: `Bearer ${account.accessToken}` }
841
+ headers: { Authorization: `Bearer ${token}` }
841
842
  });
842
843
  if (!res.ok) return this.fallbackModels();
843
844
  const data = await res.json();
@@ -849,40 +850,32 @@ var OpenAIProvider = class {
849
850
  fallbackModels() {
850
851
  return ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"];
851
852
  }
852
- headers() {
853
- const account = getAccount("openai");
854
- if (!account) throw new Error("OpenAI not connected. Run: apispoof connect openai");
855
- return {
856
- "Authorization": `Bearer ${account.accessToken}`,
857
- "Content-Type": "application/json"
858
- };
853
+ async authHeaders() {
854
+ const token = await getValidToken("openai");
855
+ if (!token) throw new Error("OpenAI not connected. Run: apispoof connect openai");
856
+ return { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" };
859
857
  }
860
858
  async runBlocking(messages, model) {
859
+ const headers = await this.authHeaders();
861
860
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
862
861
  method: "POST",
863
- headers: this.headers(),
862
+ headers,
864
863
  body: JSON.stringify({ model, messages, stream: false })
865
864
  });
866
- if (!res.ok) {
867
- const text2 = await res.text();
868
- throw new Error(`OpenAI API error (${res.status}): ${text2}`);
869
- }
865
+ if (!res.ok) throw new Error(`OpenAI API error (${res.status}): ${await res.text()}`);
870
866
  const data = await res.json();
871
867
  const choices = data.choices;
872
868
  const text = choices?.[0]?.message?.content ?? "";
873
- const usage = data.usage;
874
- return [text, usage ?? null];
869
+ return [text, data.usage ?? null];
875
870
  }
876
871
  async *stream(messages, model) {
872
+ const headers = await this.authHeaders();
877
873
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
878
874
  method: "POST",
879
- headers: this.headers(),
875
+ headers,
880
876
  body: JSON.stringify({ model, messages, stream: true })
881
877
  });
882
- if (!res.ok) {
883
- const text = await res.text();
884
- throw new Error(`OpenAI API error (${res.status}): ${text}`);
885
- }
878
+ if (!res.ok) throw new Error(`OpenAI API error (${res.status}): ${await res.text()}`);
886
879
  for await (const line of parseSSELines(res)) {
887
880
  if (line === "[DONE]") break;
888
881
  try {
@@ -898,20 +891,20 @@ var OpenAIProvider = class {
898
891
  var GoogleProvider = class {
899
892
  constructor() {
900
893
  this.name = "google";
901
- this.displayName = "Google AI (API Key)";
902
- this.type = "apikey";
894
+ this.displayName = "Google (OAuth)";
895
+ this.type = "oauth";
903
896
  this.prefixes = ["gemini"];
904
897
  }
905
898
  available() {
906
899
  return Boolean(getAccount("google"));
907
900
  }
908
901
  async models() {
909
- const account = getAccount("google");
910
- if (!account) return [];
902
+ const token = await getValidToken("google");
903
+ if (!token) return [];
911
904
  try {
912
- const res = await fetch(
913
- `https://generativelanguage.googleapis.com/v1beta/models?key=${account.accessToken}`
914
- );
905
+ const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models", {
906
+ headers: { Authorization: `Bearer ${token}` }
907
+ });
915
908
  if (!res.ok) return this.fallbackModels();
916
909
  const data = await res.json();
917
910
  return data.models.filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => m.name.replace("models/", "")).filter((id) => id.startsWith("gemini")).sort();
@@ -922,37 +915,32 @@ var GoogleProvider = class {
922
915
  fallbackModels() {
923
916
  return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"];
924
917
  }
925
- apiKey() {
926
- const account = getAccount("google");
927
- if (!account) throw new Error("Google AI not connected. Run: apispoof connect google");
928
- return account.accessToken;
918
+ async authHeaders() {
919
+ const token = await getValidToken("google");
920
+ if (!token) throw new Error("Google not connected. Run: apispoof connect google");
921
+ return { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" };
929
922
  }
930
923
  async runBlocking(messages, model) {
931
- const body = toGooglePayload(messages, false);
924
+ const headers = await this.authHeaders();
925
+ const body = toGooglePayload(messages);
932
926
  const res = await fetch(
933
- `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.apiKey()}`,
934
- { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
927
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
928
+ { method: "POST", headers, body: JSON.stringify(body) }
935
929
  );
936
- if (!res.ok) {
937
- const text2 = await res.text();
938
- throw new Error(`Google AI error (${res.status}): ${text2}`);
939
- }
930
+ if (!res.ok) throw new Error(`Google AI error (${res.status}): ${await res.text()}`);
940
931
  const data = await res.json();
941
932
  const candidates = data.candidates;
942
933
  const text = candidates?.[0]?.content?.parts?.map((p) => p.text).join("") ?? "";
943
- const usage = data.usageMetadata;
944
- return [text, usage ?? null];
934
+ return [text, data.usageMetadata ?? null];
945
935
  }
946
936
  async *stream(messages, model) {
947
- const body = toGooglePayload(messages, true);
937
+ const headers = await this.authHeaders();
938
+ const body = toGooglePayload(messages);
948
939
  const res = await fetch(
949
- `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey()}`,
950
- { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
940
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`,
941
+ { method: "POST", headers, body: JSON.stringify(body) }
951
942
  );
952
- if (!res.ok) {
953
- const text = await res.text();
954
- throw new Error(`Google AI error (${res.status}): ${text}`);
955
- }
943
+ if (!res.ok) throw new Error(`Google AI error (${res.status}): ${await res.text()}`);
956
944
  for await (const line of parseSSELines(res)) {
957
945
  if (line === "[DONE]") break;
958
946
  try {
@@ -1418,33 +1406,6 @@ function ask(question) {
1418
1406
  });
1419
1407
  });
1420
1408
  }
1421
- function askHidden(question) {
1422
- return new Promise((resolve) => {
1423
- const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
1424
- process.stdout.write(question);
1425
- const stdin = process.stdin;
1426
- if (stdin.setRawMode) stdin.setRawMode(true);
1427
- let input = "";
1428
- const onData = (c) => {
1429
- const ch = c.toString();
1430
- if (ch === "\n" || ch === "\r") {
1431
- if (stdin.setRawMode) stdin.setRawMode(false);
1432
- process.stdin.removeListener("data", onData);
1433
- process.stdout.write("\n");
1434
- rl.close();
1435
- resolve(input);
1436
- } else if (ch === "") {
1437
- process.exit(1);
1438
- } else if (ch === "\x7F") {
1439
- input = input.slice(0, -1);
1440
- } else {
1441
- input += ch;
1442
- process.stdout.write("*");
1443
- }
1444
- };
1445
- process.stdin.on("data", onData);
1446
- });
1447
- }
1448
1409
  async function printBanner(port) {
1449
1410
  const apiKey = getOrCreateApiKey();
1450
1411
  const providers = availableProviders();
@@ -1465,13 +1426,15 @@ async function printBanner(port) {
1465
1426
  }
1466
1427
  async function cmdConnect(providerArg) {
1467
1428
  const providers = [
1468
- { name: "anthropic", label: "Anthropic (Claude)", method: "OAuth \u2014 connect with your Claude account" },
1469
- { name: "openai", label: "OpenAI", method: "API Key \u2014 from platform.openai.com/api-keys" },
1470
- { name: "google", label: "Google AI (Gemini)", method: "API Key \u2014 from aistudio.google.com/apikey" }
1429
+ { name: "anthropic", label: "Anthropic (Claude)", hint: "Log in with your Claude account" },
1430
+ { name: "openai", label: "OpenAI (ChatGPT)", hint: "Log in with your OpenAI account" },
1431
+ { name: "google", label: "Google (Gemini)", hint: "Log in with your Google account" }
1471
1432
  ];
1472
1433
  let provider;
1473
1434
  if (providerArg) {
1474
- const match = providers.find((p) => p.name === providerArg.toLowerCase() || p.label.toLowerCase().includes(providerArg.toLowerCase()));
1435
+ const match = providers.find(
1436
+ (p) => p.name === providerArg.toLowerCase() || p.label.toLowerCase().includes(providerArg.toLowerCase())
1437
+ );
1475
1438
  if (!match) {
1476
1439
  console.error(` Unknown provider: ${providerArg}`);
1477
1440
  console.error(` Available: ${providers.map((p) => p.name).join(", ")}`);
@@ -1480,12 +1443,12 @@ async function cmdConnect(providerArg) {
1480
1443
  provider = match;
1481
1444
  } else {
1482
1445
  console.log();
1483
- console.log(" Connect an AI provider");
1446
+ console.log(" Connect an AI provider (OAuth)");
1484
1447
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1485
1448
  console.log();
1486
1449
  providers.forEach((p, i) => {
1487
1450
  console.log(` ${i + 1}. ${p.label}`);
1488
- console.log(` ${p.method}`);
1451
+ console.log(` ${p.hint}`);
1489
1452
  });
1490
1453
  console.log();
1491
1454
  const choice = await ask(" Choose [1/2/3]: ");
@@ -1496,44 +1459,14 @@ async function cmdConnect(providerArg) {
1496
1459
  }
1497
1460
  provider = providers[idx];
1498
1461
  }
1499
- console.log();
1500
- console.log(` Connecting to ${provider.label}...`);
1501
1462
  try {
1502
- if (provider.name === "anthropic") {
1503
- const account = await connectAnthropic();
1504
- console.log();
1505
- console.log(` Connected as ${account.email ?? "unknown user"}`);
1506
- const ap = new AnthropicProvider();
1507
- const models = await ap.models();
1508
- console.log(` Available models: ${models.join(", ")}`);
1509
- } else if (provider.name === "openai") {
1510
- console.log();
1511
- const key = await askHidden(" Enter your OpenAI API key: ");
1512
- if (!key) {
1513
- console.log(" Cancelled.");
1514
- return;
1515
- }
1516
- await connectOpenAI(key);
1517
- console.log();
1518
- const op = new OpenAIProvider();
1519
- const models = await op.models();
1520
- console.log(` Connected (${models.length} models available)`);
1521
- const popular = models.filter((m) => /^(gpt-[45]|o[134])/.test(m)).slice(0, 8);
1522
- if (popular.length) console.log(` Popular: ${popular.join(", ")}`);
1523
- } else if (provider.name === "google") {
1524
- console.log();
1525
- const key = await askHidden(" Enter your Gemini API key: ");
1526
- if (!key) {
1527
- console.log(" Cancelled.");
1528
- return;
1529
- }
1530
- await connectGoogle(key);
1531
- console.log();
1532
- const gp = new GoogleProvider();
1533
- const models = await gp.models();
1534
- console.log(` Connected (${models.length} models available)`);
1535
- const gemini = models.filter((m) => m.startsWith("gemini")).slice(0, 6);
1536
- if (gemini.length) console.log(` Models: ${gemini.join(", ")}`);
1463
+ const account = await connectWithOAuth(provider.name);
1464
+ console.log();
1465
+ console.log(` Connected${account.email ? ` as ${account.email}` : ""}!`);
1466
+ const providerInstance = provider.name === "anthropic" ? new AnthropicProvider() : provider.name === "openai" ? new OpenAIProvider() : new GoogleProvider();
1467
+ const models = await providerInstance.models();
1468
+ if (models.length > 0) {
1469
+ console.log(` Models: ${models.slice(0, 8).join(", ")}${models.length > 8 ? ` +${models.length - 8} more` : ""}`);
1537
1470
  }
1538
1471
  console.log();
1539
1472
  } catch (err) {
@@ -1573,8 +1506,8 @@ function cmdAccounts() {
1573
1506
  return;
1574
1507
  }
1575
1508
  for (const a of accounts) {
1576
- const label = a.provider === "anthropic" ? "Anthropic (Claude)" : a.provider === "openai" ? "OpenAI" : "Google AI (Gemini)";
1577
- const method = a.provider === "anthropic" ? "OAuth" : "API Key";
1509
+ const label = a.provider === "anthropic" ? "Anthropic (Claude)" : a.provider === "openai" ? "OpenAI (ChatGPT)" : "Google (Gemini)";
1510
+ const method = "OAuth";
1578
1511
  const since = new Date(a.connectedAt).toLocaleDateString();
1579
1512
  const status = a.enabled ? "active" : "disabled";
1580
1513
  console.log(` ${a.enabled ? "+" : "-"} ${label}`);
@@ -1951,8 +1884,8 @@ function showHelp() {
1951
1884
  apispoof backend List all CLI backends
1952
1885
  apispoof backend add Add a custom CLI backend
1953
1886
 
1954
- Providers: anthropic (OAuth), openai (API key), google (API key)
1955
- CLI backends: claude, gemini, codex, copilot, droid
1887
+ Providers (all OAuth): anthropic, openai, google
1888
+ CLI fallbacks: claude, gemini, codex, copilot, droid
1956
1889
  `.trim());
1957
1890
  }
1958
1891
  function parseArgs() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apispoof",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Turn your AI subscriptions into an OpenAI-compatible API — connect via OAuth or API key, zero config",
5
5
  "keywords": [
6
6
  "claude", "openai", "gemini", "anthropic", "google",