bashio 1.0.0 → 1.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.
- package/dist/index.js +1314 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { Builtins, Cli } from "clipanion";
|
|
5
6
|
import pc17 from "picocolors";
|
|
6
7
|
import updateNotifier from "update-notifier";
|
|
@@ -20,7 +21,10 @@ import { join } from "path";
|
|
|
20
21
|
import { z } from "zod";
|
|
21
22
|
var ProviderName = z.enum([
|
|
22
23
|
"claude",
|
|
24
|
+
"claude-subscription",
|
|
23
25
|
"openai",
|
|
26
|
+
"chatgpt-subscription",
|
|
27
|
+
"copilot",
|
|
24
28
|
"ollama",
|
|
25
29
|
"openrouter"
|
|
26
30
|
]);
|
|
@@ -36,10 +40,41 @@ var LocalCredentials = z.object({
|
|
|
36
40
|
type: z.literal("local"),
|
|
37
41
|
host: z.string().default("http://localhost:11434")
|
|
38
42
|
});
|
|
43
|
+
var ClaudeSubscriptionCredentials = z.object({
|
|
44
|
+
type: z.literal("claude_subscription"),
|
|
45
|
+
accessToken: z.string(),
|
|
46
|
+
refreshToken: z.string(),
|
|
47
|
+
expiresAt: z.number(),
|
|
48
|
+
// Unix timestamp in ms
|
|
49
|
+
email: z.string().optional()
|
|
50
|
+
});
|
|
51
|
+
var ChatGPTSubscriptionCredentials = z.object({
|
|
52
|
+
type: z.literal("chatgpt_subscription"),
|
|
53
|
+
accessToken: z.string(),
|
|
54
|
+
refreshToken: z.string().optional(),
|
|
55
|
+
expiresAt: z.number().optional(),
|
|
56
|
+
// Unix timestamp in ms
|
|
57
|
+
accountId: z.string().optional()
|
|
58
|
+
// ChatGPT account ID for API requests
|
|
59
|
+
});
|
|
60
|
+
var CopilotCredentials = z.object({
|
|
61
|
+
type: z.literal("copilot"),
|
|
62
|
+
githubToken: z.string(),
|
|
63
|
+
// GitHub OAuth access token (gho_xxx)
|
|
64
|
+
copilotToken: z.string(),
|
|
65
|
+
// Copilot API token (short-lived)
|
|
66
|
+
copilotTokenExpiresAt: z.number(),
|
|
67
|
+
// Unix timestamp in ms
|
|
68
|
+
apiEndpoint: z.string().optional()
|
|
69
|
+
// Derived from token (api.individual/business.githubcopilot.com)
|
|
70
|
+
});
|
|
39
71
|
var Credentials = z.discriminatedUnion("type", [
|
|
40
72
|
SessionCredentials,
|
|
41
73
|
ApiKeyCredentials,
|
|
42
|
-
LocalCredentials
|
|
74
|
+
LocalCredentials,
|
|
75
|
+
ClaudeSubscriptionCredentials,
|
|
76
|
+
ChatGPTSubscriptionCredentials,
|
|
77
|
+
CopilotCredentials
|
|
43
78
|
]);
|
|
44
79
|
var Settings = z.object({
|
|
45
80
|
confirmBeforeExecute: z.boolean().default(true),
|
|
@@ -587,6 +622,392 @@ import pc4 from "picocolors";
|
|
|
587
622
|
import { input as input3, password, select } from "@inquirer/prompts";
|
|
588
623
|
import pc3 from "picocolors";
|
|
589
624
|
|
|
625
|
+
// src/core/oauth.ts
|
|
626
|
+
import * as crypto from "crypto";
|
|
627
|
+
import * as http from "http";
|
|
628
|
+
import { URL as URL2 } from "url";
|
|
629
|
+
var CLAUDE_OAUTH_CONFIG = {
|
|
630
|
+
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
631
|
+
authorizationUrl: "https://claude.ai/oauth/authorize",
|
|
632
|
+
tokenUrl: "https://console.anthropic.com/v1/oauth/token",
|
|
633
|
+
redirectUri: "http://localhost:8765/callback",
|
|
634
|
+
scopes: "org:create_api_key user:profile user:inference",
|
|
635
|
+
callbackPort: 8765
|
|
636
|
+
};
|
|
637
|
+
var COPILOT_OAUTH_CONFIG = {
|
|
638
|
+
clientId: "Iv1.b507a08c87ecfe98",
|
|
639
|
+
// Official GitHub Copilot client ID
|
|
640
|
+
deviceCodeUrl: "https://github.com/login/device/code",
|
|
641
|
+
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
|
642
|
+
copilotTokenUrl: "https://api.github.com/copilot_internal/v2/token",
|
|
643
|
+
apiEndpoint: "https://api.githubcopilot.com/chat/completions",
|
|
644
|
+
scope: "read:user"
|
|
645
|
+
};
|
|
646
|
+
var CHATGPT_OAUTH_CONFIG = {
|
|
647
|
+
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
648
|
+
// Official Codex CLI client ID
|
|
649
|
+
authorizationUrl: "https://auth.openai.com/oauth/authorize",
|
|
650
|
+
tokenUrl: "https://auth.openai.com/oauth/token",
|
|
651
|
+
redirectUri: "http://localhost:1455/auth/callback",
|
|
652
|
+
scopes: "openid profile email offline_access",
|
|
653
|
+
callbackPort: 1455,
|
|
654
|
+
audience: "https://api.openai.com/v1",
|
|
655
|
+
// Backend API endpoint (subscription OAuth uses this, NOT api.openai.com)
|
|
656
|
+
apiEndpoint: "https://chatgpt.com/backend-api/codex/responses"
|
|
657
|
+
};
|
|
658
|
+
function generateCodeVerifier() {
|
|
659
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
660
|
+
}
|
|
661
|
+
function generateCodeChallenge(verifier) {
|
|
662
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
663
|
+
}
|
|
664
|
+
function generateState() {
|
|
665
|
+
return crypto.randomBytes(16).toString("hex");
|
|
666
|
+
}
|
|
667
|
+
function generatePKCE() {
|
|
668
|
+
const codeVerifier = generateCodeVerifier();
|
|
669
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
670
|
+
const state = generateState();
|
|
671
|
+
return { codeVerifier, codeChallenge, state };
|
|
672
|
+
}
|
|
673
|
+
function buildClaudeAuthUrl(pkce) {
|
|
674
|
+
const params = new URLSearchParams({
|
|
675
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
676
|
+
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
677
|
+
scope: CLAUDE_OAUTH_CONFIG.scopes,
|
|
678
|
+
code_challenge: pkce.codeChallenge,
|
|
679
|
+
code_challenge_method: "S256",
|
|
680
|
+
response_type: "code",
|
|
681
|
+
state: pkce.state
|
|
682
|
+
});
|
|
683
|
+
return `${CLAUDE_OAUTH_CONFIG.authorizationUrl}?${params.toString()}`;
|
|
684
|
+
}
|
|
685
|
+
async function exchangeClaudeCode(code, codeVerifier, state) {
|
|
686
|
+
const body = {
|
|
687
|
+
code,
|
|
688
|
+
state,
|
|
689
|
+
grant_type: "authorization_code",
|
|
690
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
691
|
+
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
692
|
+
code_verifier: codeVerifier
|
|
693
|
+
};
|
|
694
|
+
const response = await fetch(CLAUDE_OAUTH_CONFIG.tokenUrl, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: { "Content-Type": "application/json" },
|
|
697
|
+
body: JSON.stringify(body)
|
|
698
|
+
});
|
|
699
|
+
if (!response.ok) {
|
|
700
|
+
const errorText = await response.text();
|
|
701
|
+
throw new Error(
|
|
702
|
+
`Claude token exchange failed: ${response.status} - ${errorText}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const data = await response.json();
|
|
706
|
+
if (!data.refresh_token) {
|
|
707
|
+
throw new Error("Claude token exchange did not return a refresh_token");
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
accessToken: data.access_token,
|
|
711
|
+
refreshToken: data.refresh_token,
|
|
712
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
713
|
+
email: data.email
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
async function refreshClaudeToken(refreshToken) {
|
|
717
|
+
const body = {
|
|
718
|
+
grant_type: "refresh_token",
|
|
719
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
720
|
+
refresh_token: refreshToken
|
|
721
|
+
};
|
|
722
|
+
const response = await fetch(CLAUDE_OAUTH_CONFIG.tokenUrl, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers: { "Content-Type": "application/json" },
|
|
725
|
+
body: JSON.stringify(body)
|
|
726
|
+
});
|
|
727
|
+
if (!response.ok) {
|
|
728
|
+
const errorText = await response.text();
|
|
729
|
+
throw new Error(
|
|
730
|
+
`Claude token refresh failed: ${response.status} - ${errorText}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const data = await response.json();
|
|
734
|
+
return {
|
|
735
|
+
accessToken: data.access_token,
|
|
736
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
737
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
738
|
+
email: data.email
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function isClaudeTokenExpired(expiresAt) {
|
|
742
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
743
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
744
|
+
}
|
|
745
|
+
function parseJWTClaims(token) {
|
|
746
|
+
try {
|
|
747
|
+
const parts = token.split(".");
|
|
748
|
+
if (parts.length !== 3) return null;
|
|
749
|
+
const payload = parts[1];
|
|
750
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
751
|
+
return JSON.parse(decoded);
|
|
752
|
+
} catch {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function extractAccountIdFromToken(accessToken, idToken) {
|
|
757
|
+
const tokens = idToken ? [idToken, accessToken] : [accessToken];
|
|
758
|
+
for (const token of tokens) {
|
|
759
|
+
const claims = parseJWTClaims(token);
|
|
760
|
+
if (!claims) continue;
|
|
761
|
+
const accountId = claims.chatgpt_account_id || claims["https://api.openai.com/auth"]?.chatgpt_account_id || claims.organizations?.[0]?.id;
|
|
762
|
+
if (accountId) return accountId;
|
|
763
|
+
}
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
function buildChatGPTAuthUrl(pkce) {
|
|
767
|
+
const params = new URLSearchParams({
|
|
768
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
769
|
+
redirect_uri: CHATGPT_OAUTH_CONFIG.redirectUri,
|
|
770
|
+
scope: CHATGPT_OAUTH_CONFIG.scopes,
|
|
771
|
+
code_challenge: pkce.codeChallenge,
|
|
772
|
+
code_challenge_method: "S256",
|
|
773
|
+
response_type: "code",
|
|
774
|
+
state: pkce.state,
|
|
775
|
+
audience: CHATGPT_OAUTH_CONFIG.audience
|
|
776
|
+
});
|
|
777
|
+
return `${CHATGPT_OAUTH_CONFIG.authorizationUrl}?${params.toString()}`;
|
|
778
|
+
}
|
|
779
|
+
async function exchangeChatGPTCode(code, codeVerifier) {
|
|
780
|
+
const body = new URLSearchParams({
|
|
781
|
+
grant_type: "authorization_code",
|
|
782
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
783
|
+
code,
|
|
784
|
+
redirect_uri: CHATGPT_OAUTH_CONFIG.redirectUri,
|
|
785
|
+
code_verifier: codeVerifier
|
|
786
|
+
});
|
|
787
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.tokenUrl, {
|
|
788
|
+
method: "POST",
|
|
789
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
790
|
+
body: body.toString()
|
|
791
|
+
});
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const errorText = await response.text();
|
|
794
|
+
throw new Error(
|
|
795
|
+
`ChatGPT token exchange failed: ${response.status} - ${errorText}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const data = await response.json();
|
|
799
|
+
const accountId = extractAccountIdFromToken(data.access_token, data.id_token);
|
|
800
|
+
return {
|
|
801
|
+
accessToken: data.access_token,
|
|
802
|
+
refreshToken: data.refresh_token || "",
|
|
803
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : 0,
|
|
804
|
+
accountId
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async function refreshChatGPTToken(refreshToken) {
|
|
808
|
+
const body = new URLSearchParams({
|
|
809
|
+
grant_type: "refresh_token",
|
|
810
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
811
|
+
refresh_token: refreshToken
|
|
812
|
+
});
|
|
813
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.tokenUrl, {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
816
|
+
body: body.toString()
|
|
817
|
+
});
|
|
818
|
+
if (!response.ok) {
|
|
819
|
+
const errorText = await response.text();
|
|
820
|
+
throw new Error(
|
|
821
|
+
`ChatGPT token refresh failed: ${response.status} - ${errorText}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
const data = await response.json();
|
|
825
|
+
return {
|
|
826
|
+
accessToken: data.access_token,
|
|
827
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
828
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : 0
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function startCallbackServer(port, expectedState, timeoutMs = 5 * 60 * 1e3) {
|
|
832
|
+
return new Promise((resolve, reject) => {
|
|
833
|
+
const server = http.createServer((req, res) => {
|
|
834
|
+
const parsedUrl = new URL2(req.url || "", `http://localhost:${port}`);
|
|
835
|
+
if (parsedUrl.pathname !== "/callback" && parsedUrl.pathname !== "/auth/callback") {
|
|
836
|
+
res.writeHead(404);
|
|
837
|
+
res.end("Not Found");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const code = parsedUrl.searchParams.get("code");
|
|
841
|
+
const state = parsedUrl.searchParams.get("state");
|
|
842
|
+
const error = parsedUrl.searchParams.get("error");
|
|
843
|
+
if (error) {
|
|
844
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
845
|
+
res.end(
|
|
846
|
+
`<html><body><h1>Authentication Failed</h1><p>${error}</p></body></html>`
|
|
847
|
+
);
|
|
848
|
+
server.close();
|
|
849
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (!code || !state) {
|
|
853
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
854
|
+
res.end("<html><body><h1>Missing Parameters</h1></body></html>");
|
|
855
|
+
server.close();
|
|
856
|
+
reject(new Error("Missing code or state parameter"));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (state !== expectedState) {
|
|
860
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
861
|
+
res.end(
|
|
862
|
+
"<html><body><h1>Invalid State</h1><p>Possible CSRF attack.</p></body></html>"
|
|
863
|
+
);
|
|
864
|
+
server.close();
|
|
865
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
869
|
+
res.end(`
|
|
870
|
+
<!DOCTYPE html>
|
|
871
|
+
<html>
|
|
872
|
+
<head><title>Authentication Successful</title></head>
|
|
873
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
874
|
+
<h1>Authentication Successful!</h1>
|
|
875
|
+
<p>You can close this window and return to your terminal.</p>
|
|
876
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
877
|
+
</body>
|
|
878
|
+
</html>
|
|
879
|
+
`);
|
|
880
|
+
server.close();
|
|
881
|
+
resolve({ code, state });
|
|
882
|
+
});
|
|
883
|
+
server.on("error", (err) => {
|
|
884
|
+
if (err.code === "EADDRINUSE") {
|
|
885
|
+
reject(
|
|
886
|
+
new Error(
|
|
887
|
+
`Port ${port} is already in use. Close other applications using this port.`
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
} else {
|
|
891
|
+
reject(err);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
const timeout = setTimeout(() => {
|
|
895
|
+
server.close();
|
|
896
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
897
|
+
}, timeoutMs);
|
|
898
|
+
server.on("close", () => clearTimeout(timeout));
|
|
899
|
+
server.listen(port, "127.0.0.1");
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async function requestCopilotDeviceCode() {
|
|
903
|
+
const body = new URLSearchParams({
|
|
904
|
+
client_id: COPILOT_OAUTH_CONFIG.clientId,
|
|
905
|
+
scope: COPILOT_OAUTH_CONFIG.scope
|
|
906
|
+
});
|
|
907
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.deviceCodeUrl, {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: {
|
|
910
|
+
Accept: "application/json",
|
|
911
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
912
|
+
},
|
|
913
|
+
body: body.toString()
|
|
914
|
+
});
|
|
915
|
+
if (!response.ok) {
|
|
916
|
+
const errorText = await response.text();
|
|
917
|
+
throw new Error(
|
|
918
|
+
`Failed to request device code: ${response.status} - ${errorText}`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return await response.json();
|
|
922
|
+
}
|
|
923
|
+
async function pollForCopilotAccessToken(deviceCode, intervalMs, expiresAt) {
|
|
924
|
+
const body = new URLSearchParams({
|
|
925
|
+
client_id: COPILOT_OAUTH_CONFIG.clientId,
|
|
926
|
+
device_code: deviceCode,
|
|
927
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
928
|
+
});
|
|
929
|
+
while (Date.now() < expiresAt) {
|
|
930
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.accessTokenUrl, {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers: {
|
|
933
|
+
Accept: "application/json",
|
|
934
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
935
|
+
},
|
|
936
|
+
body: body.toString()
|
|
937
|
+
});
|
|
938
|
+
const data = await response.json();
|
|
939
|
+
if (data.access_token) {
|
|
940
|
+
return data.access_token;
|
|
941
|
+
}
|
|
942
|
+
if (data.error === "authorization_pending") {
|
|
943
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (data.error === "slow_down") {
|
|
947
|
+
await new Promise((r) => setTimeout(r, intervalMs + 5e3));
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (data.error === "expired_token") {
|
|
951
|
+
throw new Error("Device code expired. Please try again.");
|
|
952
|
+
}
|
|
953
|
+
if (data.error === "access_denied") {
|
|
954
|
+
throw new Error("Access denied. User cancelled authorization.");
|
|
955
|
+
}
|
|
956
|
+
throw new Error(`GitHub OAuth error: ${data.error || "Unknown error"}`);
|
|
957
|
+
}
|
|
958
|
+
throw new Error("Authorization timed out. Please try again.");
|
|
959
|
+
}
|
|
960
|
+
async function exchangeGitHubTokenForCopilot(githubToken) {
|
|
961
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.copilotTokenUrl, {
|
|
962
|
+
method: "GET",
|
|
963
|
+
headers: {
|
|
964
|
+
Accept: "application/json",
|
|
965
|
+
Authorization: `Bearer ${githubToken}`
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
if (!response.ok) {
|
|
969
|
+
const errorText = await response.text();
|
|
970
|
+
if (response.status === 401) {
|
|
971
|
+
throw new Error("GitHub token is invalid or expired.");
|
|
972
|
+
}
|
|
973
|
+
if (response.status === 403) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
"You do not have access to GitHub Copilot. Please ensure you have an active Copilot subscription."
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Failed to get Copilot token: ${response.status} - ${errorText}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return await response.json();
|
|
983
|
+
}
|
|
984
|
+
function parseCopilotToken(token) {
|
|
985
|
+
let expiresAt = Date.now() + 30 * 60 * 1e3;
|
|
986
|
+
let apiEndpoint = COPILOT_OAUTH_CONFIG.apiEndpoint;
|
|
987
|
+
const pairs = token.split(";");
|
|
988
|
+
for (const pair of pairs) {
|
|
989
|
+
const [key, value] = pair.split("=");
|
|
990
|
+
if (key?.trim() === "exp" && value) {
|
|
991
|
+
expiresAt = Number.parseInt(value.trim(), 10) * 1e3;
|
|
992
|
+
}
|
|
993
|
+
if (key?.trim() === "proxy-ep" && value) {
|
|
994
|
+
let proxyUrl = value.trim();
|
|
995
|
+
if (!proxyUrl.startsWith("http")) {
|
|
996
|
+
proxyUrl = `https://${proxyUrl}`;
|
|
997
|
+
}
|
|
998
|
+
apiEndpoint = proxyUrl.replace(/\/\/proxy\./i, "//api.");
|
|
999
|
+
if (!apiEndpoint.includes("/chat/completions")) {
|
|
1000
|
+
apiEndpoint = `${apiEndpoint}/chat/completions`;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { expiresAt, apiEndpoint };
|
|
1005
|
+
}
|
|
1006
|
+
function isCopilotTokenExpired(expiresAt) {
|
|
1007
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
1008
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
590
1011
|
// src/providers/base.ts
|
|
591
1012
|
var SYSTEM_PROMPT_GENERATE = `You are a shell command generator for macOS/Linux terminals.
|
|
592
1013
|
Given a natural language description, return ONLY the shell command.
|
|
@@ -602,6 +1023,181 @@ Explain the given shell command in simple terms.
|
|
|
602
1023
|
Break down each part of the command concisely.
|
|
603
1024
|
Format as a simple list without markdown.`;
|
|
604
1025
|
|
|
1026
|
+
// src/providers/chatgpt-subscription.ts
|
|
1027
|
+
var ChatGPTSubscriptionProvider = class {
|
|
1028
|
+
name = "ChatGPT (Subscription)";
|
|
1029
|
+
model;
|
|
1030
|
+
accessToken;
|
|
1031
|
+
refreshToken;
|
|
1032
|
+
expiresAt;
|
|
1033
|
+
accountId;
|
|
1034
|
+
constructor(config) {
|
|
1035
|
+
this.model = config.model;
|
|
1036
|
+
if (config.credentials.type === "chatgpt_subscription") {
|
|
1037
|
+
this.accessToken = config.credentials.accessToken;
|
|
1038
|
+
this.refreshToken = config.credentials.refreshToken;
|
|
1039
|
+
this.expiresAt = config.credentials.expiresAt;
|
|
1040
|
+
this.accountId = config.credentials.accountId;
|
|
1041
|
+
if (!this.accountId) {
|
|
1042
|
+
this.accountId = extractAccountIdFromToken(this.accessToken);
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
throw new Error("ChatGPT Subscription requires subscription credentials");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
isTokenExpired() {
|
|
1049
|
+
if (!this.expiresAt) return false;
|
|
1050
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
1051
|
+
return Date.now() >= this.expiresAt - bufferMs;
|
|
1052
|
+
}
|
|
1053
|
+
async ensureValidToken() {
|
|
1054
|
+
if (this.isTokenExpired() && this.refreshToken) {
|
|
1055
|
+
try {
|
|
1056
|
+
const newTokens = await refreshChatGPTToken(this.refreshToken);
|
|
1057
|
+
this.accessToken = newTokens.accessToken;
|
|
1058
|
+
this.refreshToken = newTokens.refreshToken || this.refreshToken;
|
|
1059
|
+
this.expiresAt = newTokens.expiresAt || this.expiresAt;
|
|
1060
|
+
if (newTokens.accountId) {
|
|
1061
|
+
this.accountId = newTokens.accountId;
|
|
1062
|
+
} else if (!this.accountId) {
|
|
1063
|
+
this.accountId = extractAccountIdFromToken(this.accessToken);
|
|
1064
|
+
}
|
|
1065
|
+
const currentConfig = loadConfig();
|
|
1066
|
+
if (currentConfig && currentConfig.credentials.type === "chatgpt_subscription") {
|
|
1067
|
+
currentConfig.credentials.accessToken = newTokens.accessToken;
|
|
1068
|
+
if (newTokens.refreshToken) {
|
|
1069
|
+
currentConfig.credentials.refreshToken = newTokens.refreshToken;
|
|
1070
|
+
}
|
|
1071
|
+
if (newTokens.expiresAt) {
|
|
1072
|
+
currentConfig.credentials.expiresAt = newTokens.expiresAt;
|
|
1073
|
+
}
|
|
1074
|
+
if (this.accountId) {
|
|
1075
|
+
currentConfig.credentials.accountId = this.accountId;
|
|
1076
|
+
}
|
|
1077
|
+
saveConfig(currentConfig);
|
|
1078
|
+
}
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Token refresh failed. Please re-authenticate with: b --auth
|
|
1082
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return this.accessToken;
|
|
1087
|
+
}
|
|
1088
|
+
async call(systemPrompt, userMessage) {
|
|
1089
|
+
const token = await this.ensureValidToken();
|
|
1090
|
+
const requestBody = {
|
|
1091
|
+
model: this.model,
|
|
1092
|
+
instructions: systemPrompt,
|
|
1093
|
+
input: [
|
|
1094
|
+
{
|
|
1095
|
+
type: "message",
|
|
1096
|
+
role: "user",
|
|
1097
|
+
content: [{ type: "input_text", text: userMessage }]
|
|
1098
|
+
}
|
|
1099
|
+
],
|
|
1100
|
+
store: false,
|
|
1101
|
+
stream: true
|
|
1102
|
+
};
|
|
1103
|
+
const headers = {
|
|
1104
|
+
"Content-Type": "application/json",
|
|
1105
|
+
Authorization: `Bearer ${token}`,
|
|
1106
|
+
"OpenAI-Beta": "responses=experimental",
|
|
1107
|
+
originator: "bashio"
|
|
1108
|
+
};
|
|
1109
|
+
if (this.accountId) {
|
|
1110
|
+
headers["ChatGPT-Account-Id"] = this.accountId;
|
|
1111
|
+
}
|
|
1112
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.apiEndpoint, {
|
|
1113
|
+
method: "POST",
|
|
1114
|
+
headers,
|
|
1115
|
+
body: JSON.stringify(requestBody)
|
|
1116
|
+
});
|
|
1117
|
+
if (!response.ok) {
|
|
1118
|
+
const errorText = await response.text();
|
|
1119
|
+
if (response.status === 401) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (response.status === 403) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`Access forbidden. Your ChatGPT subscription may not have access to this model.
|
|
1127
|
+
Error: ${errorText}`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
throw new Error(`ChatGPT API error: ${response.status} - ${errorText}`);
|
|
1131
|
+
}
|
|
1132
|
+
return this.parseStreamingResponse(response);
|
|
1133
|
+
}
|
|
1134
|
+
async parseStreamingResponse(response) {
|
|
1135
|
+
if (!response.body) {
|
|
1136
|
+
throw new Error("No response body from ChatGPT");
|
|
1137
|
+
}
|
|
1138
|
+
const reader = response.body.getReader();
|
|
1139
|
+
const decoder = new TextDecoder("utf-8");
|
|
1140
|
+
let buffer = "";
|
|
1141
|
+
let fullText = "";
|
|
1142
|
+
try {
|
|
1143
|
+
while (true) {
|
|
1144
|
+
const { done, value } = await reader.read();
|
|
1145
|
+
if (done) break;
|
|
1146
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1147
|
+
const lines = buffer.split("\n");
|
|
1148
|
+
buffer = lines.pop() || "";
|
|
1149
|
+
for (const line of lines) {
|
|
1150
|
+
if (!line.trim() || !line.startsWith("data:")) continue;
|
|
1151
|
+
const data = line.substring(5).trim();
|
|
1152
|
+
if (data === "[DONE]") {
|
|
1153
|
+
return fullText.trim();
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const chunk = JSON.parse(data);
|
|
1157
|
+
if (chunk.type === "response.output_text.delta" && chunk.delta) {
|
|
1158
|
+
fullText += chunk.delta;
|
|
1159
|
+
} else if (chunk.choices?.[0]?.delta?.content) {
|
|
1160
|
+
fullText += chunk.choices[0].delta.content;
|
|
1161
|
+
}
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
} finally {
|
|
1167
|
+
reader.releaseLock();
|
|
1168
|
+
}
|
|
1169
|
+
if (!fullText) {
|
|
1170
|
+
throw new Error("No response content from ChatGPT");
|
|
1171
|
+
}
|
|
1172
|
+
return fullText.trim();
|
|
1173
|
+
}
|
|
1174
|
+
async generateCommand(query, context) {
|
|
1175
|
+
const userMessage = context ? `Context: ${context}
|
|
1176
|
+
|
|
1177
|
+
Task: ${query}` : query;
|
|
1178
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1179
|
+
}
|
|
1180
|
+
async explainCommand(command) {
|
|
1181
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1182
|
+
}
|
|
1183
|
+
async validateCredentials() {
|
|
1184
|
+
try {
|
|
1185
|
+
const token = await this.ensureValidToken();
|
|
1186
|
+
return !!token && token.length > 0;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var CHATGPT_SUBSCRIPTION_MODELS = [
|
|
1193
|
+
{ value: "gpt-5.2-codex", label: "GPT-5.2-Codex (recommended for coding)" },
|
|
1194
|
+
{ value: "gpt-5.2", label: "GPT-5.2 (most intelligent)" },
|
|
1195
|
+
{ value: "gpt-5.1-codex-max", label: "GPT-5.1-Codex-Max (long tasks)" },
|
|
1196
|
+
{ value: "gpt-5.1-codex-mini", label: "GPT-5.1-Codex-Mini (fast)" },
|
|
1197
|
+
{ value: "o4-mini", label: "o4-mini (reasoning)" },
|
|
1198
|
+
{ value: "gpt-4o", label: "GPT-4o (legacy)" }
|
|
1199
|
+
];
|
|
1200
|
+
|
|
605
1201
|
// src/providers/claude.ts
|
|
606
1202
|
var ClaudeProvider = class {
|
|
607
1203
|
name = "Claude";
|
|
@@ -680,9 +1276,337 @@ Task: ${query}` : query;
|
|
|
680
1276
|
}
|
|
681
1277
|
};
|
|
682
1278
|
var CLAUDE_MODELS = [
|
|
683
|
-
{
|
|
684
|
-
|
|
685
|
-
|
|
1279
|
+
{
|
|
1280
|
+
value: "claude-opus-4-5-20251101",
|
|
1281
|
+
label: "Claude Opus 4.5 (most intelligent)"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
value: "claude-sonnet-4-5-20250929",
|
|
1285
|
+
label: "Claude Sonnet 4.5 (recommended)"
|
|
1286
|
+
},
|
|
1287
|
+
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (fast)" },
|
|
1288
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
1289
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" }
|
|
1290
|
+
];
|
|
1291
|
+
|
|
1292
|
+
// src/providers/claude-subscription.ts
|
|
1293
|
+
var CLAUDE_CODE_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
1294
|
+
var CLAUDE_CODE_BETAS = [
|
|
1295
|
+
"oauth-2025-04-20",
|
|
1296
|
+
"claude-code-20250219",
|
|
1297
|
+
"interleaved-thinking-2025-05-14",
|
|
1298
|
+
"fine-grained-tool-streaming-2025-05-14"
|
|
1299
|
+
].join(",");
|
|
1300
|
+
var ClaudeSubscriptionProvider = class {
|
|
1301
|
+
name = "Claude (Subscription)";
|
|
1302
|
+
model;
|
|
1303
|
+
accessToken;
|
|
1304
|
+
refreshToken;
|
|
1305
|
+
expiresAt;
|
|
1306
|
+
constructor(config) {
|
|
1307
|
+
this.model = config.model;
|
|
1308
|
+
if (config.credentials.type === "claude_subscription") {
|
|
1309
|
+
this.accessToken = config.credentials.accessToken;
|
|
1310
|
+
this.refreshToken = config.credentials.refreshToken;
|
|
1311
|
+
this.expiresAt = config.credentials.expiresAt;
|
|
1312
|
+
} else {
|
|
1313
|
+
throw new Error("Claude Subscription requires subscription credentials");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async ensureValidToken() {
|
|
1317
|
+
if (isClaudeTokenExpired(this.expiresAt)) {
|
|
1318
|
+
try {
|
|
1319
|
+
const newTokens = await refreshClaudeToken(this.refreshToken);
|
|
1320
|
+
this.accessToken = newTokens.accessToken;
|
|
1321
|
+
this.refreshToken = newTokens.refreshToken;
|
|
1322
|
+
this.expiresAt = newTokens.expiresAt;
|
|
1323
|
+
const currentConfig = loadConfig();
|
|
1324
|
+
if (currentConfig && currentConfig.credentials.type === "claude_subscription") {
|
|
1325
|
+
currentConfig.credentials.accessToken = newTokens.accessToken;
|
|
1326
|
+
currentConfig.credentials.refreshToken = newTokens.refreshToken;
|
|
1327
|
+
currentConfig.credentials.expiresAt = newTokens.expiresAt;
|
|
1328
|
+
saveConfig(currentConfig);
|
|
1329
|
+
}
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`Token refresh failed. Please re-authenticate with: b --auth
|
|
1333
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return this.accessToken;
|
|
1338
|
+
}
|
|
1339
|
+
async call(systemPrompt, userMessage) {
|
|
1340
|
+
const token = await this.ensureValidToken();
|
|
1341
|
+
const messages = [
|
|
1342
|
+
{ role: "user", content: userMessage }
|
|
1343
|
+
];
|
|
1344
|
+
const systemBlocks = [
|
|
1345
|
+
{ type: "text", text: CLAUDE_CODE_PREFIX },
|
|
1346
|
+
{ type: "text", text: systemPrompt }
|
|
1347
|
+
];
|
|
1348
|
+
const headers = {
|
|
1349
|
+
Authorization: `Bearer ${token}`,
|
|
1350
|
+
"Content-Type": "application/json",
|
|
1351
|
+
"anthropic-version": "2023-06-01",
|
|
1352
|
+
"anthropic-beta": CLAUDE_CODE_BETAS,
|
|
1353
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
1354
|
+
"user-agent": "claude-cli/1.0.119 (external, cli)",
|
|
1355
|
+
"x-app": "cli",
|
|
1356
|
+
accept: "application/json"
|
|
1357
|
+
};
|
|
1358
|
+
const response = await fetch(
|
|
1359
|
+
"https://api.anthropic.com/v1/messages?beta=true",
|
|
1360
|
+
{
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
headers,
|
|
1363
|
+
body: JSON.stringify({
|
|
1364
|
+
model: this.model,
|
|
1365
|
+
max_tokens: 1024,
|
|
1366
|
+
system: systemBlocks,
|
|
1367
|
+
messages
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
if (!response.ok) {
|
|
1372
|
+
const error = await response.text();
|
|
1373
|
+
if (response.status === 401) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
1379
|
+
}
|
|
1380
|
+
const data = await response.json();
|
|
1381
|
+
if (data.error) {
|
|
1382
|
+
throw new Error(`Claude API error: ${data.error.message}`);
|
|
1383
|
+
}
|
|
1384
|
+
const textContent = data.content.find((c) => c.type === "text");
|
|
1385
|
+
if (!textContent) {
|
|
1386
|
+
throw new Error("No text response from Claude");
|
|
1387
|
+
}
|
|
1388
|
+
return textContent.text.trim();
|
|
1389
|
+
}
|
|
1390
|
+
async generateCommand(query, context) {
|
|
1391
|
+
const userMessage = context ? `Context: ${context}
|
|
1392
|
+
|
|
1393
|
+
Task: ${query}` : query;
|
|
1394
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1395
|
+
}
|
|
1396
|
+
async explainCommand(command) {
|
|
1397
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1398
|
+
}
|
|
1399
|
+
async validateCredentials() {
|
|
1400
|
+
try {
|
|
1401
|
+
const token = await this.ensureValidToken();
|
|
1402
|
+
const headers = {
|
|
1403
|
+
Authorization: `Bearer ${token}`,
|
|
1404
|
+
"Content-Type": "application/json",
|
|
1405
|
+
"anthropic-version": "2023-06-01",
|
|
1406
|
+
"anthropic-beta": CLAUDE_CODE_BETAS,
|
|
1407
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
1408
|
+
"user-agent": "claude-cli/1.0.119 (external, cli)",
|
|
1409
|
+
"x-app": "cli",
|
|
1410
|
+
accept: "application/json"
|
|
1411
|
+
};
|
|
1412
|
+
const response = await fetch(
|
|
1413
|
+
"https://api.anthropic.com/v1/messages?beta=true",
|
|
1414
|
+
{
|
|
1415
|
+
method: "POST",
|
|
1416
|
+
headers,
|
|
1417
|
+
body: JSON.stringify({
|
|
1418
|
+
model: this.model,
|
|
1419
|
+
max_tokens: 10,
|
|
1420
|
+
system: [{ type: "text", text: CLAUDE_CODE_PREFIX }],
|
|
1421
|
+
messages: [{ role: "user", content: "hi" }]
|
|
1422
|
+
})
|
|
1423
|
+
}
|
|
1424
|
+
);
|
|
1425
|
+
return response.ok;
|
|
1426
|
+
} catch {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var CLAUDE_SUBSCRIPTION_MODELS = [
|
|
1432
|
+
{
|
|
1433
|
+
value: "claude-opus-4-5-20251101",
|
|
1434
|
+
label: "Claude Opus 4.5 (most intelligent)"
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
value: "claude-sonnet-4-5-20250929",
|
|
1438
|
+
label: "Claude Sonnet 4.5 (recommended)"
|
|
1439
|
+
},
|
|
1440
|
+
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (fast)" },
|
|
1441
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
1442
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4 (Max plan only)" }
|
|
1443
|
+
];
|
|
1444
|
+
|
|
1445
|
+
// src/providers/copilot.ts
|
|
1446
|
+
var CopilotProvider = class {
|
|
1447
|
+
name = "GitHub Copilot";
|
|
1448
|
+
model;
|
|
1449
|
+
githubToken;
|
|
1450
|
+
copilotToken;
|
|
1451
|
+
copilotTokenExpiresAt;
|
|
1452
|
+
apiEndpoint;
|
|
1453
|
+
constructor(config) {
|
|
1454
|
+
this.model = config.model;
|
|
1455
|
+
if (config.credentials.type === "copilot") {
|
|
1456
|
+
this.githubToken = config.credentials.githubToken;
|
|
1457
|
+
this.copilotToken = config.credentials.copilotToken;
|
|
1458
|
+
this.copilotTokenExpiresAt = config.credentials.copilotTokenExpiresAt;
|
|
1459
|
+
this.apiEndpoint = this.normalizeEndpoint(
|
|
1460
|
+
config.credentials.apiEndpoint || COPILOT_OAUTH_CONFIG.apiEndpoint
|
|
1461
|
+
);
|
|
1462
|
+
} else {
|
|
1463
|
+
throw new Error("Copilot requires copilot credentials");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
normalizeEndpoint(endpoint) {
|
|
1467
|
+
let url = endpoint;
|
|
1468
|
+
if (!url.startsWith("http")) {
|
|
1469
|
+
url = `https://${url}`;
|
|
1470
|
+
}
|
|
1471
|
+
url = url.replace(/\/\/proxy\./i, "//api.");
|
|
1472
|
+
if (!url.includes("/chat/completions")) {
|
|
1473
|
+
url = `${url}/chat/completions`;
|
|
1474
|
+
}
|
|
1475
|
+
return url;
|
|
1476
|
+
}
|
|
1477
|
+
async ensureValidToken() {
|
|
1478
|
+
if (isCopilotTokenExpired(this.copilotTokenExpiresAt)) {
|
|
1479
|
+
try {
|
|
1480
|
+
const newTokenData = await exchangeGitHubTokenForCopilot(
|
|
1481
|
+
this.githubToken
|
|
1482
|
+
);
|
|
1483
|
+
this.copilotToken = newTokenData.token;
|
|
1484
|
+
const parsed = parseCopilotToken(newTokenData.token);
|
|
1485
|
+
this.copilotTokenExpiresAt = parsed.expiresAt;
|
|
1486
|
+
this.apiEndpoint = parsed.apiEndpoint;
|
|
1487
|
+
const currentConfig = loadConfig();
|
|
1488
|
+
if (currentConfig && currentConfig.credentials.type === "copilot") {
|
|
1489
|
+
currentConfig.credentials.copilotToken = newTokenData.token;
|
|
1490
|
+
currentConfig.credentials.copilotTokenExpiresAt = parsed.expiresAt;
|
|
1491
|
+
currentConfig.credentials.apiEndpoint = parsed.apiEndpoint;
|
|
1492
|
+
saveConfig(currentConfig);
|
|
1493
|
+
}
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
throw new Error(
|
|
1496
|
+
`Copilot token refresh failed. Please re-authenticate with: b --auth
|
|
1497
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return this.copilotToken;
|
|
1502
|
+
}
|
|
1503
|
+
async call(systemPrompt, userMessage) {
|
|
1504
|
+
const token = await this.ensureValidToken();
|
|
1505
|
+
const requestBody = {
|
|
1506
|
+
model: this.model,
|
|
1507
|
+
messages: [
|
|
1508
|
+
{ role: "system", content: systemPrompt },
|
|
1509
|
+
{ role: "user", content: userMessage }
|
|
1510
|
+
],
|
|
1511
|
+
temperature: 0.1,
|
|
1512
|
+
top_p: 1,
|
|
1513
|
+
n: 1,
|
|
1514
|
+
stream: true
|
|
1515
|
+
};
|
|
1516
|
+
const headers = {
|
|
1517
|
+
"Content-Type": "application/json",
|
|
1518
|
+
Accept: "application/json",
|
|
1519
|
+
Authorization: `Bearer ${token}`,
|
|
1520
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
1521
|
+
"Editor-Version": "vscode/1.107.0",
|
|
1522
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
1523
|
+
"Copilot-Integration-Id": "vscode-chat"
|
|
1524
|
+
};
|
|
1525
|
+
const response = await fetch(this.apiEndpoint, {
|
|
1526
|
+
method: "POST",
|
|
1527
|
+
headers,
|
|
1528
|
+
body: JSON.stringify(requestBody)
|
|
1529
|
+
});
|
|
1530
|
+
if (!response.ok) {
|
|
1531
|
+
const errorText = await response.text();
|
|
1532
|
+
if (response.status === 401) {
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
if (response.status === 403) {
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
"Access forbidden. Please ensure you have an active GitHub Copilot subscription."
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
throw new Error(`Copilot API error: ${response.status} - ${errorText}`);
|
|
1543
|
+
}
|
|
1544
|
+
return this.parseStreamingResponse(response);
|
|
1545
|
+
}
|
|
1546
|
+
async parseStreamingResponse(response) {
|
|
1547
|
+
if (!response.body) {
|
|
1548
|
+
throw new Error("No response body from Copilot");
|
|
1549
|
+
}
|
|
1550
|
+
const reader = response.body.getReader();
|
|
1551
|
+
const decoder = new TextDecoder("utf-8");
|
|
1552
|
+
let buffer = "";
|
|
1553
|
+
let fullText = "";
|
|
1554
|
+
try {
|
|
1555
|
+
while (true) {
|
|
1556
|
+
const { done, value } = await reader.read();
|
|
1557
|
+
if (done) break;
|
|
1558
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1559
|
+
const lines = buffer.split("\n");
|
|
1560
|
+
buffer = lines.pop() || "";
|
|
1561
|
+
for (const line of lines) {
|
|
1562
|
+
if (!line.trim() || !line.startsWith("data:")) continue;
|
|
1563
|
+
const data = line.substring(5).trim();
|
|
1564
|
+
if (data === "[DONE]") {
|
|
1565
|
+
return fullText.trim();
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
const chunk = JSON.parse(data);
|
|
1569
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1570
|
+
if (content) {
|
|
1571
|
+
fullText += content;
|
|
1572
|
+
}
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} finally {
|
|
1578
|
+
reader.releaseLock();
|
|
1579
|
+
}
|
|
1580
|
+
if (!fullText) {
|
|
1581
|
+
throw new Error("No response content from Copilot");
|
|
1582
|
+
}
|
|
1583
|
+
return fullText.trim();
|
|
1584
|
+
}
|
|
1585
|
+
async generateCommand(query, context) {
|
|
1586
|
+
const userMessage = context ? `Context: ${context}
|
|
1587
|
+
|
|
1588
|
+
Task: ${query}` : query;
|
|
1589
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1590
|
+
}
|
|
1591
|
+
async explainCommand(command) {
|
|
1592
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1593
|
+
}
|
|
1594
|
+
async validateCredentials() {
|
|
1595
|
+
try {
|
|
1596
|
+
const token = await this.ensureValidToken();
|
|
1597
|
+
return !!token && token.length > 0;
|
|
1598
|
+
} catch {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
var COPILOT_MODELS = [
|
|
1604
|
+
{ value: "gpt-4.1", label: "GPT-4.1 (default)" },
|
|
1605
|
+
{ value: "gpt-5.1", label: "GPT-5.1 (latest)" },
|
|
1606
|
+
{ value: "gpt-5-mini", label: "GPT-5 Mini (fast)" },
|
|
1607
|
+
{ value: "claude-sonnet-4", label: "Claude Sonnet 4" },
|
|
1608
|
+
{ value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5 (latest)" },
|
|
1609
|
+
{ value: "gpt-4o", label: "GPT-4o" }
|
|
686
1610
|
];
|
|
687
1611
|
|
|
688
1612
|
// src/providers/ollama.ts
|
|
@@ -823,9 +1747,12 @@ Task: ${query}` : query;
|
|
|
823
1747
|
}
|
|
824
1748
|
};
|
|
825
1749
|
var OPENAI_MODELS = [
|
|
826
|
-
{ value: "gpt-
|
|
827
|
-
{ value: "gpt-
|
|
828
|
-
{ value: "gpt-
|
|
1750
|
+
{ value: "gpt-5.2", label: "GPT-5.2 (most intelligent)" },
|
|
1751
|
+
{ value: "gpt-5.1", label: "GPT-5.1 (recommended)" },
|
|
1752
|
+
{ value: "gpt-5-mini", label: "GPT-5 Mini (fast)" },
|
|
1753
|
+
{ value: "gpt-5-nano", label: "GPT-5 Nano (fastest)" },
|
|
1754
|
+
{ value: "gpt-4.1", label: "GPT-4.1" },
|
|
1755
|
+
{ value: "gpt-4o", label: "GPT-4o (legacy)" }
|
|
829
1756
|
];
|
|
830
1757
|
|
|
831
1758
|
// src/providers/openrouter.ts
|
|
@@ -916,8 +1843,14 @@ function createProvider(config) {
|
|
|
916
1843
|
switch (config.provider) {
|
|
917
1844
|
case "claude":
|
|
918
1845
|
return new ClaudeProvider(providerConfig);
|
|
1846
|
+
case "claude-subscription":
|
|
1847
|
+
return new ClaudeSubscriptionProvider(providerConfig);
|
|
919
1848
|
case "openai":
|
|
920
1849
|
return new OpenAIProvider(providerConfig);
|
|
1850
|
+
case "chatgpt-subscription":
|
|
1851
|
+
return new ChatGPTSubscriptionProvider(providerConfig);
|
|
1852
|
+
case "copilot":
|
|
1853
|
+
return new CopilotProvider(providerConfig);
|
|
921
1854
|
case "ollama":
|
|
922
1855
|
return new OllamaProvider(providerConfig);
|
|
923
1856
|
case "openrouter":
|
|
@@ -937,6 +1870,19 @@ function createSpinner(text) {
|
|
|
937
1870
|
}
|
|
938
1871
|
|
|
939
1872
|
// src/core/auth.ts
|
|
1873
|
+
async function openBrowser(url) {
|
|
1874
|
+
try {
|
|
1875
|
+
const { exec: exec2 } = await import("child_process");
|
|
1876
|
+
const { promisify: promisify2 } = await import("util");
|
|
1877
|
+
const execAsync2 = promisify2(exec2);
|
|
1878
|
+
const platform = process.platform;
|
|
1879
|
+
const command = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
1880
|
+
await execAsync2(command);
|
|
1881
|
+
return true;
|
|
1882
|
+
} catch {
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
940
1886
|
function showWelcomeBanner() {
|
|
941
1887
|
const cyan = pc3.cyan;
|
|
942
1888
|
const dim = pc3.dim;
|
|
@@ -1000,14 +1946,29 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1000
1946
|
const provider = await select({
|
|
1001
1947
|
message: "Select your AI provider:",
|
|
1002
1948
|
choices: [
|
|
1949
|
+
{
|
|
1950
|
+
value: "claude-subscription",
|
|
1951
|
+
name: "Claude Pro/Max (Subscription)",
|
|
1952
|
+
description: "Use your existing Claude subscription"
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
value: "chatgpt-subscription",
|
|
1956
|
+
name: "ChatGPT Plus/Pro (Subscription)",
|
|
1957
|
+
description: "Use your existing ChatGPT subscription"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
value: "copilot",
|
|
1961
|
+
name: "GitHub Copilot",
|
|
1962
|
+
description: "Use your GitHub Copilot subscription"
|
|
1963
|
+
},
|
|
1003
1964
|
{
|
|
1004
1965
|
value: "claude",
|
|
1005
|
-
name: "Claude (
|
|
1966
|
+
name: "Claude (API Key)",
|
|
1006
1967
|
description: "Use Anthropic API key"
|
|
1007
1968
|
},
|
|
1008
1969
|
{
|
|
1009
1970
|
value: "openai",
|
|
1010
|
-
name: "ChatGPT (
|
|
1971
|
+
name: "ChatGPT (API Key)",
|
|
1011
1972
|
description: "Use OpenAI API key"
|
|
1012
1973
|
},
|
|
1013
1974
|
{
|
|
@@ -1025,6 +1986,105 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1025
1986
|
let credentials;
|
|
1026
1987
|
let model;
|
|
1027
1988
|
switch (provider) {
|
|
1989
|
+
case "claude-subscription": {
|
|
1990
|
+
console.log();
|
|
1991
|
+
console.log(
|
|
1992
|
+
pc3.yellow(
|
|
1993
|
+
" Note: This uses your Claude Pro/Max subscription via OAuth."
|
|
1994
|
+
)
|
|
1995
|
+
);
|
|
1996
|
+
console.log(
|
|
1997
|
+
pc3.dim(
|
|
1998
|
+
" Your credentials are stored locally and refreshed automatically."
|
|
1999
|
+
)
|
|
2000
|
+
);
|
|
2001
|
+
console.log();
|
|
2002
|
+
const tokens = await performClaudeOAuth();
|
|
2003
|
+
if (!tokens) {
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
credentials = {
|
|
2007
|
+
type: "claude_subscription",
|
|
2008
|
+
accessToken: tokens.accessToken,
|
|
2009
|
+
refreshToken: tokens.refreshToken,
|
|
2010
|
+
expiresAt: tokens.expiresAt,
|
|
2011
|
+
email: tokens.email
|
|
2012
|
+
};
|
|
2013
|
+
model = await select({
|
|
2014
|
+
message: "Select model:",
|
|
2015
|
+
choices: CLAUDE_SUBSCRIPTION_MODELS.map((m) => ({
|
|
2016
|
+
value: m.value,
|
|
2017
|
+
name: m.label
|
|
2018
|
+
}))
|
|
2019
|
+
});
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
case "chatgpt-subscription": {
|
|
2023
|
+
console.log();
|
|
2024
|
+
console.log(
|
|
2025
|
+
pc3.yellow(
|
|
2026
|
+
" Note: This uses your ChatGPT Plus/Pro subscription via OAuth."
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
console.log(
|
|
2030
|
+
pc3.red(" WARNING: This is EXPERIMENTAL and uses an unofficial API.")
|
|
2031
|
+
);
|
|
2032
|
+
console.log(
|
|
2033
|
+
pc3.dim(" The API may change or stop working without notice.")
|
|
2034
|
+
);
|
|
2035
|
+
console.log(
|
|
2036
|
+
pc3.dim(" For stable usage, consider using an API key instead.")
|
|
2037
|
+
);
|
|
2038
|
+
console.log();
|
|
2039
|
+
const tokens = await performChatGPTOAuth();
|
|
2040
|
+
if (!tokens) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
credentials = {
|
|
2044
|
+
type: "chatgpt_subscription",
|
|
2045
|
+
accessToken: tokens.accessToken,
|
|
2046
|
+
refreshToken: tokens.refreshToken || void 0,
|
|
2047
|
+
expiresAt: tokens.expiresAt || void 0,
|
|
2048
|
+
accountId: tokens.accountId
|
|
2049
|
+
};
|
|
2050
|
+
model = await select({
|
|
2051
|
+
message: "Select model:",
|
|
2052
|
+
choices: CHATGPT_SUBSCRIPTION_MODELS.map((m) => ({
|
|
2053
|
+
value: m.value,
|
|
2054
|
+
name: m.label
|
|
2055
|
+
}))
|
|
2056
|
+
});
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
case "copilot": {
|
|
2060
|
+
console.log();
|
|
2061
|
+
console.log(
|
|
2062
|
+
pc3.yellow(" Note: This uses your GitHub Copilot subscription.")
|
|
2063
|
+
);
|
|
2064
|
+
console.log(
|
|
2065
|
+
pc3.dim(" You need an active GitHub Copilot subscription to use this.")
|
|
2066
|
+
);
|
|
2067
|
+
console.log();
|
|
2068
|
+
const copilotResult = await performCopilotDeviceFlow();
|
|
2069
|
+
if (!copilotResult) {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
credentials = {
|
|
2073
|
+
type: "copilot",
|
|
2074
|
+
githubToken: copilotResult.githubToken,
|
|
2075
|
+
copilotToken: copilotResult.copilotToken,
|
|
2076
|
+
copilotTokenExpiresAt: copilotResult.copilotTokenExpiresAt,
|
|
2077
|
+
apiEndpoint: copilotResult.apiEndpoint
|
|
2078
|
+
};
|
|
2079
|
+
model = await select({
|
|
2080
|
+
message: "Select model:",
|
|
2081
|
+
choices: COPILOT_MODELS.map((m) => ({
|
|
2082
|
+
value: m.value,
|
|
2083
|
+
name: m.label
|
|
2084
|
+
}))
|
|
2085
|
+
});
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
1028
2088
|
case "claude": {
|
|
1029
2089
|
const apiKey = await password({
|
|
1030
2090
|
message: "Enter your Anthropic API key:",
|
|
@@ -1134,6 +2194,214 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1134
2194
|
console.log();
|
|
1135
2195
|
return true;
|
|
1136
2196
|
}
|
|
2197
|
+
async function performClaudeOAuth() {
|
|
2198
|
+
const authMethod = await select({
|
|
2199
|
+
message: "How would you like to authenticate?",
|
|
2200
|
+
choices: [
|
|
2201
|
+
{
|
|
2202
|
+
value: "browser",
|
|
2203
|
+
name: "Open browser automatically",
|
|
2204
|
+
description: "Recommended - opens browser and waits for callback"
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
value: "manual",
|
|
2208
|
+
name: "Manual URL copy/paste",
|
|
2209
|
+
description: "Copy URL to browser, then paste the callback URL"
|
|
2210
|
+
}
|
|
2211
|
+
]
|
|
2212
|
+
});
|
|
2213
|
+
const pkce = generatePKCE();
|
|
2214
|
+
const authUrl = buildClaudeAuthUrl(pkce);
|
|
2215
|
+
if (authMethod === "browser") {
|
|
2216
|
+
return performBrowserOAuth(
|
|
2217
|
+
"Claude",
|
|
2218
|
+
authUrl,
|
|
2219
|
+
pkce,
|
|
2220
|
+
CLAUDE_OAUTH_CONFIG.callbackPort,
|
|
2221
|
+
async (code) => exchangeClaudeCode(code, pkce.codeVerifier, pkce.state)
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
return performManualOAuth(
|
|
2225
|
+
"Claude",
|
|
2226
|
+
authUrl,
|
|
2227
|
+
pkce,
|
|
2228
|
+
async (code) => exchangeClaudeCode(code, pkce.codeVerifier, pkce.state)
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
async function performChatGPTOAuth() {
|
|
2232
|
+
const authMethod = await select({
|
|
2233
|
+
message: "How would you like to authenticate?",
|
|
2234
|
+
choices: [
|
|
2235
|
+
{
|
|
2236
|
+
value: "browser",
|
|
2237
|
+
name: "Open browser automatically",
|
|
2238
|
+
description: "Recommended - opens browser and waits for callback"
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
value: "manual",
|
|
2242
|
+
name: "Manual URL copy/paste",
|
|
2243
|
+
description: "Copy URL to browser, then paste the callback URL"
|
|
2244
|
+
}
|
|
2245
|
+
]
|
|
2246
|
+
});
|
|
2247
|
+
const pkce = generatePKCE();
|
|
2248
|
+
const authUrl = buildChatGPTAuthUrl(pkce);
|
|
2249
|
+
if (authMethod === "browser") {
|
|
2250
|
+
return performBrowserOAuth(
|
|
2251
|
+
"ChatGPT",
|
|
2252
|
+
authUrl,
|
|
2253
|
+
pkce,
|
|
2254
|
+
CHATGPT_OAUTH_CONFIG.callbackPort,
|
|
2255
|
+
async (code) => exchangeChatGPTCode(code, pkce.codeVerifier)
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
return performManualOAuth(
|
|
2259
|
+
"ChatGPT",
|
|
2260
|
+
authUrl,
|
|
2261
|
+
pkce,
|
|
2262
|
+
async (code) => exchangeChatGPTCode(code, pkce.codeVerifier)
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
async function performBrowserOAuth(providerName, authUrl, pkce, port, exchangeCode) {
|
|
2266
|
+
console.log();
|
|
2267
|
+
console.log(pc3.dim(" Starting local callback server..."));
|
|
2268
|
+
const serverPromise = startCallbackServer(port, pkce.state);
|
|
2269
|
+
const browserOpened = await openBrowser(authUrl);
|
|
2270
|
+
if (browserOpened) {
|
|
2271
|
+
console.log(
|
|
2272
|
+
pc3.green(` Browser opened. Please log in to ${providerName}.`)
|
|
2273
|
+
);
|
|
2274
|
+
} else {
|
|
2275
|
+
console.log(pc3.yellow(" Could not open browser automatically."));
|
|
2276
|
+
console.log(pc3.dim(" Please open this URL manually:"));
|
|
2277
|
+
console.log();
|
|
2278
|
+
console.log(` ${pc3.cyan(authUrl)}`);
|
|
2279
|
+
}
|
|
2280
|
+
console.log();
|
|
2281
|
+
console.log(pc3.dim(" Waiting for authentication (5 minute timeout)..."));
|
|
2282
|
+
try {
|
|
2283
|
+
const { code } = await serverPromise;
|
|
2284
|
+
const spinner = createSpinner("Exchanging authorization code...").start();
|
|
2285
|
+
try {
|
|
2286
|
+
const tokens = await exchangeCode(code);
|
|
2287
|
+
spinner.succeed("Authentication successful!");
|
|
2288
|
+
return tokens;
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
spinner.fail(
|
|
2291
|
+
`Token exchange failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2292
|
+
);
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
logger.error(
|
|
2297
|
+
`Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2298
|
+
);
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
async function performManualOAuth(providerName, authUrl, pkce, exchangeCode) {
|
|
2303
|
+
console.log();
|
|
2304
|
+
console.log(
|
|
2305
|
+
pc3.dim(
|
|
2306
|
+
` Open this URL in your browser to authenticate with ${providerName}:`
|
|
2307
|
+
)
|
|
2308
|
+
);
|
|
2309
|
+
console.log();
|
|
2310
|
+
console.log(` ${pc3.cyan(authUrl)}`);
|
|
2311
|
+
console.log();
|
|
2312
|
+
console.log(
|
|
2313
|
+
pc3.dim(" After logging in, you will be redirected to a localhost URL.")
|
|
2314
|
+
);
|
|
2315
|
+
console.log(pc3.dim(" Copy the full redirect URL and paste it below."));
|
|
2316
|
+
console.log();
|
|
2317
|
+
const callbackUrl = await input3({
|
|
2318
|
+
message: "Paste the callback URL here:"
|
|
2319
|
+
});
|
|
2320
|
+
try {
|
|
2321
|
+
const url = new URL(callbackUrl);
|
|
2322
|
+
const code = url.searchParams.get("code");
|
|
2323
|
+
const state = url.searchParams.get("state");
|
|
2324
|
+
const error = url.searchParams.get("error");
|
|
2325
|
+
if (error) {
|
|
2326
|
+
logger.error(`OAuth error: ${error}`);
|
|
2327
|
+
return null;
|
|
2328
|
+
}
|
|
2329
|
+
if (!code) {
|
|
2330
|
+
logger.error("No authorization code found in URL");
|
|
2331
|
+
return null;
|
|
2332
|
+
}
|
|
2333
|
+
if (state !== pkce.state) {
|
|
2334
|
+
logger.error("State mismatch - possible security issue");
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
const spinner = createSpinner("Exchanging authorization code...").start();
|
|
2338
|
+
try {
|
|
2339
|
+
const tokens = await exchangeCode(code);
|
|
2340
|
+
spinner.succeed("Authentication successful!");
|
|
2341
|
+
return tokens;
|
|
2342
|
+
} catch (err) {
|
|
2343
|
+
spinner.fail(
|
|
2344
|
+
`Token exchange failed: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
2345
|
+
);
|
|
2346
|
+
return null;
|
|
2347
|
+
}
|
|
2348
|
+
} catch {
|
|
2349
|
+
logger.error("Invalid URL format");
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
async function performCopilotDeviceFlow() {
|
|
2354
|
+
try {
|
|
2355
|
+
const spinner = createSpinner("Requesting device code...").start();
|
|
2356
|
+
const deviceCode = await requestCopilotDeviceCode();
|
|
2357
|
+
spinner.stop();
|
|
2358
|
+
console.log();
|
|
2359
|
+
console.log(pc3.bold(" To authenticate with GitHub Copilot:"));
|
|
2360
|
+
console.log();
|
|
2361
|
+
console.log(
|
|
2362
|
+
` 1. Visit: ${pc3.cyan(pc3.underline(deviceCode.verification_uri))}`
|
|
2363
|
+
);
|
|
2364
|
+
console.log(` 2. Enter code: ${pc3.bold(pc3.green(deviceCode.user_code))}`);
|
|
2365
|
+
console.log();
|
|
2366
|
+
const browserOpened = await openBrowser(deviceCode.verification_uri);
|
|
2367
|
+
if (browserOpened) {
|
|
2368
|
+
console.log(pc3.dim(" Browser opened automatically."));
|
|
2369
|
+
}
|
|
2370
|
+
console.log(pc3.dim(" Waiting for authorization..."));
|
|
2371
|
+
console.log();
|
|
2372
|
+
const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
|
|
2373
|
+
const intervalMs = (deviceCode.interval || 5) * 1e3;
|
|
2374
|
+
const githubToken = await pollForCopilotAccessToken(
|
|
2375
|
+
deviceCode.device_code,
|
|
2376
|
+
intervalMs,
|
|
2377
|
+
expiresAt
|
|
2378
|
+
);
|
|
2379
|
+
const exchangeSpinner = createSpinner(
|
|
2380
|
+
"Getting Copilot access token..."
|
|
2381
|
+
).start();
|
|
2382
|
+
try {
|
|
2383
|
+
const copilotTokenData = await exchangeGitHubTokenForCopilot(githubToken);
|
|
2384
|
+
const parsed = parseCopilotToken(copilotTokenData.token);
|
|
2385
|
+
exchangeSpinner.succeed("Authentication successful!");
|
|
2386
|
+
return {
|
|
2387
|
+
githubToken,
|
|
2388
|
+
copilotToken: copilotTokenData.token,
|
|
2389
|
+
copilotTokenExpiresAt: parsed.expiresAt,
|
|
2390
|
+
apiEndpoint: parsed.apiEndpoint
|
|
2391
|
+
};
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
exchangeSpinner.fail(
|
|
2394
|
+
`Failed to get Copilot token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2395
|
+
);
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
logger.error(
|
|
2400
|
+
`GitHub authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2401
|
+
);
|
|
2402
|
+
return null;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
1137
2405
|
|
|
1138
2406
|
// src/cli/commands/AuthCommand.ts
|
|
1139
2407
|
var AuthCommand = class extends Command2 {
|
|
@@ -2113,6 +3381,39 @@ var ModelCommand = class extends Command8 {
|
|
|
2113
3381
|
});
|
|
2114
3382
|
break;
|
|
2115
3383
|
}
|
|
3384
|
+
case "claude-subscription": {
|
|
3385
|
+
newModel = await selectWithEsc({
|
|
3386
|
+
message: "Select new model:",
|
|
3387
|
+
choices: CLAUDE_SUBSCRIPTION_MODELS.map((m) => ({
|
|
3388
|
+
value: m.value,
|
|
3389
|
+
name: m.label
|
|
3390
|
+
})),
|
|
3391
|
+
default: config.model
|
|
3392
|
+
});
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
case "chatgpt-subscription": {
|
|
3396
|
+
newModel = await selectWithEsc({
|
|
3397
|
+
message: "Select new model:",
|
|
3398
|
+
choices: CHATGPT_SUBSCRIPTION_MODELS.map((m) => ({
|
|
3399
|
+
value: m.value,
|
|
3400
|
+
name: m.label
|
|
3401
|
+
})),
|
|
3402
|
+
default: config.model
|
|
3403
|
+
});
|
|
3404
|
+
break;
|
|
3405
|
+
}
|
|
3406
|
+
case "copilot": {
|
|
3407
|
+
newModel = await selectWithEsc({
|
|
3408
|
+
message: "Select new model:",
|
|
3409
|
+
choices: COPILOT_MODELS.map((m) => ({
|
|
3410
|
+
value: m.value,
|
|
3411
|
+
name: m.label
|
|
3412
|
+
})),
|
|
3413
|
+
default: config.model
|
|
3414
|
+
});
|
|
3415
|
+
break;
|
|
3416
|
+
}
|
|
2116
3417
|
default:
|
|
2117
3418
|
logger.error(`Unknown provider: ${config.provider}`);
|
|
2118
3419
|
return 1;
|
|
@@ -2544,9 +3845,11 @@ var SuggestShortcutsCommand = class extends Command12 {
|
|
|
2544
3845
|
};
|
|
2545
3846
|
|
|
2546
3847
|
// src/cli/index.ts
|
|
3848
|
+
var require2 = createRequire(import.meta.url);
|
|
3849
|
+
var packageJson = require2("../../package.json");
|
|
2547
3850
|
var pkg = {
|
|
2548
|
-
name:
|
|
2549
|
-
version:
|
|
3851
|
+
name: packageJson.name,
|
|
3852
|
+
version: packageJson.version
|
|
2550
3853
|
};
|
|
2551
3854
|
var notifier = updateNotifier({
|
|
2552
3855
|
pkg,
|