@oh-my-pi/pi-ai 1.337.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Anthropic OAuth flow (Claude Pro/Max)
3
+ */
4
+
5
+ import { generatePKCE } from "./pkce.js";
6
+ import type { OAuthCredentials } from "./types.js";
7
+
8
+ const decode = (s: string) => atob(s);
9
+ const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
10
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
11
+ const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
12
+ const REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
13
+ const SCOPES = "org:create_api_key user:profile user:inference";
14
+
15
+ /**
16
+ * Login with Anthropic OAuth (device code flow)
17
+ *
18
+ * @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser)
19
+ * @param onPromptCode - Callback to prompt user for the authorization code
20
+ */
21
+ export async function loginAnthropic(
22
+ onAuthUrl: (url: string) => void,
23
+ onPromptCode: () => Promise<string>,
24
+ ): Promise<OAuthCredentials> {
25
+ const { verifier, challenge } = await generatePKCE();
26
+
27
+ // Build authorization URL
28
+ const authParams = new URLSearchParams({
29
+ code: "true",
30
+ client_id: CLIENT_ID,
31
+ response_type: "code",
32
+ redirect_uri: REDIRECT_URI,
33
+ scope: SCOPES,
34
+ code_challenge: challenge,
35
+ code_challenge_method: "S256",
36
+ state: verifier,
37
+ });
38
+
39
+ const authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`;
40
+
41
+ // Notify caller with URL to open
42
+ onAuthUrl(authUrl);
43
+
44
+ // Wait for user to paste authorization code (format: code#state)
45
+ const authCode = await onPromptCode();
46
+ const splits = authCode.split("#");
47
+ const code = splits[0];
48
+ const state = splits[1];
49
+
50
+ // Exchange code for tokens
51
+ const tokenResponse = await fetch(TOKEN_URL, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ },
56
+ body: JSON.stringify({
57
+ grant_type: "authorization_code",
58
+ client_id: CLIENT_ID,
59
+ code: code,
60
+ state: state,
61
+ redirect_uri: REDIRECT_URI,
62
+ code_verifier: verifier,
63
+ }),
64
+ });
65
+
66
+ if (!tokenResponse.ok) {
67
+ const error = await tokenResponse.text();
68
+ throw new Error(`Token exchange failed: ${error}`);
69
+ }
70
+
71
+ const tokenData = (await tokenResponse.json()) as {
72
+ access_token: string;
73
+ refresh_token: string;
74
+ expires_in: number;
75
+ };
76
+
77
+ // Calculate expiry time (current time + expires_in seconds - 5 min buffer)
78
+ const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
79
+
80
+ // Save credentials
81
+ return {
82
+ refresh: tokenData.refresh_token,
83
+ access: tokenData.access_token,
84
+ expires: expiresAt,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Refresh Anthropic OAuth token
90
+ */
91
+ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
92
+ const response = await fetch(TOKEN_URL, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({
96
+ grant_type: "refresh_token",
97
+ client_id: CLIENT_ID,
98
+ refresh_token: refreshToken,
99
+ }),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const error = await response.text();
104
+ throw new Error(`Anthropic token refresh failed: ${error}`);
105
+ }
106
+
107
+ const data = (await response.json()) as {
108
+ access_token: string;
109
+ refresh_token: string;
110
+ expires_in: number;
111
+ };
112
+
113
+ return {
114
+ refresh: data.refresh_token,
115
+ access: data.access_token,
116
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
117
+ };
118
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * GitHub Copilot OAuth flow
3
+ */
4
+
5
+ import { getModels } from "../../models.js";
6
+ import type { OAuthCredentials } from "./types.js";
7
+
8
+ const decode = (s: string) => atob(s);
9
+ const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
10
+
11
+ const COPILOT_HEADERS = {
12
+ "User-Agent": "GitHubCopilotChat/0.35.0",
13
+ "Editor-Version": "vscode/1.107.0",
14
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
15
+ "Copilot-Integration-Id": "vscode-chat",
16
+ } as const;
17
+
18
+ type DeviceCodeResponse = {
19
+ device_code: string;
20
+ user_code: string;
21
+ verification_uri: string;
22
+ interval: number;
23
+ expires_in: number;
24
+ };
25
+
26
+ type DeviceTokenSuccessResponse = {
27
+ access_token: string;
28
+ token_type?: string;
29
+ scope?: string;
30
+ };
31
+
32
+ type DeviceTokenErrorResponse = {
33
+ error: string;
34
+ error_description?: string;
35
+ interval?: number;
36
+ };
37
+
38
+ export function normalizeDomain(input: string): string | null {
39
+ const trimmed = input.trim();
40
+ if (!trimmed) return null;
41
+ try {
42
+ const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
43
+ return url.hostname;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function getUrls(domain: string): {
50
+ deviceCodeUrl: string;
51
+ accessTokenUrl: string;
52
+ copilotTokenUrl: string;
53
+ } {
54
+ return {
55
+ deviceCodeUrl: `https://${domain}/login/device/code`,
56
+ accessTokenUrl: `https://${domain}/login/oauth/access_token`,
57
+ copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Parse the proxy-ep from a Copilot token and convert to API base URL.
63
+ * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
64
+ * Returns API URL like https://api.individual.githubcopilot.com
65
+ */
66
+ function getBaseUrlFromToken(token: string): string | null {
67
+ const match = token.match(/proxy-ep=([^;]+)/);
68
+ if (!match) return null;
69
+ const proxyHost = match[1];
70
+ // Convert proxy.xxx to api.xxx
71
+ const apiHost = proxyHost.replace(/^proxy\./, "api.");
72
+ return `https://${apiHost}`;
73
+ }
74
+
75
+ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
76
+ // If we have a token, extract the base URL from proxy-ep
77
+ if (token) {
78
+ const urlFromToken = getBaseUrlFromToken(token);
79
+ if (urlFromToken) return urlFromToken;
80
+ }
81
+ // Fallback for enterprise or if token parsing fails
82
+ if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;
83
+ return "https://api.individual.githubcopilot.com";
84
+ }
85
+
86
+ async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
87
+ const response = await fetch(url, init);
88
+ if (!response.ok) {
89
+ const text = await response.text();
90
+ throw new Error(`${response.status} ${response.statusText}: ${text}`);
91
+ }
92
+ return response.json();
93
+ }
94
+
95
+ async function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {
96
+ const urls = getUrls(domain);
97
+ const data = await fetchJson(urls.deviceCodeUrl, {
98
+ method: "POST",
99
+ headers: {
100
+ Accept: "application/json",
101
+ "Content-Type": "application/json",
102
+ "User-Agent": "GitHubCopilotChat/0.35.0",
103
+ },
104
+ body: JSON.stringify({
105
+ client_id: CLIENT_ID,
106
+ scope: "read:user",
107
+ }),
108
+ });
109
+
110
+ if (!data || typeof data !== "object") {
111
+ throw new Error("Invalid device code response");
112
+ }
113
+
114
+ const deviceCode = (data as Record<string, unknown>).device_code;
115
+ const userCode = (data as Record<string, unknown>).user_code;
116
+ const verificationUri = (data as Record<string, unknown>).verification_uri;
117
+ const interval = (data as Record<string, unknown>).interval;
118
+ const expiresIn = (data as Record<string, unknown>).expires_in;
119
+
120
+ if (
121
+ typeof deviceCode !== "string" ||
122
+ typeof userCode !== "string" ||
123
+ typeof verificationUri !== "string" ||
124
+ typeof interval !== "number" ||
125
+ typeof expiresIn !== "number"
126
+ ) {
127
+ throw new Error("Invalid device code response fields");
128
+ }
129
+
130
+ return {
131
+ device_code: deviceCode,
132
+ user_code: userCode,
133
+ verification_uri: verificationUri,
134
+ interval,
135
+ expires_in: expiresIn,
136
+ };
137
+ }
138
+
139
+ async function pollForGitHubAccessToken(
140
+ domain: string,
141
+ deviceCode: string,
142
+ intervalSeconds: number,
143
+ expiresIn: number,
144
+ ) {
145
+ const urls = getUrls(domain);
146
+ const deadline = Date.now() + expiresIn * 1000;
147
+ let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
148
+
149
+ while (Date.now() < deadline) {
150
+ const raw = await fetchJson(urls.accessTokenUrl, {
151
+ method: "POST",
152
+ headers: {
153
+ Accept: "application/json",
154
+ "Content-Type": "application/json",
155
+ "User-Agent": "GitHubCopilotChat/0.35.0",
156
+ },
157
+ body: JSON.stringify({
158
+ client_id: CLIENT_ID,
159
+ device_code: deviceCode,
160
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
161
+ }),
162
+ });
163
+
164
+ if (raw && typeof raw === "object" && typeof (raw as DeviceTokenSuccessResponse).access_token === "string") {
165
+ return (raw as DeviceTokenSuccessResponse).access_token;
166
+ }
167
+
168
+ if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") {
169
+ const err = (raw as DeviceTokenErrorResponse).error;
170
+ if (err === "authorization_pending") {
171
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
172
+ continue;
173
+ }
174
+
175
+ if (err === "slow_down") {
176
+ intervalMs += 5000;
177
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
178
+ continue;
179
+ }
180
+
181
+ throw new Error(`Device flow failed: ${err}`);
182
+ }
183
+
184
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
185
+ }
186
+
187
+ throw new Error("Device flow timed out");
188
+ }
189
+
190
+ /**
191
+ * Refresh GitHub Copilot token
192
+ */
193
+ export async function refreshGitHubCopilotToken(
194
+ refreshToken: string,
195
+ enterpriseDomain?: string,
196
+ ): Promise<OAuthCredentials> {
197
+ const domain = enterpriseDomain || "github.com";
198
+ const urls = getUrls(domain);
199
+
200
+ const raw = await fetchJson(urls.copilotTokenUrl, {
201
+ headers: {
202
+ Accept: "application/json",
203
+ Authorization: `Bearer ${refreshToken}`,
204
+ ...COPILOT_HEADERS,
205
+ },
206
+ });
207
+
208
+ if (!raw || typeof raw !== "object") {
209
+ throw new Error("Invalid Copilot token response");
210
+ }
211
+
212
+ const token = (raw as Record<string, unknown>).token;
213
+ const expiresAt = (raw as Record<string, unknown>).expires_at;
214
+
215
+ if (typeof token !== "string" || typeof expiresAt !== "number") {
216
+ throw new Error("Invalid Copilot token response fields");
217
+ }
218
+
219
+ return {
220
+ refresh: refreshToken,
221
+ access: token,
222
+ expires: expiresAt * 1000 - 5 * 60 * 1000,
223
+ enterpriseUrl: enterpriseDomain,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Enable a model for the user's GitHub Copilot account.
229
+ * This is required for some models (like Claude, Grok) before they can be used.
230
+ */
231
+ async function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {
232
+ const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);
233
+ const url = `${baseUrl}/models/${modelId}/policy`;
234
+
235
+ try {
236
+ const response = await fetch(url, {
237
+ method: "POST",
238
+ headers: {
239
+ "Content-Type": "application/json",
240
+ Authorization: `Bearer ${token}`,
241
+ ...COPILOT_HEADERS,
242
+ "openai-intent": "chat-policy",
243
+ "x-interaction-type": "chat-policy",
244
+ },
245
+ body: JSON.stringify({ state: "enabled" }),
246
+ });
247
+ return response.ok;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Enable all known GitHub Copilot models that may require policy acceptance.
255
+ * Called after successful login to ensure all models are available.
256
+ */
257
+ async function enableAllGitHubCopilotModels(
258
+ token: string,
259
+ enterpriseDomain?: string,
260
+ onProgress?: (model: string, success: boolean) => void,
261
+ ): Promise<void> {
262
+ const models = getModels("github-copilot");
263
+ await Promise.all(
264
+ models.map(async (model) => {
265
+ const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
266
+ onProgress?.(model.id, success);
267
+ }),
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Login with GitHub Copilot OAuth (device code flow)
273
+ *
274
+ * @param options.onAuth - Callback with URL and optional instructions (user code)
275
+ * @param options.onPrompt - Callback to prompt user for input
276
+ * @param options.onProgress - Optional progress callback
277
+ */
278
+ export async function loginGitHubCopilot(options: {
279
+ onAuth: (url: string, instructions?: string) => void;
280
+ onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;
281
+ onProgress?: (message: string) => void;
282
+ }): Promise<OAuthCredentials> {
283
+ const input = await options.onPrompt({
284
+ message: "GitHub Enterprise URL/domain (blank for github.com)",
285
+ placeholder: "company.ghe.com",
286
+ allowEmpty: true,
287
+ });
288
+
289
+ const trimmed = input.trim();
290
+ const enterpriseDomain = normalizeDomain(input);
291
+ if (trimmed && !enterpriseDomain) {
292
+ throw new Error("Invalid GitHub Enterprise URL/domain");
293
+ }
294
+ const domain = enterpriseDomain || "github.com";
295
+
296
+ const device = await startDeviceFlow(domain);
297
+ options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
298
+
299
+ const githubAccessToken = await pollForGitHubAccessToken(
300
+ domain,
301
+ device.device_code,
302
+ device.interval,
303
+ device.expires_in,
304
+ );
305
+ const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
306
+
307
+ // Enable all models after successful login
308
+ options.onProgress?.("Enabling models...");
309
+ await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
310
+ return credentials;
311
+ }