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 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 Config = z.object({
52
- version: z.number().default(1),
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
- return Config.parse(data);
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
- const data = await response.json();
643
- if (data.error) {
644
- throw new Error(`Claude API error: ${data.error.message}`);
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 textContent = data.content.find((c) => c.type === "text");
647
- if (!textContent) {
648
- throw new Error("No text response from Claude");
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
- return textContent.text.trim();
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 response = await fetch("https://api.anthropic.com/v1/messages", {
664
- method: "POST",
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 CLAUDE_MODELS = [
683
- { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (recommended)" },
684
- { value: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet" },
685
- { value: "claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku (fast)" }
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-4o", label: "GPT-4o (recommended)" },
827
- { value: "gpt-4o-mini", label: "GPT-4o Mini (fast)" },
828
- { value: "gpt-4-turbo", label: "GPT-4 Turbo" }
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: config.model,
914
- credentials: config.credentials
1894
+ model: settings.model,
1895
+ credentials: settings.credentials
915
1896
  };
916
- switch (config.provider) {
1897
+ return createProviderFromType(activeProvider, providerConfig);
1898
+ }
1899
+ function createProviderFromType(provider, config) {
1900
+ switch (provider) {
917
1901
  case "claude":
918
- return new ClaudeProvider(providerConfig);
1902
+ return new ClaudeProvider(config);
1903
+ case "claude-subscription":
1904
+ return new ClaudeSubscriptionProvider(config);
919
1905
  case "openai":
920
- return new OpenAIProvider(providerConfig);
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(providerConfig);
1912
+ return new OllamaProvider(config);
923
1913
  case "openrouter":
924
- return new OpenRouterProvider(providerConfig);
1914
+ return new OpenRouterProvider(config);
925
1915
  default:
926
- throw new Error(`Unknown provider: ${config.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 (Anthropic)",
2033
+ name: "Claude (API Key)",
1006
2034
  description: "Use Anthropic API key"
1007
2035
  },
1008
2036
  {
1009
2037
  value: "openai",
1010
- name: "ChatGPT (OpenAI)",
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 config = {
1102
- version: 1,
1103
- provider,
1104
- model,
1105
- credentials,
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(config);
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
- saveConfig(config);
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(` Provider: ${pc7.cyan(config.provider)}`);
1285
- console.log(` Model: ${pc7.cyan(config.model)}`);
1286
- console.log(` Auth: ${pc7.green("Configured")}`);
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: ${settings?.historyEnabled !== false ? pc7.green("enabled") : pc7.gray("disabled")}`
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
- return false;
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 model for current provider",
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
- console.log(pc12.bold("\n Change AI Model\n"));
2057
- console.log(pc12.gray(` Current provider: ${config.provider}`));
2058
- console.log(pc12.gray(` Current model: ${config.model}`));
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
- switch (config.provider) {
2063
- case "claude": {
2064
- newModel = await selectWithEsc({
2065
- message: "Select new model:",
2066
- choices: CLAUDE_MODELS.map((m) => ({
2067
- value: m.value,
2068
- name: m.label
2069
- })),
2070
- default: config.model
2071
- });
2072
- break;
2073
- }
2074
- case "openai": {
2075
- newModel = await selectWithEsc({
2076
- message: "Select new model:",
2077
- choices: OPENAI_MODELS.map((m) => ({
2078
- value: m.value,
2079
- name: m.label
2080
- })),
2081
- default: config.model
2082
- });
2083
- break;
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
- case "ollama": {
2086
- const host = config.credentials.type === "local" ? config.credentials.host : "http://localhost:11434";
2087
- const spinner = createSpinner("Fetching available models...").start();
2088
- const availableModels = await OllamaProvider.getAvailableModels(host);
2089
- spinner.stop();
2090
- if (availableModels.length === 0) {
2091
- logger.warn("No models found. Make sure Ollama is running.");
2092
- console.log(pc12.gray("\n Install a model: ollama pull llama3.2\n"));
2093
- return 1;
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
- default:
2117
- logger.error(`Unknown provider: ${config.provider}`);
2118
- return 1;
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: "0.7.0"
3956
+ version: "1.1.1"
2550
3957
  };
2551
3958
  var notifier = updateNotifier({
2552
3959
  pkg,