bashio 1.0.0 → 1.1.1
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 +1539 -132
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,10 @@ import { join } from "path";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
var ProviderName = z.enum([
|
|
22
22
|
"claude",
|
|
23
|
+
"claude-subscription",
|
|
23
24
|
"openai",
|
|
25
|
+
"chatgpt-subscription",
|
|
26
|
+
"copilot",
|
|
24
27
|
"ollama",
|
|
25
28
|
"openrouter"
|
|
26
29
|
]);
|
|
@@ -36,10 +39,41 @@ var LocalCredentials = z.object({
|
|
|
36
39
|
type: z.literal("local"),
|
|
37
40
|
host: z.string().default("http://localhost:11434")
|
|
38
41
|
});
|
|
42
|
+
var ClaudeSubscriptionCredentials = z.object({
|
|
43
|
+
type: z.literal("claude_subscription"),
|
|
44
|
+
accessToken: z.string(),
|
|
45
|
+
refreshToken: z.string(),
|
|
46
|
+
expiresAt: z.number(),
|
|
47
|
+
// Unix timestamp in ms
|
|
48
|
+
email: z.string().optional()
|
|
49
|
+
});
|
|
50
|
+
var ChatGPTSubscriptionCredentials = z.object({
|
|
51
|
+
type: z.literal("chatgpt_subscription"),
|
|
52
|
+
accessToken: z.string(),
|
|
53
|
+
refreshToken: z.string().optional(),
|
|
54
|
+
expiresAt: z.number().optional(),
|
|
55
|
+
// Unix timestamp in ms
|
|
56
|
+
accountId: z.string().optional()
|
|
57
|
+
// ChatGPT account ID for API requests
|
|
58
|
+
});
|
|
59
|
+
var CopilotCredentials = z.object({
|
|
60
|
+
type: z.literal("copilot"),
|
|
61
|
+
githubToken: z.string(),
|
|
62
|
+
// GitHub OAuth access token (gho_xxx)
|
|
63
|
+
copilotToken: z.string(),
|
|
64
|
+
// Copilot API token (short-lived)
|
|
65
|
+
copilotTokenExpiresAt: z.number(),
|
|
66
|
+
// Unix timestamp in ms
|
|
67
|
+
apiEndpoint: z.string().optional()
|
|
68
|
+
// Derived from token (api.individual/business.githubcopilot.com)
|
|
69
|
+
});
|
|
39
70
|
var Credentials = z.discriminatedUnion("type", [
|
|
40
71
|
SessionCredentials,
|
|
41
72
|
ApiKeyCredentials,
|
|
42
|
-
LocalCredentials
|
|
73
|
+
LocalCredentials,
|
|
74
|
+
ClaudeSubscriptionCredentials,
|
|
75
|
+
ChatGPTSubscriptionCredentials,
|
|
76
|
+
CopilotCredentials
|
|
43
77
|
]);
|
|
44
78
|
var Settings = z.object({
|
|
45
79
|
confirmBeforeExecute: z.boolean().default(true),
|
|
@@ -48,13 +82,24 @@ var Settings = z.object({
|
|
|
48
82
|
historyMaxEntries: z.number().default(2e3),
|
|
49
83
|
autoConfirmShortcuts: z.boolean().default(false)
|
|
50
84
|
});
|
|
51
|
-
var
|
|
52
|
-
version: z.
|
|
85
|
+
var ConfigV1 = z.object({
|
|
86
|
+
version: z.literal(1).default(1),
|
|
53
87
|
provider: ProviderName,
|
|
54
88
|
model: z.string(),
|
|
55
89
|
credentials: Credentials,
|
|
56
90
|
settings: Settings.optional()
|
|
57
91
|
});
|
|
92
|
+
var ProviderSettings = z.object({
|
|
93
|
+
model: z.string(),
|
|
94
|
+
credentials: Credentials
|
|
95
|
+
});
|
|
96
|
+
var ConfigV2 = z.object({
|
|
97
|
+
version: z.literal(2),
|
|
98
|
+
activeProvider: ProviderName,
|
|
99
|
+
providers: z.record(z.string(), ProviderSettings),
|
|
100
|
+
settings: Settings.optional()
|
|
101
|
+
});
|
|
102
|
+
var Config = z.union([ConfigV2, ConfigV1]);
|
|
58
103
|
var ShortcutDefinition = z.object({
|
|
59
104
|
template: z.string(),
|
|
60
105
|
args: z.array(z.string()).default([]),
|
|
@@ -98,6 +143,19 @@ function ensureConfigDir() {
|
|
|
98
143
|
function configExists() {
|
|
99
144
|
return existsSync(CONFIG_FILE);
|
|
100
145
|
}
|
|
146
|
+
function migrateV1toV2(v1) {
|
|
147
|
+
return {
|
|
148
|
+
version: 2,
|
|
149
|
+
activeProvider: v1.provider,
|
|
150
|
+
providers: {
|
|
151
|
+
[v1.provider]: {
|
|
152
|
+
model: v1.model,
|
|
153
|
+
credentials: v1.credentials
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
settings: v1.settings
|
|
157
|
+
};
|
|
158
|
+
}
|
|
101
159
|
function loadConfig() {
|
|
102
160
|
if (!configExists()) {
|
|
103
161
|
return null;
|
|
@@ -105,7 +163,17 @@ function loadConfig() {
|
|
|
105
163
|
try {
|
|
106
164
|
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
107
165
|
const data = JSON.parse(raw);
|
|
108
|
-
|
|
166
|
+
const v2Result = ConfigV2.safeParse(data);
|
|
167
|
+
if (v2Result.success) {
|
|
168
|
+
return v2Result.data;
|
|
169
|
+
}
|
|
170
|
+
const v1Result = ConfigV1.safeParse(data);
|
|
171
|
+
if (v1Result.success) {
|
|
172
|
+
const migrated = migrateV1toV2(v1Result.data);
|
|
173
|
+
saveConfig(migrated);
|
|
174
|
+
return migrated;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
109
177
|
} catch {
|
|
110
178
|
return null;
|
|
111
179
|
}
|
|
@@ -122,6 +190,19 @@ function getConfigPath() {
|
|
|
122
190
|
function getConfigDir() {
|
|
123
191
|
return CONFIG_DIR;
|
|
124
192
|
}
|
|
193
|
+
function isProviderConfigured(config, provider) {
|
|
194
|
+
return provider in config.providers;
|
|
195
|
+
}
|
|
196
|
+
function setProviderConfig(config, provider, settings, setActive = true) {
|
|
197
|
+
return {
|
|
198
|
+
...config,
|
|
199
|
+
activeProvider: setActive ? provider : config.activeProvider,
|
|
200
|
+
providers: {
|
|
201
|
+
...config.providers,
|
|
202
|
+
[provider]: settings
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
125
206
|
|
|
126
207
|
// src/core/database.ts
|
|
127
208
|
import { chmodSync as chmodSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -587,6 +668,392 @@ import pc4 from "picocolors";
|
|
|
587
668
|
import { input as input3, password, select } from "@inquirer/prompts";
|
|
588
669
|
import pc3 from "picocolors";
|
|
589
670
|
|
|
671
|
+
// src/core/oauth.ts
|
|
672
|
+
import * as crypto from "crypto";
|
|
673
|
+
import * as http from "http";
|
|
674
|
+
import { URL as URL2 } from "url";
|
|
675
|
+
var CLAUDE_OAUTH_CONFIG = {
|
|
676
|
+
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
677
|
+
authorizationUrl: "https://claude.ai/oauth/authorize",
|
|
678
|
+
tokenUrl: "https://console.anthropic.com/v1/oauth/token",
|
|
679
|
+
redirectUri: "http://localhost:8765/callback",
|
|
680
|
+
scopes: "org:create_api_key user:profile user:inference",
|
|
681
|
+
callbackPort: 8765
|
|
682
|
+
};
|
|
683
|
+
var COPILOT_OAUTH_CONFIG = {
|
|
684
|
+
clientId: "Iv1.b507a08c87ecfe98",
|
|
685
|
+
// Official GitHub Copilot client ID
|
|
686
|
+
deviceCodeUrl: "https://github.com/login/device/code",
|
|
687
|
+
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
|
688
|
+
copilotTokenUrl: "https://api.github.com/copilot_internal/v2/token",
|
|
689
|
+
apiEndpoint: "https://api.githubcopilot.com/chat/completions",
|
|
690
|
+
scope: "read:user"
|
|
691
|
+
};
|
|
692
|
+
var CHATGPT_OAUTH_CONFIG = {
|
|
693
|
+
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
694
|
+
// Official Codex CLI client ID
|
|
695
|
+
authorizationUrl: "https://auth.openai.com/oauth/authorize",
|
|
696
|
+
tokenUrl: "https://auth.openai.com/oauth/token",
|
|
697
|
+
redirectUri: "http://localhost:1455/auth/callback",
|
|
698
|
+
scopes: "openid profile email offline_access",
|
|
699
|
+
callbackPort: 1455,
|
|
700
|
+
audience: "https://api.openai.com/v1",
|
|
701
|
+
// Backend API endpoint (subscription OAuth uses this, NOT api.openai.com)
|
|
702
|
+
apiEndpoint: "https://chatgpt.com/backend-api/codex/responses"
|
|
703
|
+
};
|
|
704
|
+
function generateCodeVerifier() {
|
|
705
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
706
|
+
}
|
|
707
|
+
function generateCodeChallenge(verifier) {
|
|
708
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
709
|
+
}
|
|
710
|
+
function generateState() {
|
|
711
|
+
return crypto.randomBytes(16).toString("hex");
|
|
712
|
+
}
|
|
713
|
+
function generatePKCE() {
|
|
714
|
+
const codeVerifier = generateCodeVerifier();
|
|
715
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
716
|
+
const state = generateState();
|
|
717
|
+
return { codeVerifier, codeChallenge, state };
|
|
718
|
+
}
|
|
719
|
+
function buildClaudeAuthUrl(pkce) {
|
|
720
|
+
const params = new URLSearchParams({
|
|
721
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
722
|
+
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
723
|
+
scope: CLAUDE_OAUTH_CONFIG.scopes,
|
|
724
|
+
code_challenge: pkce.codeChallenge,
|
|
725
|
+
code_challenge_method: "S256",
|
|
726
|
+
response_type: "code",
|
|
727
|
+
state: pkce.state
|
|
728
|
+
});
|
|
729
|
+
return `${CLAUDE_OAUTH_CONFIG.authorizationUrl}?${params.toString()}`;
|
|
730
|
+
}
|
|
731
|
+
async function exchangeClaudeCode(code, codeVerifier, state) {
|
|
732
|
+
const body = {
|
|
733
|
+
code,
|
|
734
|
+
state,
|
|
735
|
+
grant_type: "authorization_code",
|
|
736
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
737
|
+
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
738
|
+
code_verifier: codeVerifier
|
|
739
|
+
};
|
|
740
|
+
const response = await fetch(CLAUDE_OAUTH_CONFIG.tokenUrl, {
|
|
741
|
+
method: "POST",
|
|
742
|
+
headers: { "Content-Type": "application/json" },
|
|
743
|
+
body: JSON.stringify(body)
|
|
744
|
+
});
|
|
745
|
+
if (!response.ok) {
|
|
746
|
+
const errorText = await response.text();
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Claude token exchange failed: ${response.status} - ${errorText}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const data = await response.json();
|
|
752
|
+
if (!data.refresh_token) {
|
|
753
|
+
throw new Error("Claude token exchange did not return a refresh_token");
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
accessToken: data.access_token,
|
|
757
|
+
refreshToken: data.refresh_token,
|
|
758
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
759
|
+
email: data.email
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function refreshClaudeToken(refreshToken) {
|
|
763
|
+
const body = {
|
|
764
|
+
grant_type: "refresh_token",
|
|
765
|
+
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
766
|
+
refresh_token: refreshToken
|
|
767
|
+
};
|
|
768
|
+
const response = await fetch(CLAUDE_OAUTH_CONFIG.tokenUrl, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: { "Content-Type": "application/json" },
|
|
771
|
+
body: JSON.stringify(body)
|
|
772
|
+
});
|
|
773
|
+
if (!response.ok) {
|
|
774
|
+
const errorText = await response.text();
|
|
775
|
+
throw new Error(
|
|
776
|
+
`Claude token refresh failed: ${response.status} - ${errorText}`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
const data = await response.json();
|
|
780
|
+
return {
|
|
781
|
+
accessToken: data.access_token,
|
|
782
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
783
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
784
|
+
email: data.email
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function isClaudeTokenExpired(expiresAt) {
|
|
788
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
789
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
790
|
+
}
|
|
791
|
+
function parseJWTClaims(token) {
|
|
792
|
+
try {
|
|
793
|
+
const parts = token.split(".");
|
|
794
|
+
if (parts.length !== 3) return null;
|
|
795
|
+
const payload = parts[1];
|
|
796
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
797
|
+
return JSON.parse(decoded);
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function extractAccountIdFromToken(accessToken, idToken) {
|
|
803
|
+
const tokens = idToken ? [idToken, accessToken] : [accessToken];
|
|
804
|
+
for (const token of tokens) {
|
|
805
|
+
const claims = parseJWTClaims(token);
|
|
806
|
+
if (!claims) continue;
|
|
807
|
+
const accountId = claims.chatgpt_account_id || claims["https://api.openai.com/auth"]?.chatgpt_account_id || claims.organizations?.[0]?.id;
|
|
808
|
+
if (accountId) return accountId;
|
|
809
|
+
}
|
|
810
|
+
return void 0;
|
|
811
|
+
}
|
|
812
|
+
function buildChatGPTAuthUrl(pkce) {
|
|
813
|
+
const params = new URLSearchParams({
|
|
814
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
815
|
+
redirect_uri: CHATGPT_OAUTH_CONFIG.redirectUri,
|
|
816
|
+
scope: CHATGPT_OAUTH_CONFIG.scopes,
|
|
817
|
+
code_challenge: pkce.codeChallenge,
|
|
818
|
+
code_challenge_method: "S256",
|
|
819
|
+
response_type: "code",
|
|
820
|
+
state: pkce.state,
|
|
821
|
+
audience: CHATGPT_OAUTH_CONFIG.audience
|
|
822
|
+
});
|
|
823
|
+
return `${CHATGPT_OAUTH_CONFIG.authorizationUrl}?${params.toString()}`;
|
|
824
|
+
}
|
|
825
|
+
async function exchangeChatGPTCode(code, codeVerifier) {
|
|
826
|
+
const body = new URLSearchParams({
|
|
827
|
+
grant_type: "authorization_code",
|
|
828
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
829
|
+
code,
|
|
830
|
+
redirect_uri: CHATGPT_OAUTH_CONFIG.redirectUri,
|
|
831
|
+
code_verifier: codeVerifier
|
|
832
|
+
});
|
|
833
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.tokenUrl, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
836
|
+
body: body.toString()
|
|
837
|
+
});
|
|
838
|
+
if (!response.ok) {
|
|
839
|
+
const errorText = await response.text();
|
|
840
|
+
throw new Error(
|
|
841
|
+
`ChatGPT token exchange failed: ${response.status} - ${errorText}`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const data = await response.json();
|
|
845
|
+
const accountId = extractAccountIdFromToken(data.access_token, data.id_token);
|
|
846
|
+
return {
|
|
847
|
+
accessToken: data.access_token,
|
|
848
|
+
refreshToken: data.refresh_token || "",
|
|
849
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : 0,
|
|
850
|
+
accountId
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
async function refreshChatGPTToken(refreshToken) {
|
|
854
|
+
const body = new URLSearchParams({
|
|
855
|
+
grant_type: "refresh_token",
|
|
856
|
+
client_id: CHATGPT_OAUTH_CONFIG.clientId,
|
|
857
|
+
refresh_token: refreshToken
|
|
858
|
+
});
|
|
859
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.tokenUrl, {
|
|
860
|
+
method: "POST",
|
|
861
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
862
|
+
body: body.toString()
|
|
863
|
+
});
|
|
864
|
+
if (!response.ok) {
|
|
865
|
+
const errorText = await response.text();
|
|
866
|
+
throw new Error(
|
|
867
|
+
`ChatGPT token refresh failed: ${response.status} - ${errorText}`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const data = await response.json();
|
|
871
|
+
return {
|
|
872
|
+
accessToken: data.access_token,
|
|
873
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
874
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : 0
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function startCallbackServer(port, expectedState, timeoutMs = 5 * 60 * 1e3) {
|
|
878
|
+
return new Promise((resolve, reject) => {
|
|
879
|
+
const server = http.createServer((req, res) => {
|
|
880
|
+
const parsedUrl = new URL2(req.url || "", `http://localhost:${port}`);
|
|
881
|
+
if (parsedUrl.pathname !== "/callback" && parsedUrl.pathname !== "/auth/callback") {
|
|
882
|
+
res.writeHead(404);
|
|
883
|
+
res.end("Not Found");
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const code = parsedUrl.searchParams.get("code");
|
|
887
|
+
const state = parsedUrl.searchParams.get("state");
|
|
888
|
+
const error = parsedUrl.searchParams.get("error");
|
|
889
|
+
if (error) {
|
|
890
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
891
|
+
res.end(
|
|
892
|
+
`<html><body><h1>Authentication Failed</h1><p>${error}</p></body></html>`
|
|
893
|
+
);
|
|
894
|
+
server.close();
|
|
895
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (!code || !state) {
|
|
899
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
900
|
+
res.end("<html><body><h1>Missing Parameters</h1></body></html>");
|
|
901
|
+
server.close();
|
|
902
|
+
reject(new Error("Missing code or state parameter"));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (state !== expectedState) {
|
|
906
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
907
|
+
res.end(
|
|
908
|
+
"<html><body><h1>Invalid State</h1><p>Possible CSRF attack.</p></body></html>"
|
|
909
|
+
);
|
|
910
|
+
server.close();
|
|
911
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
915
|
+
res.end(`
|
|
916
|
+
<!DOCTYPE html>
|
|
917
|
+
<html>
|
|
918
|
+
<head><title>Authentication Successful</title></head>
|
|
919
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
920
|
+
<h1>Authentication Successful!</h1>
|
|
921
|
+
<p>You can close this window and return to your terminal.</p>
|
|
922
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
923
|
+
</body>
|
|
924
|
+
</html>
|
|
925
|
+
`);
|
|
926
|
+
server.close();
|
|
927
|
+
resolve({ code, state });
|
|
928
|
+
});
|
|
929
|
+
server.on("error", (err) => {
|
|
930
|
+
if (err.code === "EADDRINUSE") {
|
|
931
|
+
reject(
|
|
932
|
+
new Error(
|
|
933
|
+
`Port ${port} is already in use. Close other applications using this port.`
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
} else {
|
|
937
|
+
reject(err);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
const timeout = setTimeout(() => {
|
|
941
|
+
server.close();
|
|
942
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
943
|
+
}, timeoutMs);
|
|
944
|
+
server.on("close", () => clearTimeout(timeout));
|
|
945
|
+
server.listen(port, "127.0.0.1");
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
async function requestCopilotDeviceCode() {
|
|
949
|
+
const body = new URLSearchParams({
|
|
950
|
+
client_id: COPILOT_OAUTH_CONFIG.clientId,
|
|
951
|
+
scope: COPILOT_OAUTH_CONFIG.scope
|
|
952
|
+
});
|
|
953
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.deviceCodeUrl, {
|
|
954
|
+
method: "POST",
|
|
955
|
+
headers: {
|
|
956
|
+
Accept: "application/json",
|
|
957
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
958
|
+
},
|
|
959
|
+
body: body.toString()
|
|
960
|
+
});
|
|
961
|
+
if (!response.ok) {
|
|
962
|
+
const errorText = await response.text();
|
|
963
|
+
throw new Error(
|
|
964
|
+
`Failed to request device code: ${response.status} - ${errorText}`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
return await response.json();
|
|
968
|
+
}
|
|
969
|
+
async function pollForCopilotAccessToken(deviceCode, intervalMs, expiresAt) {
|
|
970
|
+
const body = new URLSearchParams({
|
|
971
|
+
client_id: COPILOT_OAUTH_CONFIG.clientId,
|
|
972
|
+
device_code: deviceCode,
|
|
973
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
974
|
+
});
|
|
975
|
+
while (Date.now() < expiresAt) {
|
|
976
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.accessTokenUrl, {
|
|
977
|
+
method: "POST",
|
|
978
|
+
headers: {
|
|
979
|
+
Accept: "application/json",
|
|
980
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
981
|
+
},
|
|
982
|
+
body: body.toString()
|
|
983
|
+
});
|
|
984
|
+
const data = await response.json();
|
|
985
|
+
if (data.access_token) {
|
|
986
|
+
return data.access_token;
|
|
987
|
+
}
|
|
988
|
+
if (data.error === "authorization_pending") {
|
|
989
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (data.error === "slow_down") {
|
|
993
|
+
await new Promise((r) => setTimeout(r, intervalMs + 5e3));
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (data.error === "expired_token") {
|
|
997
|
+
throw new Error("Device code expired. Please try again.");
|
|
998
|
+
}
|
|
999
|
+
if (data.error === "access_denied") {
|
|
1000
|
+
throw new Error("Access denied. User cancelled authorization.");
|
|
1001
|
+
}
|
|
1002
|
+
throw new Error(`GitHub OAuth error: ${data.error || "Unknown error"}`);
|
|
1003
|
+
}
|
|
1004
|
+
throw new Error("Authorization timed out. Please try again.");
|
|
1005
|
+
}
|
|
1006
|
+
async function exchangeGitHubTokenForCopilot(githubToken) {
|
|
1007
|
+
const response = await fetch(COPILOT_OAUTH_CONFIG.copilotTokenUrl, {
|
|
1008
|
+
method: "GET",
|
|
1009
|
+
headers: {
|
|
1010
|
+
Accept: "application/json",
|
|
1011
|
+
Authorization: `Bearer ${githubToken}`
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
if (!response.ok) {
|
|
1015
|
+
const errorText = await response.text();
|
|
1016
|
+
if (response.status === 401) {
|
|
1017
|
+
throw new Error("GitHub token is invalid or expired.");
|
|
1018
|
+
}
|
|
1019
|
+
if (response.status === 403) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
"You do not have access to GitHub Copilot. Please ensure you have an active Copilot subscription."
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`Failed to get Copilot token: ${response.status} - ${errorText}`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return await response.json();
|
|
1029
|
+
}
|
|
1030
|
+
function parseCopilotToken(token) {
|
|
1031
|
+
let expiresAt = Date.now() + 30 * 60 * 1e3;
|
|
1032
|
+
let apiEndpoint = COPILOT_OAUTH_CONFIG.apiEndpoint;
|
|
1033
|
+
const pairs = token.split(";");
|
|
1034
|
+
for (const pair of pairs) {
|
|
1035
|
+
const [key, value] = pair.split("=");
|
|
1036
|
+
if (key?.trim() === "exp" && value) {
|
|
1037
|
+
expiresAt = Number.parseInt(value.trim(), 10) * 1e3;
|
|
1038
|
+
}
|
|
1039
|
+
if (key?.trim() === "proxy-ep" && value) {
|
|
1040
|
+
let proxyUrl = value.trim();
|
|
1041
|
+
if (!proxyUrl.startsWith("http")) {
|
|
1042
|
+
proxyUrl = `https://${proxyUrl}`;
|
|
1043
|
+
}
|
|
1044
|
+
apiEndpoint = proxyUrl.replace(/\/\/proxy\./i, "//api.");
|
|
1045
|
+
if (!apiEndpoint.includes("/chat/completions")) {
|
|
1046
|
+
apiEndpoint = `${apiEndpoint}/chat/completions`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return { expiresAt, apiEndpoint };
|
|
1051
|
+
}
|
|
1052
|
+
function isCopilotTokenExpired(expiresAt) {
|
|
1053
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
1054
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
590
1057
|
// src/providers/base.ts
|
|
591
1058
|
var SYSTEM_PROMPT_GENERATE = `You are a shell command generator for macOS/Linux terminals.
|
|
592
1059
|
Given a natural language description, return ONLY the shell command.
|
|
@@ -602,6 +1069,182 @@ Explain the given shell command in simple terms.
|
|
|
602
1069
|
Break down each part of the command concisely.
|
|
603
1070
|
Format as a simple list without markdown.`;
|
|
604
1071
|
|
|
1072
|
+
// src/providers/chatgpt-subscription.ts
|
|
1073
|
+
var ChatGPTSubscriptionProvider = class {
|
|
1074
|
+
name = "ChatGPT (Subscription)";
|
|
1075
|
+
model;
|
|
1076
|
+
accessToken;
|
|
1077
|
+
refreshToken;
|
|
1078
|
+
expiresAt;
|
|
1079
|
+
accountId;
|
|
1080
|
+
constructor(config) {
|
|
1081
|
+
this.model = config.model;
|
|
1082
|
+
if (config.credentials.type === "chatgpt_subscription") {
|
|
1083
|
+
this.accessToken = config.credentials.accessToken;
|
|
1084
|
+
this.refreshToken = config.credentials.refreshToken;
|
|
1085
|
+
this.expiresAt = config.credentials.expiresAt;
|
|
1086
|
+
this.accountId = config.credentials.accountId;
|
|
1087
|
+
if (!this.accountId) {
|
|
1088
|
+
this.accountId = extractAccountIdFromToken(this.accessToken);
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
throw new Error("ChatGPT Subscription requires subscription credentials");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
isTokenExpired() {
|
|
1095
|
+
if (!this.expiresAt) return false;
|
|
1096
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
1097
|
+
return Date.now() >= this.expiresAt - bufferMs;
|
|
1098
|
+
}
|
|
1099
|
+
async ensureValidToken() {
|
|
1100
|
+
if (this.isTokenExpired() && this.refreshToken) {
|
|
1101
|
+
try {
|
|
1102
|
+
const newTokens = await refreshChatGPTToken(this.refreshToken);
|
|
1103
|
+
this.accessToken = newTokens.accessToken;
|
|
1104
|
+
this.refreshToken = newTokens.refreshToken || this.refreshToken;
|
|
1105
|
+
this.expiresAt = newTokens.expiresAt || this.expiresAt;
|
|
1106
|
+
if (newTokens.accountId) {
|
|
1107
|
+
this.accountId = newTokens.accountId;
|
|
1108
|
+
} else if (!this.accountId) {
|
|
1109
|
+
this.accountId = extractAccountIdFromToken(this.accessToken);
|
|
1110
|
+
}
|
|
1111
|
+
const currentConfig = loadConfig();
|
|
1112
|
+
const providerSettings = currentConfig?.providers["chatgpt-subscription"];
|
|
1113
|
+
if (currentConfig && providerSettings?.credentials.type === "chatgpt_subscription") {
|
|
1114
|
+
providerSettings.credentials.accessToken = newTokens.accessToken;
|
|
1115
|
+
if (newTokens.refreshToken) {
|
|
1116
|
+
providerSettings.credentials.refreshToken = newTokens.refreshToken;
|
|
1117
|
+
}
|
|
1118
|
+
if (newTokens.expiresAt) {
|
|
1119
|
+
providerSettings.credentials.expiresAt = newTokens.expiresAt;
|
|
1120
|
+
}
|
|
1121
|
+
if (this.accountId) {
|
|
1122
|
+
providerSettings.credentials.accountId = this.accountId;
|
|
1123
|
+
}
|
|
1124
|
+
saveConfig(currentConfig);
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
throw new Error(
|
|
1128
|
+
`Token refresh failed. Please re-authenticate with: b --auth
|
|
1129
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return this.accessToken;
|
|
1134
|
+
}
|
|
1135
|
+
async call(systemPrompt, userMessage) {
|
|
1136
|
+
const token = await this.ensureValidToken();
|
|
1137
|
+
const requestBody = {
|
|
1138
|
+
model: this.model,
|
|
1139
|
+
instructions: systemPrompt,
|
|
1140
|
+
input: [
|
|
1141
|
+
{
|
|
1142
|
+
type: "message",
|
|
1143
|
+
role: "user",
|
|
1144
|
+
content: [{ type: "input_text", text: userMessage }]
|
|
1145
|
+
}
|
|
1146
|
+
],
|
|
1147
|
+
store: false,
|
|
1148
|
+
stream: true
|
|
1149
|
+
};
|
|
1150
|
+
const headers = {
|
|
1151
|
+
"Content-Type": "application/json",
|
|
1152
|
+
Authorization: `Bearer ${token}`,
|
|
1153
|
+
"OpenAI-Beta": "responses=experimental",
|
|
1154
|
+
originator: "bashio"
|
|
1155
|
+
};
|
|
1156
|
+
if (this.accountId) {
|
|
1157
|
+
headers["ChatGPT-Account-Id"] = this.accountId;
|
|
1158
|
+
}
|
|
1159
|
+
const response = await fetch(CHATGPT_OAUTH_CONFIG.apiEndpoint, {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
headers,
|
|
1162
|
+
body: JSON.stringify(requestBody)
|
|
1163
|
+
});
|
|
1164
|
+
if (!response.ok) {
|
|
1165
|
+
const errorText = await response.text();
|
|
1166
|
+
if (response.status === 401) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (response.status === 403) {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`Access forbidden. Your ChatGPT subscription may not have access to this model.
|
|
1174
|
+
Error: ${errorText}`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
throw new Error(`ChatGPT API error: ${response.status} - ${errorText}`);
|
|
1178
|
+
}
|
|
1179
|
+
return this.parseStreamingResponse(response);
|
|
1180
|
+
}
|
|
1181
|
+
async parseStreamingResponse(response) {
|
|
1182
|
+
if (!response.body) {
|
|
1183
|
+
throw new Error("No response body from ChatGPT");
|
|
1184
|
+
}
|
|
1185
|
+
const reader = response.body.getReader();
|
|
1186
|
+
const decoder = new TextDecoder("utf-8");
|
|
1187
|
+
let buffer = "";
|
|
1188
|
+
let fullText = "";
|
|
1189
|
+
try {
|
|
1190
|
+
while (true) {
|
|
1191
|
+
const { done, value } = await reader.read();
|
|
1192
|
+
if (done) break;
|
|
1193
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1194
|
+
const lines = buffer.split("\n");
|
|
1195
|
+
buffer = lines.pop() || "";
|
|
1196
|
+
for (const line of lines) {
|
|
1197
|
+
if (!line.trim() || !line.startsWith("data:")) continue;
|
|
1198
|
+
const data = line.substring(5).trim();
|
|
1199
|
+
if (data === "[DONE]") {
|
|
1200
|
+
return fullText.trim();
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
const chunk = JSON.parse(data);
|
|
1204
|
+
if (chunk.type === "response.output_text.delta" && chunk.delta) {
|
|
1205
|
+
fullText += chunk.delta;
|
|
1206
|
+
} else if (chunk.choices?.[0]?.delta?.content) {
|
|
1207
|
+
fullText += chunk.choices[0].delta.content;
|
|
1208
|
+
}
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
} finally {
|
|
1214
|
+
reader.releaseLock();
|
|
1215
|
+
}
|
|
1216
|
+
if (!fullText) {
|
|
1217
|
+
throw new Error("No response content from ChatGPT");
|
|
1218
|
+
}
|
|
1219
|
+
return fullText.trim();
|
|
1220
|
+
}
|
|
1221
|
+
async generateCommand(query, context) {
|
|
1222
|
+
const userMessage = context ? `Context: ${context}
|
|
1223
|
+
|
|
1224
|
+
Task: ${query}` : query;
|
|
1225
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1226
|
+
}
|
|
1227
|
+
async explainCommand(command) {
|
|
1228
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1229
|
+
}
|
|
1230
|
+
async validateCredentials() {
|
|
1231
|
+
try {
|
|
1232
|
+
const token = await this.ensureValidToken();
|
|
1233
|
+
return !!token && token.length > 0;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
var CHATGPT_SUBSCRIPTION_MODELS = [
|
|
1240
|
+
{ value: "gpt-5.2-codex", label: "GPT-5.2-Codex (recommended for coding)" },
|
|
1241
|
+
{ value: "gpt-5.2", label: "GPT-5.2 (most intelligent)" },
|
|
1242
|
+
{ value: "gpt-5.1-codex-max", label: "GPT-5.1-Codex-Max (long tasks)" },
|
|
1243
|
+
{ value: "gpt-5.1-codex-mini", label: "GPT-5.1-Codex-Mini (fast)" },
|
|
1244
|
+
{ value: "o4-mini", label: "o4-mini (reasoning)" },
|
|
1245
|
+
{ value: "gpt-4o", label: "GPT-4o (legacy)" }
|
|
1246
|
+
];
|
|
1247
|
+
|
|
605
1248
|
// src/providers/claude.ts
|
|
606
1249
|
var ClaudeProvider = class {
|
|
607
1250
|
name = "Claude";
|
|
@@ -636,18 +1279,357 @@ var ClaudeProvider = class {
|
|
|
636
1279
|
})
|
|
637
1280
|
});
|
|
638
1281
|
if (!response.ok) {
|
|
639
|
-
const error = await response.text();
|
|
640
|
-
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
1282
|
+
const error = await response.text();
|
|
1283
|
+
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
1284
|
+
}
|
|
1285
|
+
const data = await response.json();
|
|
1286
|
+
if (data.error) {
|
|
1287
|
+
throw new Error(`Claude API error: ${data.error.message}`);
|
|
1288
|
+
}
|
|
1289
|
+
const textContent = data.content.find((c) => c.type === "text");
|
|
1290
|
+
if (!textContent) {
|
|
1291
|
+
throw new Error("No text response from Claude");
|
|
1292
|
+
}
|
|
1293
|
+
return textContent.text.trim();
|
|
1294
|
+
}
|
|
1295
|
+
async generateCommand(query, context) {
|
|
1296
|
+
const userMessage = context ? `Context: ${context}
|
|
1297
|
+
|
|
1298
|
+
Task: ${query}` : query;
|
|
1299
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1300
|
+
}
|
|
1301
|
+
async explainCommand(command) {
|
|
1302
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1303
|
+
}
|
|
1304
|
+
async validateCredentials() {
|
|
1305
|
+
try {
|
|
1306
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1307
|
+
method: "POST",
|
|
1308
|
+
headers: {
|
|
1309
|
+
"Content-Type": "application/json",
|
|
1310
|
+
"x-api-key": this.apiKey,
|
|
1311
|
+
"anthropic-version": "2023-06-01"
|
|
1312
|
+
},
|
|
1313
|
+
body: JSON.stringify({
|
|
1314
|
+
model: this.model,
|
|
1315
|
+
max_tokens: 10,
|
|
1316
|
+
messages: [{ role: "user", content: "hi" }]
|
|
1317
|
+
})
|
|
1318
|
+
});
|
|
1319
|
+
return response.ok;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
var CLAUDE_MODELS = [
|
|
1326
|
+
{
|
|
1327
|
+
value: "claude-opus-4-5-20251101",
|
|
1328
|
+
label: "Claude Opus 4.5 (most intelligent)"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
value: "claude-sonnet-4-5-20250929",
|
|
1332
|
+
label: "Claude Sonnet 4.5 (recommended)"
|
|
1333
|
+
},
|
|
1334
|
+
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (fast)" },
|
|
1335
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
1336
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" }
|
|
1337
|
+
];
|
|
1338
|
+
|
|
1339
|
+
// src/providers/claude-subscription.ts
|
|
1340
|
+
var CLAUDE_CODE_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
1341
|
+
var CLAUDE_CODE_BETAS = [
|
|
1342
|
+
"oauth-2025-04-20",
|
|
1343
|
+
"claude-code-20250219",
|
|
1344
|
+
"interleaved-thinking-2025-05-14",
|
|
1345
|
+
"fine-grained-tool-streaming-2025-05-14"
|
|
1346
|
+
].join(",");
|
|
1347
|
+
var ClaudeSubscriptionProvider = class {
|
|
1348
|
+
name = "Claude (Subscription)";
|
|
1349
|
+
model;
|
|
1350
|
+
accessToken;
|
|
1351
|
+
refreshToken;
|
|
1352
|
+
expiresAt;
|
|
1353
|
+
constructor(config) {
|
|
1354
|
+
this.model = config.model;
|
|
1355
|
+
if (config.credentials.type === "claude_subscription") {
|
|
1356
|
+
this.accessToken = config.credentials.accessToken;
|
|
1357
|
+
this.refreshToken = config.credentials.refreshToken;
|
|
1358
|
+
this.expiresAt = config.credentials.expiresAt;
|
|
1359
|
+
} else {
|
|
1360
|
+
throw new Error("Claude Subscription requires subscription credentials");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async ensureValidToken() {
|
|
1364
|
+
if (isClaudeTokenExpired(this.expiresAt)) {
|
|
1365
|
+
try {
|
|
1366
|
+
const newTokens = await refreshClaudeToken(this.refreshToken);
|
|
1367
|
+
this.accessToken = newTokens.accessToken;
|
|
1368
|
+
this.refreshToken = newTokens.refreshToken;
|
|
1369
|
+
this.expiresAt = newTokens.expiresAt;
|
|
1370
|
+
const currentConfig = loadConfig();
|
|
1371
|
+
const providerSettings = currentConfig?.providers["claude-subscription"];
|
|
1372
|
+
if (currentConfig && providerSettings?.credentials.type === "claude_subscription") {
|
|
1373
|
+
providerSettings.credentials.accessToken = newTokens.accessToken;
|
|
1374
|
+
providerSettings.credentials.refreshToken = newTokens.refreshToken;
|
|
1375
|
+
providerSettings.credentials.expiresAt = newTokens.expiresAt;
|
|
1376
|
+
saveConfig(currentConfig);
|
|
1377
|
+
}
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`Token refresh failed. Please re-authenticate with: b --auth
|
|
1381
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return this.accessToken;
|
|
1386
|
+
}
|
|
1387
|
+
async call(systemPrompt, userMessage) {
|
|
1388
|
+
const token = await this.ensureValidToken();
|
|
1389
|
+
const messages = [
|
|
1390
|
+
{ role: "user", content: userMessage }
|
|
1391
|
+
];
|
|
1392
|
+
const systemBlocks = [
|
|
1393
|
+
{ type: "text", text: CLAUDE_CODE_PREFIX },
|
|
1394
|
+
{ type: "text", text: systemPrompt }
|
|
1395
|
+
];
|
|
1396
|
+
const headers = {
|
|
1397
|
+
Authorization: `Bearer ${token}`,
|
|
1398
|
+
"Content-Type": "application/json",
|
|
1399
|
+
"anthropic-version": "2023-06-01",
|
|
1400
|
+
"anthropic-beta": CLAUDE_CODE_BETAS,
|
|
1401
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
1402
|
+
"user-agent": "claude-cli/1.0.119 (external, cli)",
|
|
1403
|
+
"x-app": "cli",
|
|
1404
|
+
accept: "application/json"
|
|
1405
|
+
};
|
|
1406
|
+
const response = await fetch(
|
|
1407
|
+
"https://api.anthropic.com/v1/messages?beta=true",
|
|
1408
|
+
{
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
headers,
|
|
1411
|
+
body: JSON.stringify({
|
|
1412
|
+
model: this.model,
|
|
1413
|
+
max_tokens: 1024,
|
|
1414
|
+
system: systemBlocks,
|
|
1415
|
+
messages
|
|
1416
|
+
})
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
1419
|
+
if (!response.ok) {
|
|
1420
|
+
const error = await response.text();
|
|
1421
|
+
if (response.status === 401) {
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
1427
|
+
}
|
|
1428
|
+
const data = await response.json();
|
|
1429
|
+
if (data.error) {
|
|
1430
|
+
throw new Error(`Claude API error: ${data.error.message}`);
|
|
1431
|
+
}
|
|
1432
|
+
const textContent = data.content.find((c) => c.type === "text");
|
|
1433
|
+
if (!textContent) {
|
|
1434
|
+
throw new Error("No text response from Claude");
|
|
1435
|
+
}
|
|
1436
|
+
return textContent.text.trim();
|
|
1437
|
+
}
|
|
1438
|
+
async generateCommand(query, context) {
|
|
1439
|
+
const userMessage = context ? `Context: ${context}
|
|
1440
|
+
|
|
1441
|
+
Task: ${query}` : query;
|
|
1442
|
+
return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
|
|
1443
|
+
}
|
|
1444
|
+
async explainCommand(command) {
|
|
1445
|
+
return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
|
|
1446
|
+
}
|
|
1447
|
+
async validateCredentials() {
|
|
1448
|
+
try {
|
|
1449
|
+
const token = await this.ensureValidToken();
|
|
1450
|
+
const headers = {
|
|
1451
|
+
Authorization: `Bearer ${token}`,
|
|
1452
|
+
"Content-Type": "application/json",
|
|
1453
|
+
"anthropic-version": "2023-06-01",
|
|
1454
|
+
"anthropic-beta": CLAUDE_CODE_BETAS,
|
|
1455
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
1456
|
+
"user-agent": "claude-cli/1.0.119 (external, cli)",
|
|
1457
|
+
"x-app": "cli",
|
|
1458
|
+
accept: "application/json"
|
|
1459
|
+
};
|
|
1460
|
+
const response = await fetch(
|
|
1461
|
+
"https://api.anthropic.com/v1/messages?beta=true",
|
|
1462
|
+
{
|
|
1463
|
+
method: "POST",
|
|
1464
|
+
headers,
|
|
1465
|
+
body: JSON.stringify({
|
|
1466
|
+
model: this.model,
|
|
1467
|
+
max_tokens: 10,
|
|
1468
|
+
system: [{ type: "text", text: CLAUDE_CODE_PREFIX }],
|
|
1469
|
+
messages: [{ role: "user", content: "hi" }]
|
|
1470
|
+
})
|
|
1471
|
+
}
|
|
1472
|
+
);
|
|
1473
|
+
return response.ok;
|
|
1474
|
+
} catch {
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
var CLAUDE_SUBSCRIPTION_MODELS = [
|
|
1480
|
+
{
|
|
1481
|
+
value: "claude-opus-4-5-20251101",
|
|
1482
|
+
label: "Claude Opus 4.5 (most intelligent)"
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
value: "claude-sonnet-4-5-20250929",
|
|
1486
|
+
label: "Claude Sonnet 4.5 (recommended)"
|
|
1487
|
+
},
|
|
1488
|
+
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (fast)" },
|
|
1489
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
1490
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4 (Max plan only)" }
|
|
1491
|
+
];
|
|
1492
|
+
|
|
1493
|
+
// src/providers/copilot.ts
|
|
1494
|
+
var CopilotProvider = class {
|
|
1495
|
+
name = "GitHub Copilot";
|
|
1496
|
+
model;
|
|
1497
|
+
githubToken;
|
|
1498
|
+
copilotToken;
|
|
1499
|
+
copilotTokenExpiresAt;
|
|
1500
|
+
apiEndpoint;
|
|
1501
|
+
constructor(config) {
|
|
1502
|
+
this.model = config.model;
|
|
1503
|
+
if (config.credentials.type === "copilot") {
|
|
1504
|
+
this.githubToken = config.credentials.githubToken;
|
|
1505
|
+
this.copilotToken = config.credentials.copilotToken;
|
|
1506
|
+
this.copilotTokenExpiresAt = config.credentials.copilotTokenExpiresAt;
|
|
1507
|
+
this.apiEndpoint = this.normalizeEndpoint(
|
|
1508
|
+
config.credentials.apiEndpoint || COPILOT_OAUTH_CONFIG.apiEndpoint
|
|
1509
|
+
);
|
|
1510
|
+
} else {
|
|
1511
|
+
throw new Error("Copilot requires copilot credentials");
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
normalizeEndpoint(endpoint) {
|
|
1515
|
+
let url = endpoint;
|
|
1516
|
+
if (!url.startsWith("http")) {
|
|
1517
|
+
url = `https://${url}`;
|
|
1518
|
+
}
|
|
1519
|
+
url = url.replace(/\/\/proxy\./i, "//api.");
|
|
1520
|
+
if (!url.includes("/chat/completions")) {
|
|
1521
|
+
url = `${url}/chat/completions`;
|
|
1522
|
+
}
|
|
1523
|
+
return url;
|
|
1524
|
+
}
|
|
1525
|
+
async ensureValidToken() {
|
|
1526
|
+
if (isCopilotTokenExpired(this.copilotTokenExpiresAt)) {
|
|
1527
|
+
try {
|
|
1528
|
+
const newTokenData = await exchangeGitHubTokenForCopilot(
|
|
1529
|
+
this.githubToken
|
|
1530
|
+
);
|
|
1531
|
+
this.copilotToken = newTokenData.token;
|
|
1532
|
+
const parsed = parseCopilotToken(newTokenData.token);
|
|
1533
|
+
this.copilotTokenExpiresAt = parsed.expiresAt;
|
|
1534
|
+
this.apiEndpoint = parsed.apiEndpoint;
|
|
1535
|
+
const currentConfig = loadConfig();
|
|
1536
|
+
const providerSettings = currentConfig?.providers.copilot;
|
|
1537
|
+
if (currentConfig && providerSettings?.credentials.type === "copilot") {
|
|
1538
|
+
providerSettings.credentials.copilotToken = newTokenData.token;
|
|
1539
|
+
providerSettings.credentials.copilotTokenExpiresAt = parsed.expiresAt;
|
|
1540
|
+
providerSettings.credentials.apiEndpoint = parsed.apiEndpoint;
|
|
1541
|
+
saveConfig(currentConfig);
|
|
1542
|
+
}
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
throw new Error(
|
|
1545
|
+
`Copilot token refresh failed. Please re-authenticate with: b --auth
|
|
1546
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return this.copilotToken;
|
|
1551
|
+
}
|
|
1552
|
+
async call(systemPrompt, userMessage) {
|
|
1553
|
+
const token = await this.ensureValidToken();
|
|
1554
|
+
const requestBody = {
|
|
1555
|
+
model: this.model,
|
|
1556
|
+
messages: [
|
|
1557
|
+
{ role: "system", content: systemPrompt },
|
|
1558
|
+
{ role: "user", content: userMessage }
|
|
1559
|
+
],
|
|
1560
|
+
temperature: 0.1,
|
|
1561
|
+
top_p: 1,
|
|
1562
|
+
n: 1,
|
|
1563
|
+
stream: true
|
|
1564
|
+
};
|
|
1565
|
+
const headers = {
|
|
1566
|
+
"Content-Type": "application/json",
|
|
1567
|
+
Accept: "application/json",
|
|
1568
|
+
Authorization: `Bearer ${token}`,
|
|
1569
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
1570
|
+
"Editor-Version": "vscode/1.107.0",
|
|
1571
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
1572
|
+
"Copilot-Integration-Id": "vscode-chat"
|
|
1573
|
+
};
|
|
1574
|
+
const response = await fetch(this.apiEndpoint, {
|
|
1575
|
+
method: "POST",
|
|
1576
|
+
headers,
|
|
1577
|
+
body: JSON.stringify(requestBody)
|
|
1578
|
+
});
|
|
1579
|
+
if (!response.ok) {
|
|
1580
|
+
const errorText = await response.text();
|
|
1581
|
+
if (response.status === 401) {
|
|
1582
|
+
throw new Error(
|
|
1583
|
+
"Authentication failed. Please re-authenticate with: b --auth"
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
if (response.status === 403) {
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
"Access forbidden. Please ensure you have an active GitHub Copilot subscription."
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
throw new Error(`Copilot API error: ${response.status} - ${errorText}`);
|
|
641
1592
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
1593
|
+
return this.parseStreamingResponse(response);
|
|
1594
|
+
}
|
|
1595
|
+
async parseStreamingResponse(response) {
|
|
1596
|
+
if (!response.body) {
|
|
1597
|
+
throw new Error("No response body from Copilot");
|
|
645
1598
|
}
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
1599
|
+
const reader = response.body.getReader();
|
|
1600
|
+
const decoder = new TextDecoder("utf-8");
|
|
1601
|
+
let buffer = "";
|
|
1602
|
+
let fullText = "";
|
|
1603
|
+
try {
|
|
1604
|
+
while (true) {
|
|
1605
|
+
const { done, value } = await reader.read();
|
|
1606
|
+
if (done) break;
|
|
1607
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1608
|
+
const lines = buffer.split("\n");
|
|
1609
|
+
buffer = lines.pop() || "";
|
|
1610
|
+
for (const line of lines) {
|
|
1611
|
+
if (!line.trim() || !line.startsWith("data:")) continue;
|
|
1612
|
+
const data = line.substring(5).trim();
|
|
1613
|
+
if (data === "[DONE]") {
|
|
1614
|
+
return fullText.trim();
|
|
1615
|
+
}
|
|
1616
|
+
try {
|
|
1617
|
+
const chunk = JSON.parse(data);
|
|
1618
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1619
|
+
if (content) {
|
|
1620
|
+
fullText += content;
|
|
1621
|
+
}
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
} finally {
|
|
1627
|
+
reader.releaseLock();
|
|
649
1628
|
}
|
|
650
|
-
|
|
1629
|
+
if (!fullText) {
|
|
1630
|
+
throw new Error("No response content from Copilot");
|
|
1631
|
+
}
|
|
1632
|
+
return fullText.trim();
|
|
651
1633
|
}
|
|
652
1634
|
async generateCommand(query, context) {
|
|
653
1635
|
const userMessage = context ? `Context: ${context}
|
|
@@ -660,29 +1642,20 @@ Task: ${query}` : query;
|
|
|
660
1642
|
}
|
|
661
1643
|
async validateCredentials() {
|
|
662
1644
|
try {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
headers: {
|
|
666
|
-
"Content-Type": "application/json",
|
|
667
|
-
"x-api-key": this.apiKey,
|
|
668
|
-
"anthropic-version": "2023-06-01"
|
|
669
|
-
},
|
|
670
|
-
body: JSON.stringify({
|
|
671
|
-
model: this.model,
|
|
672
|
-
max_tokens: 10,
|
|
673
|
-
messages: [{ role: "user", content: "hi" }]
|
|
674
|
-
})
|
|
675
|
-
});
|
|
676
|
-
return response.ok;
|
|
1645
|
+
const token = await this.ensureValidToken();
|
|
1646
|
+
return !!token && token.length > 0;
|
|
677
1647
|
} catch {
|
|
678
1648
|
return false;
|
|
679
1649
|
}
|
|
680
1650
|
}
|
|
681
1651
|
};
|
|
682
|
-
var
|
|
683
|
-
{ value: "
|
|
684
|
-
{ value: "
|
|
685
|
-
{ value: "
|
|
1652
|
+
var COPILOT_MODELS = [
|
|
1653
|
+
{ value: "gpt-4.1", label: "GPT-4.1 (default)" },
|
|
1654
|
+
{ value: "gpt-5.1", label: "GPT-5.1 (latest)" },
|
|
1655
|
+
{ value: "gpt-5-mini", label: "GPT-5 Mini (fast)" },
|
|
1656
|
+
{ value: "claude-sonnet-4", label: "Claude Sonnet 4" },
|
|
1657
|
+
{ value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5 (latest)" },
|
|
1658
|
+
{ value: "gpt-4o", label: "GPT-4o" }
|
|
686
1659
|
];
|
|
687
1660
|
|
|
688
1661
|
// src/providers/ollama.ts
|
|
@@ -823,9 +1796,12 @@ Task: ${query}` : query;
|
|
|
823
1796
|
}
|
|
824
1797
|
};
|
|
825
1798
|
var OPENAI_MODELS = [
|
|
826
|
-
{ value: "gpt-
|
|
827
|
-
{ value: "gpt-
|
|
828
|
-
{ value: "gpt-
|
|
1799
|
+
{ value: "gpt-5.2", label: "GPT-5.2 (most intelligent)" },
|
|
1800
|
+
{ value: "gpt-5.1", label: "GPT-5.1 (recommended)" },
|
|
1801
|
+
{ value: "gpt-5-mini", label: "GPT-5 Mini (fast)" },
|
|
1802
|
+
{ value: "gpt-5-nano", label: "GPT-5 Nano (fastest)" },
|
|
1803
|
+
{ value: "gpt-4.1", label: "GPT-4.1" },
|
|
1804
|
+
{ value: "gpt-4o", label: "GPT-4o (legacy)" }
|
|
829
1805
|
];
|
|
830
1806
|
|
|
831
1807
|
// src/providers/openrouter.ts
|
|
@@ -909,21 +1885,35 @@ var OPENROUTER_MODELS = [
|
|
|
909
1885
|
|
|
910
1886
|
// src/providers/index.ts
|
|
911
1887
|
function createProvider(config) {
|
|
1888
|
+
const activeProvider = config.activeProvider;
|
|
1889
|
+
const settings = config.providers[activeProvider];
|
|
1890
|
+
if (!settings) {
|
|
1891
|
+
throw new Error(`Provider ${activeProvider} not configured`);
|
|
1892
|
+
}
|
|
912
1893
|
const providerConfig = {
|
|
913
|
-
model:
|
|
914
|
-
credentials:
|
|
1894
|
+
model: settings.model,
|
|
1895
|
+
credentials: settings.credentials
|
|
915
1896
|
};
|
|
916
|
-
|
|
1897
|
+
return createProviderFromType(activeProvider, providerConfig);
|
|
1898
|
+
}
|
|
1899
|
+
function createProviderFromType(provider, config) {
|
|
1900
|
+
switch (provider) {
|
|
917
1901
|
case "claude":
|
|
918
|
-
return new ClaudeProvider(
|
|
1902
|
+
return new ClaudeProvider(config);
|
|
1903
|
+
case "claude-subscription":
|
|
1904
|
+
return new ClaudeSubscriptionProvider(config);
|
|
919
1905
|
case "openai":
|
|
920
|
-
return new OpenAIProvider(
|
|
1906
|
+
return new OpenAIProvider(config);
|
|
1907
|
+
case "chatgpt-subscription":
|
|
1908
|
+
return new ChatGPTSubscriptionProvider(config);
|
|
1909
|
+
case "copilot":
|
|
1910
|
+
return new CopilotProvider(config);
|
|
921
1911
|
case "ollama":
|
|
922
|
-
return new OllamaProvider(
|
|
1912
|
+
return new OllamaProvider(config);
|
|
923
1913
|
case "openrouter":
|
|
924
|
-
return new OpenRouterProvider(
|
|
1914
|
+
return new OpenRouterProvider(config);
|
|
925
1915
|
default:
|
|
926
|
-
throw new Error(`Unknown provider: ${
|
|
1916
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
927
1917
|
}
|
|
928
1918
|
}
|
|
929
1919
|
|
|
@@ -937,6 +1927,28 @@ function createSpinner(text) {
|
|
|
937
1927
|
}
|
|
938
1928
|
|
|
939
1929
|
// src/core/auth.ts
|
|
1930
|
+
var PROVIDER_DISPLAY_NAMES = {
|
|
1931
|
+
"claude-subscription": "Claude Pro/Max",
|
|
1932
|
+
"chatgpt-subscription": "ChatGPT Plus/Pro",
|
|
1933
|
+
copilot: "GitHub Copilot",
|
|
1934
|
+
claude: "Claude (API Key)",
|
|
1935
|
+
openai: "ChatGPT (API Key)",
|
|
1936
|
+
ollama: "Ollama (Local)",
|
|
1937
|
+
openrouter: "OpenRouter"
|
|
1938
|
+
};
|
|
1939
|
+
async function openBrowser(url) {
|
|
1940
|
+
try {
|
|
1941
|
+
const { exec: exec2 } = await import("child_process");
|
|
1942
|
+
const { promisify: promisify2 } = await import("util");
|
|
1943
|
+
const execAsync2 = promisify2(exec2);
|
|
1944
|
+
const platform = process.platform;
|
|
1945
|
+
const command = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
1946
|
+
await execAsync2(command);
|
|
1947
|
+
return true;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
940
1952
|
function showWelcomeBanner() {
|
|
941
1953
|
const cyan = pc3.cyan;
|
|
942
1954
|
const dim = pc3.dim;
|
|
@@ -997,17 +2009,33 @@ async function runAuthSetup(showBanner = true) {
|
|
|
997
2009
|
showWelcomeBanner();
|
|
998
2010
|
}
|
|
999
2011
|
console.log(pc3.bold(" Bashio Setup\n"));
|
|
2012
|
+
const existingConfig = loadConfig();
|
|
1000
2013
|
const provider = await select({
|
|
1001
2014
|
message: "Select your AI provider:",
|
|
1002
2015
|
choices: [
|
|
2016
|
+
{
|
|
2017
|
+
value: "claude-subscription",
|
|
2018
|
+
name: "Claude Pro/Max (Subscription)",
|
|
2019
|
+
description: "Use your existing Claude subscription"
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
value: "chatgpt-subscription",
|
|
2023
|
+
name: "ChatGPT Plus/Pro (Subscription)",
|
|
2024
|
+
description: "Use your existing ChatGPT subscription"
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
value: "copilot",
|
|
2028
|
+
name: "GitHub Copilot",
|
|
2029
|
+
description: "Use your GitHub Copilot subscription"
|
|
2030
|
+
},
|
|
1003
2031
|
{
|
|
1004
2032
|
value: "claude",
|
|
1005
|
-
name: "Claude (
|
|
2033
|
+
name: "Claude (API Key)",
|
|
1006
2034
|
description: "Use Anthropic API key"
|
|
1007
2035
|
},
|
|
1008
2036
|
{
|
|
1009
2037
|
value: "openai",
|
|
1010
|
-
name: "ChatGPT (
|
|
2038
|
+
name: "ChatGPT (API Key)",
|
|
1011
2039
|
description: "Use OpenAI API key"
|
|
1012
2040
|
},
|
|
1013
2041
|
{
|
|
@@ -1022,9 +2050,128 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1022
2050
|
}
|
|
1023
2051
|
]
|
|
1024
2052
|
});
|
|
2053
|
+
if (existingConfig && isProviderConfigured(existingConfig, provider)) {
|
|
2054
|
+
const currentModel = existingConfig.providers[provider]?.model;
|
|
2055
|
+
console.log();
|
|
2056
|
+
console.log(
|
|
2057
|
+
pc3.yellow(` ${PROVIDER_DISPLAY_NAMES[provider]} is already configured.`)
|
|
2058
|
+
);
|
|
2059
|
+
console.log(pc3.dim(` Current model: ${currentModel}`));
|
|
2060
|
+
console.log();
|
|
2061
|
+
const action = await select({
|
|
2062
|
+
message: "What would you like to do?",
|
|
2063
|
+
choices: [
|
|
2064
|
+
{ value: "reauth", name: "Update credentials (re-authenticate)" },
|
|
2065
|
+
{ value: "cancel", name: "Cancel" }
|
|
2066
|
+
]
|
|
2067
|
+
});
|
|
2068
|
+
if (action === "cancel") {
|
|
2069
|
+
console.log(pc3.dim("\n Cancelled.\n"));
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
1025
2073
|
let credentials;
|
|
1026
2074
|
let model;
|
|
1027
2075
|
switch (provider) {
|
|
2076
|
+
case "claude-subscription": {
|
|
2077
|
+
console.log();
|
|
2078
|
+
console.log(
|
|
2079
|
+
pc3.yellow(
|
|
2080
|
+
" Note: This uses your Claude Pro/Max subscription via OAuth."
|
|
2081
|
+
)
|
|
2082
|
+
);
|
|
2083
|
+
console.log(
|
|
2084
|
+
pc3.dim(
|
|
2085
|
+
" Your credentials are stored locally and refreshed automatically."
|
|
2086
|
+
)
|
|
2087
|
+
);
|
|
2088
|
+
console.log();
|
|
2089
|
+
const tokens = await performClaudeOAuth();
|
|
2090
|
+
if (!tokens) {
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
credentials = {
|
|
2094
|
+
type: "claude_subscription",
|
|
2095
|
+
accessToken: tokens.accessToken,
|
|
2096
|
+
refreshToken: tokens.refreshToken,
|
|
2097
|
+
expiresAt: tokens.expiresAt,
|
|
2098
|
+
email: tokens.email
|
|
2099
|
+
};
|
|
2100
|
+
model = await select({
|
|
2101
|
+
message: "Select model:",
|
|
2102
|
+
choices: CLAUDE_SUBSCRIPTION_MODELS.map((m) => ({
|
|
2103
|
+
value: m.value,
|
|
2104
|
+
name: m.label
|
|
2105
|
+
}))
|
|
2106
|
+
});
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
case "chatgpt-subscription": {
|
|
2110
|
+
console.log();
|
|
2111
|
+
console.log(
|
|
2112
|
+
pc3.yellow(
|
|
2113
|
+
" Note: This uses your ChatGPT Plus/Pro subscription via OAuth."
|
|
2114
|
+
)
|
|
2115
|
+
);
|
|
2116
|
+
console.log(
|
|
2117
|
+
pc3.red(" WARNING: This is EXPERIMENTAL and uses an unofficial API.")
|
|
2118
|
+
);
|
|
2119
|
+
console.log(
|
|
2120
|
+
pc3.dim(" The API may change or stop working without notice.")
|
|
2121
|
+
);
|
|
2122
|
+
console.log(
|
|
2123
|
+
pc3.dim(" For stable usage, consider using an API key instead.")
|
|
2124
|
+
);
|
|
2125
|
+
console.log();
|
|
2126
|
+
const tokens = await performChatGPTOAuth();
|
|
2127
|
+
if (!tokens) {
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
credentials = {
|
|
2131
|
+
type: "chatgpt_subscription",
|
|
2132
|
+
accessToken: tokens.accessToken,
|
|
2133
|
+
refreshToken: tokens.refreshToken || void 0,
|
|
2134
|
+
expiresAt: tokens.expiresAt || void 0,
|
|
2135
|
+
accountId: tokens.accountId
|
|
2136
|
+
};
|
|
2137
|
+
model = await select({
|
|
2138
|
+
message: "Select model:",
|
|
2139
|
+
choices: CHATGPT_SUBSCRIPTION_MODELS.map((m) => ({
|
|
2140
|
+
value: m.value,
|
|
2141
|
+
name: m.label
|
|
2142
|
+
}))
|
|
2143
|
+
});
|
|
2144
|
+
break;
|
|
2145
|
+
}
|
|
2146
|
+
case "copilot": {
|
|
2147
|
+
console.log();
|
|
2148
|
+
console.log(
|
|
2149
|
+
pc3.yellow(" Note: This uses your GitHub Copilot subscription.")
|
|
2150
|
+
);
|
|
2151
|
+
console.log(
|
|
2152
|
+
pc3.dim(" You need an active GitHub Copilot subscription to use this.")
|
|
2153
|
+
);
|
|
2154
|
+
console.log();
|
|
2155
|
+
const copilotResult = await performCopilotDeviceFlow();
|
|
2156
|
+
if (!copilotResult) {
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
credentials = {
|
|
2160
|
+
type: "copilot",
|
|
2161
|
+
githubToken: copilotResult.githubToken,
|
|
2162
|
+
copilotToken: copilotResult.copilotToken,
|
|
2163
|
+
copilotTokenExpiresAt: copilotResult.copilotTokenExpiresAt,
|
|
2164
|
+
apiEndpoint: copilotResult.apiEndpoint
|
|
2165
|
+
};
|
|
2166
|
+
model = await select({
|
|
2167
|
+
message: "Select model:",
|
|
2168
|
+
choices: COPILOT_MODELS.map((m) => ({
|
|
2169
|
+
value: m.value,
|
|
2170
|
+
name: m.label
|
|
2171
|
+
}))
|
|
2172
|
+
});
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
1028
2175
|
case "claude": {
|
|
1029
2176
|
const apiKey = await password({
|
|
1030
2177
|
message: "Enter your Anthropic API key:",
|
|
@@ -1098,12 +2245,12 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1098
2245
|
default:
|
|
1099
2246
|
throw new Error(`Unknown provider: ${provider}`);
|
|
1100
2247
|
}
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
settings: {
|
|
2248
|
+
const providerSettings = { model, credentials };
|
|
2249
|
+
const tempConfig = {
|
|
2250
|
+
version: 2,
|
|
2251
|
+
activeProvider: provider,
|
|
2252
|
+
providers: { [provider]: providerSettings },
|
|
2253
|
+
settings: existingConfig?.settings || {
|
|
1107
2254
|
confirmBeforeExecute: true,
|
|
1108
2255
|
historyEnabled: true,
|
|
1109
2256
|
historyRetentionDays: 30,
|
|
@@ -1113,7 +2260,7 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1113
2260
|
};
|
|
1114
2261
|
const spinner = createSpinner("Validating credentials...").start();
|
|
1115
2262
|
try {
|
|
1116
|
-
const providerInstance = createProvider(
|
|
2263
|
+
const providerInstance = createProvider(tempConfig);
|
|
1117
2264
|
const valid = await providerInstance.validateCredentials();
|
|
1118
2265
|
if (!valid) {
|
|
1119
2266
|
spinner.fail("Invalid credentials");
|
|
@@ -1126,14 +2273,223 @@ async function runAuthSetup(showBanner = true) {
|
|
|
1126
2273
|
);
|
|
1127
2274
|
return false;
|
|
1128
2275
|
}
|
|
1129
|
-
|
|
2276
|
+
const finalConfig = existingConfig ? setProviderConfig(existingConfig, provider, providerSettings, true) : tempConfig;
|
|
2277
|
+
saveConfig(finalConfig);
|
|
1130
2278
|
console.log();
|
|
1131
2279
|
logger.success("Configuration saved!");
|
|
1132
|
-
console.log(pc3.gray(` Provider: ${provider}`));
|
|
2280
|
+
console.log(pc3.gray(` Provider: ${PROVIDER_DISPLAY_NAMES[provider]}`));
|
|
1133
2281
|
console.log(pc3.gray(` Model: ${model}`));
|
|
1134
2282
|
console.log();
|
|
1135
2283
|
return true;
|
|
1136
2284
|
}
|
|
2285
|
+
async function performClaudeOAuth() {
|
|
2286
|
+
const authMethod = await select({
|
|
2287
|
+
message: "How would you like to authenticate?",
|
|
2288
|
+
choices: [
|
|
2289
|
+
{
|
|
2290
|
+
value: "browser",
|
|
2291
|
+
name: "Open browser automatically",
|
|
2292
|
+
description: "Recommended - opens browser and waits for callback"
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
value: "manual",
|
|
2296
|
+
name: "Manual URL copy/paste",
|
|
2297
|
+
description: "Copy URL to browser, then paste the callback URL"
|
|
2298
|
+
}
|
|
2299
|
+
]
|
|
2300
|
+
});
|
|
2301
|
+
const pkce = generatePKCE();
|
|
2302
|
+
const authUrl = buildClaudeAuthUrl(pkce);
|
|
2303
|
+
if (authMethod === "browser") {
|
|
2304
|
+
return performBrowserOAuth(
|
|
2305
|
+
"Claude",
|
|
2306
|
+
authUrl,
|
|
2307
|
+
pkce,
|
|
2308
|
+
CLAUDE_OAUTH_CONFIG.callbackPort,
|
|
2309
|
+
async (code) => exchangeClaudeCode(code, pkce.codeVerifier, pkce.state)
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
return performManualOAuth(
|
|
2313
|
+
"Claude",
|
|
2314
|
+
authUrl,
|
|
2315
|
+
pkce,
|
|
2316
|
+
async (code) => exchangeClaudeCode(code, pkce.codeVerifier, pkce.state)
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
async function performChatGPTOAuth() {
|
|
2320
|
+
const authMethod = await select({
|
|
2321
|
+
message: "How would you like to authenticate?",
|
|
2322
|
+
choices: [
|
|
2323
|
+
{
|
|
2324
|
+
value: "browser",
|
|
2325
|
+
name: "Open browser automatically",
|
|
2326
|
+
description: "Recommended - opens browser and waits for callback"
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
value: "manual",
|
|
2330
|
+
name: "Manual URL copy/paste",
|
|
2331
|
+
description: "Copy URL to browser, then paste the callback URL"
|
|
2332
|
+
}
|
|
2333
|
+
]
|
|
2334
|
+
});
|
|
2335
|
+
const pkce = generatePKCE();
|
|
2336
|
+
const authUrl = buildChatGPTAuthUrl(pkce);
|
|
2337
|
+
if (authMethod === "browser") {
|
|
2338
|
+
return performBrowserOAuth(
|
|
2339
|
+
"ChatGPT",
|
|
2340
|
+
authUrl,
|
|
2341
|
+
pkce,
|
|
2342
|
+
CHATGPT_OAUTH_CONFIG.callbackPort,
|
|
2343
|
+
async (code) => exchangeChatGPTCode(code, pkce.codeVerifier)
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
return performManualOAuth(
|
|
2347
|
+
"ChatGPT",
|
|
2348
|
+
authUrl,
|
|
2349
|
+
pkce,
|
|
2350
|
+
async (code) => exchangeChatGPTCode(code, pkce.codeVerifier)
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
async function performBrowserOAuth(providerName, authUrl, pkce, port, exchangeCode) {
|
|
2354
|
+
console.log();
|
|
2355
|
+
console.log(pc3.dim(" Starting local callback server..."));
|
|
2356
|
+
const serverPromise = startCallbackServer(port, pkce.state);
|
|
2357
|
+
const browserOpened = await openBrowser(authUrl);
|
|
2358
|
+
if (browserOpened) {
|
|
2359
|
+
console.log(
|
|
2360
|
+
pc3.green(` Browser opened. Please log in to ${providerName}.`)
|
|
2361
|
+
);
|
|
2362
|
+
} else {
|
|
2363
|
+
console.log(pc3.yellow(" Could not open browser automatically."));
|
|
2364
|
+
console.log(pc3.dim(" Please open this URL manually:"));
|
|
2365
|
+
console.log();
|
|
2366
|
+
console.log(` ${pc3.cyan(authUrl)}`);
|
|
2367
|
+
}
|
|
2368
|
+
console.log();
|
|
2369
|
+
console.log(pc3.dim(" Waiting for authentication (5 minute timeout)..."));
|
|
2370
|
+
try {
|
|
2371
|
+
const { code } = await serverPromise;
|
|
2372
|
+
const spinner = createSpinner("Exchanging authorization code...").start();
|
|
2373
|
+
try {
|
|
2374
|
+
const tokens = await exchangeCode(code);
|
|
2375
|
+
spinner.succeed("Authentication successful!");
|
|
2376
|
+
return tokens;
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
spinner.fail(
|
|
2379
|
+
`Token exchange failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2380
|
+
);
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
logger.error(
|
|
2385
|
+
`Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2386
|
+
);
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
async function performManualOAuth(providerName, authUrl, pkce, exchangeCode) {
|
|
2391
|
+
console.log();
|
|
2392
|
+
console.log(
|
|
2393
|
+
pc3.dim(
|
|
2394
|
+
` Open this URL in your browser to authenticate with ${providerName}:`
|
|
2395
|
+
)
|
|
2396
|
+
);
|
|
2397
|
+
console.log();
|
|
2398
|
+
console.log(` ${pc3.cyan(authUrl)}`);
|
|
2399
|
+
console.log();
|
|
2400
|
+
console.log(
|
|
2401
|
+
pc3.dim(" After logging in, you will be redirected to a localhost URL.")
|
|
2402
|
+
);
|
|
2403
|
+
console.log(pc3.dim(" Copy the full redirect URL and paste it below."));
|
|
2404
|
+
console.log();
|
|
2405
|
+
const callbackUrl = await input3({
|
|
2406
|
+
message: "Paste the callback URL here:"
|
|
2407
|
+
});
|
|
2408
|
+
try {
|
|
2409
|
+
const url = new URL(callbackUrl);
|
|
2410
|
+
const code = url.searchParams.get("code");
|
|
2411
|
+
const state = url.searchParams.get("state");
|
|
2412
|
+
const error = url.searchParams.get("error");
|
|
2413
|
+
if (error) {
|
|
2414
|
+
logger.error(`OAuth error: ${error}`);
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
if (!code) {
|
|
2418
|
+
logger.error("No authorization code found in URL");
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
if (state !== pkce.state) {
|
|
2422
|
+
logger.error("State mismatch - possible security issue");
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
const spinner = createSpinner("Exchanging authorization code...").start();
|
|
2426
|
+
try {
|
|
2427
|
+
const tokens = await exchangeCode(code);
|
|
2428
|
+
spinner.succeed("Authentication successful!");
|
|
2429
|
+
return tokens;
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
spinner.fail(
|
|
2432
|
+
`Token exchange failed: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
2433
|
+
);
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
} catch {
|
|
2437
|
+
logger.error("Invalid URL format");
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async function performCopilotDeviceFlow() {
|
|
2442
|
+
try {
|
|
2443
|
+
const spinner = createSpinner("Requesting device code...").start();
|
|
2444
|
+
const deviceCode = await requestCopilotDeviceCode();
|
|
2445
|
+
spinner.stop();
|
|
2446
|
+
console.log();
|
|
2447
|
+
console.log(pc3.bold(" To authenticate with GitHub Copilot:"));
|
|
2448
|
+
console.log();
|
|
2449
|
+
console.log(
|
|
2450
|
+
` 1. Visit: ${pc3.cyan(pc3.underline(deviceCode.verification_uri))}`
|
|
2451
|
+
);
|
|
2452
|
+
console.log(` 2. Enter code: ${pc3.bold(pc3.green(deviceCode.user_code))}`);
|
|
2453
|
+
console.log();
|
|
2454
|
+
const browserOpened = await openBrowser(deviceCode.verification_uri);
|
|
2455
|
+
if (browserOpened) {
|
|
2456
|
+
console.log(pc3.dim(" Browser opened automatically."));
|
|
2457
|
+
}
|
|
2458
|
+
console.log(pc3.dim(" Waiting for authorization..."));
|
|
2459
|
+
console.log();
|
|
2460
|
+
const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
|
|
2461
|
+
const intervalMs = (deviceCode.interval || 5) * 1e3;
|
|
2462
|
+
const githubToken = await pollForCopilotAccessToken(
|
|
2463
|
+
deviceCode.device_code,
|
|
2464
|
+
intervalMs,
|
|
2465
|
+
expiresAt
|
|
2466
|
+
);
|
|
2467
|
+
const exchangeSpinner = createSpinner(
|
|
2468
|
+
"Getting Copilot access token..."
|
|
2469
|
+
).start();
|
|
2470
|
+
try {
|
|
2471
|
+
const copilotTokenData = await exchangeGitHubTokenForCopilot(githubToken);
|
|
2472
|
+
const parsed = parseCopilotToken(copilotTokenData.token);
|
|
2473
|
+
exchangeSpinner.succeed("Authentication successful!");
|
|
2474
|
+
return {
|
|
2475
|
+
githubToken,
|
|
2476
|
+
copilotToken: copilotTokenData.token,
|
|
2477
|
+
copilotTokenExpiresAt: parsed.expiresAt,
|
|
2478
|
+
apiEndpoint: parsed.apiEndpoint
|
|
2479
|
+
};
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
exchangeSpinner.fail(
|
|
2482
|
+
`Failed to get Copilot token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2483
|
+
);
|
|
2484
|
+
return null;
|
|
2485
|
+
}
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
logger.error(
|
|
2488
|
+
`GitHub authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2489
|
+
);
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
1137
2493
|
|
|
1138
2494
|
// src/cli/commands/AuthCommand.ts
|
|
1139
2495
|
var AuthCommand = class extends Command2 {
|
|
@@ -1280,15 +2636,32 @@ var ConfigCommand = class extends Command4 {
|
|
|
1280
2636
|
logger.error("Failed to load configuration.");
|
|
1281
2637
|
return 1;
|
|
1282
2638
|
}
|
|
2639
|
+
const activeSettings = config.providers[config.activeProvider];
|
|
2640
|
+
const configuredProviders = Object.keys(config.providers);
|
|
1283
2641
|
console.log(pc7.bold("\n Bashio Configuration\n"));
|
|
1284
|
-
console.log(
|
|
1285
|
-
|
|
1286
|
-
|
|
2642
|
+
console.log(
|
|
2643
|
+
` Active Provider: ${pc7.cyan(PROVIDER_DISPLAY_NAMES[config.activeProvider])}`
|
|
2644
|
+
);
|
|
2645
|
+
console.log(
|
|
2646
|
+
` Model: ${pc7.cyan(activeSettings?.model || "N/A")}`
|
|
2647
|
+
);
|
|
2648
|
+
if (configuredProviders.length > 1) {
|
|
2649
|
+
console.log();
|
|
2650
|
+
console.log(pc7.bold(" Configured Providers"));
|
|
2651
|
+
for (const p of configuredProviders) {
|
|
2652
|
+
const settings2 = config.providers[p];
|
|
2653
|
+
const isActive = p === config.activeProvider;
|
|
2654
|
+
const marker = isActive ? pc7.green("\u25CF") : pc7.dim("\u25CB");
|
|
2655
|
+
console.log(
|
|
2656
|
+
` ${marker} ${PROVIDER_DISPLAY_NAMES[p]} - ${pc7.dim(settings2?.model || "N/A")}`
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
1287
2660
|
const settings = config.settings;
|
|
1288
2661
|
console.log();
|
|
1289
2662
|
console.log(pc7.bold(" Settings"));
|
|
1290
2663
|
console.log(
|
|
1291
|
-
` History:
|
|
2664
|
+
` History: ${settings?.historyEnabled !== false ? pc7.green("enabled") : pc7.gray("disabled")}`
|
|
1292
2665
|
);
|
|
1293
2666
|
console.log(
|
|
1294
2667
|
` Auto-confirm shortcuts: ${settings?.autoConfirmShortcuts ? pc7.green("enabled") : pc7.gray("disabled")}`
|
|
@@ -2029,16 +3402,35 @@ var selectWithEsc = async (config) => {
|
|
|
2029
3402
|
}
|
|
2030
3403
|
};
|
|
2031
3404
|
var isPromptExit = (error) => {
|
|
2032
|
-
if (!(error instanceof Error))
|
|
2033
|
-
|
|
2034
|
-
}
|
|
2035
|
-
return error.name === "ExitPromptError" || error.name === "AbortPromptError" || error.name === "CancelPromptError";
|
|
3405
|
+
if (!(error instanceof Error)) return false;
|
|
3406
|
+
return error.name === "ExitPromptError" || error.name === "AbortPromptError" || error.name === "CancelPromptError" || error.message.includes("SIGINT") || error.message.includes("force closed");
|
|
2036
3407
|
};
|
|
3408
|
+
function getModelsForProvider(provider) {
|
|
3409
|
+
switch (provider) {
|
|
3410
|
+
case "claude":
|
|
3411
|
+
return CLAUDE_MODELS;
|
|
3412
|
+
case "claude-subscription":
|
|
3413
|
+
return CLAUDE_SUBSCRIPTION_MODELS;
|
|
3414
|
+
case "openai":
|
|
3415
|
+
return OPENAI_MODELS;
|
|
3416
|
+
case "chatgpt-subscription":
|
|
3417
|
+
return CHATGPT_SUBSCRIPTION_MODELS;
|
|
3418
|
+
case "copilot":
|
|
3419
|
+
return COPILOT_MODELS;
|
|
3420
|
+
case "openrouter":
|
|
3421
|
+
return OPENROUTER_MODELS;
|
|
3422
|
+
case "ollama":
|
|
3423
|
+
return [];
|
|
3424
|
+
// Handled separately
|
|
3425
|
+
default:
|
|
3426
|
+
return [];
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
2037
3429
|
var ModelCommand = class extends Command8 {
|
|
2038
3430
|
static paths = [["model"], ["--model"]];
|
|
2039
3431
|
static usage = Command8.Usage({
|
|
2040
|
-
description: "Change the AI
|
|
2041
|
-
examples: [["Change model", "$0 --model"]]
|
|
3432
|
+
description: "Change the AI provider and model",
|
|
3433
|
+
examples: [["Change provider/model", "$0 --model"]]
|
|
2042
3434
|
});
|
|
2043
3435
|
async execute() {
|
|
2044
3436
|
if (!configExists()) {
|
|
@@ -2053,70 +3445,95 @@ var ModelCommand = class extends Command8 {
|
|
|
2053
3445
|
logger.error("Failed to load configuration.");
|
|
2054
3446
|
return 1;
|
|
2055
3447
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
3448
|
+
const configuredProviders = Object.keys(config.providers);
|
|
3449
|
+
if (configuredProviders.length === 0) {
|
|
3450
|
+
logger.warn("No providers configured.");
|
|
3451
|
+
console.log(pc12.gray("Run 'b --auth' to set up a provider.\n"));
|
|
3452
|
+
return 1;
|
|
3453
|
+
}
|
|
3454
|
+
const activeSettings = config.providers[config.activeProvider];
|
|
3455
|
+
console.log(pc12.bold("\n Change Provider & Model\n"));
|
|
3456
|
+
console.log(
|
|
3457
|
+
pc12.gray(
|
|
3458
|
+
` Current: ${PROVIDER_DISPLAY_NAMES[config.activeProvider]} / ${activeSettings?.model}`
|
|
3459
|
+
)
|
|
3460
|
+
);
|
|
2059
3461
|
console.log(pc12.dim(" Press Esc to cancel\n"));
|
|
2060
|
-
let newModel;
|
|
2061
3462
|
try {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
3463
|
+
const providerChoices = configuredProviders.map((p) => {
|
|
3464
|
+
const settings = config.providers[p];
|
|
3465
|
+
const isActive = p === config.activeProvider;
|
|
3466
|
+
const marker = isActive ? pc12.green("\u25CF") : pc12.dim("\u25CB");
|
|
3467
|
+
const name = `${marker} ${PROVIDER_DISPLAY_NAMES[p]}`;
|
|
3468
|
+
const description = settings?.model || "Not configured";
|
|
3469
|
+
return { value: p, name, description };
|
|
3470
|
+
});
|
|
3471
|
+
providerChoices.push({
|
|
3472
|
+
value: "__add_new__",
|
|
3473
|
+
name: pc12.cyan("+ Add new provider..."),
|
|
3474
|
+
description: "Configure a new AI provider"
|
|
3475
|
+
});
|
|
3476
|
+
const selectedProvider = await selectWithEsc({
|
|
3477
|
+
message: "Select provider:",
|
|
3478
|
+
choices: providerChoices
|
|
3479
|
+
});
|
|
3480
|
+
if (selectedProvider === "__add_new__") {
|
|
3481
|
+
console.log(pc12.dim("\n Run 'b --auth' to add a new provider.\n"));
|
|
3482
|
+
return 0;
|
|
3483
|
+
}
|
|
3484
|
+
const currentModel = config.providers[selectedProvider]?.model;
|
|
3485
|
+
let newModel;
|
|
3486
|
+
if (selectedProvider === "ollama") {
|
|
3487
|
+
const host = config.providers.ollama?.credentials.type === "local" ? config.providers.ollama.credentials.host : "http://localhost:11434";
|
|
3488
|
+
const spinner = createSpinner("Fetching available models...").start();
|
|
3489
|
+
const availableModels = await OllamaProvider.getAvailableModels(host);
|
|
3490
|
+
spinner.stop();
|
|
3491
|
+
if (availableModels.length === 0) {
|
|
3492
|
+
logger.warn("No models found. Make sure Ollama is running.");
|
|
3493
|
+
console.log(pc12.gray("\n Install a model: ollama pull llama3.2\n"));
|
|
3494
|
+
return 1;
|
|
2084
3495
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
3496
|
+
newModel = await selectWithEsc({
|
|
3497
|
+
message: "Select model:",
|
|
3498
|
+
choices: availableModels.map((m) => ({ value: m, name: m })),
|
|
3499
|
+
default: currentModel
|
|
3500
|
+
});
|
|
3501
|
+
} else {
|
|
3502
|
+
const models = getModelsForProvider(selectedProvider);
|
|
3503
|
+
newModel = await selectWithEsc({
|
|
3504
|
+
message: "Select model:",
|
|
3505
|
+
choices: models.map((m) => ({ value: m.value, name: m.label })),
|
|
3506
|
+
default: currentModel
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
const providerSettings = config.providers[selectedProvider];
|
|
3510
|
+
if (!providerSettings) {
|
|
3511
|
+
logger.error("Provider not configured.");
|
|
3512
|
+
return 1;
|
|
3513
|
+
}
|
|
3514
|
+
const updatedConfig = {
|
|
3515
|
+
...config,
|
|
3516
|
+
activeProvider: selectedProvider,
|
|
3517
|
+
providers: {
|
|
3518
|
+
...config.providers,
|
|
3519
|
+
[selectedProvider]: {
|
|
3520
|
+
...providerSettings,
|
|
3521
|
+
model: newModel
|
|
2094
3522
|
}
|
|
2095
|
-
newModel = await selectWithEsc({
|
|
2096
|
-
message: "Select new model:",
|
|
2097
|
-
choices: availableModels.map((m) => ({
|
|
2098
|
-
value: m,
|
|
2099
|
-
name: m
|
|
2100
|
-
})),
|
|
2101
|
-
default: config.model
|
|
2102
|
-
});
|
|
2103
|
-
break;
|
|
2104
|
-
}
|
|
2105
|
-
case "openrouter": {
|
|
2106
|
-
newModel = await selectWithEsc({
|
|
2107
|
-
message: "Select new model:",
|
|
2108
|
-
choices: OPENROUTER_MODELS.map((m) => ({
|
|
2109
|
-
value: m.value,
|
|
2110
|
-
name: m.label
|
|
2111
|
-
})),
|
|
2112
|
-
default: config.model
|
|
2113
|
-
});
|
|
2114
|
-
break;
|
|
2115
3523
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
3524
|
+
};
|
|
3525
|
+
saveConfig(updatedConfig);
|
|
3526
|
+
const changed = selectedProvider !== config.activeProvider || newModel !== currentModel;
|
|
3527
|
+
if (changed) {
|
|
3528
|
+
console.log();
|
|
3529
|
+
logger.success(
|
|
3530
|
+
`Switched to: ${PROVIDER_DISPLAY_NAMES[selectedProvider]} / ${newModel}`
|
|
3531
|
+
);
|
|
3532
|
+
} else {
|
|
3533
|
+
logger.info("No changes made.");
|
|
2119
3534
|
}
|
|
3535
|
+
console.log();
|
|
3536
|
+
return 0;
|
|
2120
3537
|
} catch (error) {
|
|
2121
3538
|
if (isPromptExit(error)) {
|
|
2122
3539
|
console.log(pc12.dim("\n Cancelled.\n"));
|
|
@@ -2124,16 +3541,6 @@ var ModelCommand = class extends Command8 {
|
|
|
2124
3541
|
}
|
|
2125
3542
|
throw error;
|
|
2126
3543
|
}
|
|
2127
|
-
if (newModel === config.model) {
|
|
2128
|
-
logger.info("Model unchanged.");
|
|
2129
|
-
return 0;
|
|
2130
|
-
}
|
|
2131
|
-
config.model = newModel;
|
|
2132
|
-
saveConfig(config);
|
|
2133
|
-
console.log();
|
|
2134
|
-
logger.success(`Model changed to: ${newModel}`);
|
|
2135
|
-
console.log();
|
|
2136
|
-
return 0;
|
|
2137
3544
|
}
|
|
2138
3545
|
};
|
|
2139
3546
|
|
|
@@ -2546,7 +3953,7 @@ var SuggestShortcutsCommand = class extends Command12 {
|
|
|
2546
3953
|
// src/cli/index.ts
|
|
2547
3954
|
var pkg = {
|
|
2548
3955
|
name: "bashio",
|
|
2549
|
-
version: "
|
|
3956
|
+
version: "1.1.1"
|
|
2550
3957
|
};
|
|
2551
3958
|
var notifier = updateNotifier({
|
|
2552
3959
|
pkg,
|