pi-free 2.2.6 → 2.2.8

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.
@@ -1,155 +1,152 @@
1
- /**
2
- * Kilo device authorization flow and token management.
3
- */
4
-
5
- import type {
6
- OAuthCredentials,
7
- OAuthLoginCallbacks,
8
- } from "@earendil-works/pi-ai";
9
- import {
10
- KILO_POLL_INTERVAL_MS,
11
- KILO_TOKEN_EXPIRATION_MS,
12
- } from "../../constants.ts";
13
- import { createLogger } from "../../lib/logger.ts";
14
- import { openBrowser } from "../../lib/open-browser.ts";
15
-
16
- const _logger = createLogger("kilo-auth");
17
-
18
- const KILO_API_BASE = process.env.KILO_API_URL || "https://api.kilo.ai";
19
- const DEVICE_AUTH_ENDPOINT = `${KILO_API_BASE}/api/device-auth/codes`;
20
- const PROFILE_ENDPOINT = `${KILO_API_BASE}/api/profile`;
21
-
22
- // =============================================================================
23
- // Balance & Rate Limit
24
- // =============================================================================
25
-
26
- export async function fetchKiloBalance(token: string): Promise<number | null> {
27
- try {
28
- const response = await fetch(`${PROFILE_ENDPOINT}/balance`, {
29
- headers: {
30
- Authorization: `Bearer ${token}`,
31
- "Content-Type": "application/json",
32
- },
33
- });
34
- if (!response.ok) return null;
35
- const data = (await response.json()) as { balance?: number };
36
- return data.balance ?? null;
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- export function formatCredits(balance: number): string {
43
- return balance >= 1000
44
- ? `$${(balance / 1000).toFixed(1)}k`
45
- : `$${balance.toFixed(2)}`;
46
- }
47
-
48
- // =============================================================================
49
- // Device auth
50
- // =============================================================================
51
-
52
- interface DeviceAuthResponse {
53
- code: string;
54
- verificationUrl: string;
55
- expiresIn: number;
56
- }
57
-
58
- interface DeviceAuthPollResponse {
59
- status: "pending" | "approved" | "denied" | "expired";
60
- token?: string;
61
- }
62
-
63
- function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
64
- return new Promise((resolve, reject) => {
65
- if (signal?.aborted) {
66
- reject(new Error("Login cancelled"));
67
- return;
68
- }
69
- const timeout = setTimeout(resolve, ms);
70
- signal?.addEventListener(
71
- "abort",
72
- () => {
73
- clearTimeout(timeout);
74
- reject(new Error("Login cancelled"));
75
- },
76
- { once: true },
77
- );
78
- });
79
- }
80
-
81
- async function initiateDeviceAuth(): Promise<DeviceAuthResponse> {
82
- const response = await fetch(DEVICE_AUTH_ENDPOINT, {
83
- method: "POST",
84
- headers: { "Content-Type": "application/json" },
85
- });
86
- if (!response.ok) {
87
- throw new Error(
88
- response.status === 429
89
- ? "Too many pending authorization requests. Please try again later."
90
- : `Failed to initiate device authorization: ${response.status}`,
91
- );
92
- }
93
- return (await response.json()) as DeviceAuthResponse;
94
- }
95
-
96
- async function pollDeviceAuth(code: string): Promise<DeviceAuthPollResponse> {
97
- const response = await fetch(`${DEVICE_AUTH_ENDPOINT}/${code}`);
98
- if (response.status === 202) return { status: "pending" };
99
- if (response.status === 403) return { status: "denied" };
100
- if (response.status === 410) return { status: "expired" };
101
- if (!response.ok)
102
- throw new Error(`Failed to poll device authorization: ${response.status}`);
103
- return (await response.json()) as DeviceAuthPollResponse;
104
- }
105
-
106
- export async function loginKilo(
107
- callbacks: OAuthLoginCallbacks,
108
- ): Promise<OAuthCredentials> {
109
- callbacks.onProgress?.("Initiating device authorization...");
110
- const { code, verificationUrl, expiresIn } = await initiateDeviceAuth();
111
-
112
- callbacks.onAuth({
113
- url: verificationUrl,
114
- instructions: `Enter code: ${code}`,
115
- });
116
- openBrowser(verificationUrl);
117
- callbacks.onProgress?.("Waiting for browser authorization...");
118
-
119
- const deadline = Date.now() + expiresIn * 1000;
120
- while (Date.now() < deadline) {
121
- if (callbacks.signal?.aborted) throw new Error("Login cancelled");
122
- await abortableSleep(KILO_POLL_INTERVAL_MS, callbacks.signal);
123
-
124
- const result = await pollDeviceAuth(code);
125
- if (result.status === "approved") {
126
- if (!result.token)
127
- throw new Error("Authorization approved but no token received");
128
- callbacks.onProgress?.("Login successful!");
129
- return {
130
- refresh: result.token,
131
- access: result.token,
132
- expires: Date.now() + KILO_TOKEN_EXPIRATION_MS,
133
- };
134
- }
135
- if (result.status === "denied")
136
- throw new Error("Authorization denied by user.");
137
- if (result.status === "expired")
138
- throw new Error("Authorization code expired. Please try again.");
139
-
140
- const remaining = Math.ceil((deadline - Date.now()) / 1000);
141
- callbacks.onProgress?.(
142
- `Waiting for browser authorization... (${remaining}s remaining)`,
143
- );
144
- }
145
- throw new Error("Authentication timed out. Please try again.");
146
- }
147
-
148
- export async function refreshKiloToken(
149
- credentials: OAuthCredentials,
150
- ): Promise<OAuthCredentials> {
151
- if (credentials.expires > Date.now()) return credentials;
152
- throw new Error(
153
- "Kilo token expired. Please run /login kilo to re-authenticate.",
154
- );
155
- }
1
+ /**
2
+ * Kilo device authorization flow and token management.
3
+ */
4
+
5
+ import type {
6
+ OAuthCredentials,
7
+ OAuthLoginCallbacks,
8
+ } from "@earendil-works/pi-ai/compat";
9
+ import {
10
+ KILO_POLL_INTERVAL_MS,
11
+ KILO_TOKEN_EXPIRATION_MS,
12
+ } from "../../constants.ts";
13
+ import { openBrowser } from "../../lib/open-browser.ts";
14
+
15
+ const KILO_API_BASE = process.env.KILO_API_URL || "https://api.kilo.ai";
16
+ const DEVICE_AUTH_ENDPOINT = `${KILO_API_BASE}/api/device-auth/codes`;
17
+ const PROFILE_ENDPOINT = `${KILO_API_BASE}/api/profile`;
18
+
19
+ // =============================================================================
20
+ // Balance & Rate Limit
21
+ // =============================================================================
22
+
23
+ export async function fetchKiloBalance(token: string): Promise<number | null> {
24
+ try {
25
+ const response = await fetch(`${PROFILE_ENDPOINT}/balance`, {
26
+ headers: {
27
+ Authorization: `Bearer ${token}`,
28
+ "Content-Type": "application/json",
29
+ },
30
+ });
31
+ if (!response.ok) return null;
32
+ const data = (await response.json()) as { balance?: number };
33
+ return data.balance ?? null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function formatCredits(balance: number): string {
40
+ return balance >= 1000
41
+ ? `$${(balance / 1000).toFixed(1)}k`
42
+ : `$${balance.toFixed(2)}`;
43
+ }
44
+
45
+ // =============================================================================
46
+ // Device auth
47
+ // =============================================================================
48
+
49
+ interface DeviceAuthResponse {
50
+ code: string;
51
+ verificationUrl: string;
52
+ expiresIn: number;
53
+ }
54
+
55
+ interface DeviceAuthPollResponse {
56
+ status: "pending" | "approved" | "denied" | "expired";
57
+ token?: string;
58
+ }
59
+
60
+ function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
61
+ return new Promise((resolve, reject) => {
62
+ if (signal?.aborted) {
63
+ reject(new Error("Login cancelled"));
64
+ return;
65
+ }
66
+ const timeout = setTimeout(resolve, ms);
67
+ signal?.addEventListener(
68
+ "abort",
69
+ () => {
70
+ clearTimeout(timeout);
71
+ reject(new Error("Login cancelled"));
72
+ },
73
+ { once: true },
74
+ );
75
+ });
76
+ }
77
+
78
+ async function initiateDeviceAuth(): Promise<DeviceAuthResponse> {
79
+ const response = await fetch(DEVICE_AUTH_ENDPOINT, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ });
83
+ if (!response.ok) {
84
+ throw new Error(
85
+ response.status === 429
86
+ ? "Too many pending authorization requests. Please try again later."
87
+ : `Failed to initiate device authorization: ${response.status}`,
88
+ );
89
+ }
90
+ return (await response.json()) as DeviceAuthResponse;
91
+ }
92
+
93
+ async function pollDeviceAuth(code: string): Promise<DeviceAuthPollResponse> {
94
+ const response = await fetch(`${DEVICE_AUTH_ENDPOINT}/${code}`);
95
+ if (response.status === 202) return { status: "pending" };
96
+ if (response.status === 403) return { status: "denied" };
97
+ if (response.status === 410) return { status: "expired" };
98
+ if (!response.ok)
99
+ throw new Error(`Failed to poll device authorization: ${response.status}`);
100
+ return (await response.json()) as DeviceAuthPollResponse;
101
+ }
102
+
103
+ export async function loginKilo(
104
+ callbacks: OAuthLoginCallbacks,
105
+ ): Promise<OAuthCredentials> {
106
+ callbacks.onProgress?.("Initiating device authorization...");
107
+ const { code, verificationUrl, expiresIn } = await initiateDeviceAuth();
108
+
109
+ callbacks.onAuth({
110
+ url: verificationUrl,
111
+ instructions: `Enter code: ${code}`,
112
+ });
113
+ openBrowser(verificationUrl);
114
+ callbacks.onProgress?.("Waiting for browser authorization...");
115
+
116
+ const deadline = Date.now() + expiresIn * 1000;
117
+ while (Date.now() < deadline) {
118
+ if (callbacks.signal?.aborted) throw new Error("Login cancelled");
119
+ await abortableSleep(KILO_POLL_INTERVAL_MS, callbacks.signal);
120
+
121
+ const result = await pollDeviceAuth(code);
122
+ if (result.status === "approved") {
123
+ if (!result.token)
124
+ throw new Error("Authorization approved but no token received");
125
+ callbacks.onProgress?.("Login successful!");
126
+ return {
127
+ refresh: result.token,
128
+ access: result.token,
129
+ expires: Date.now() + KILO_TOKEN_EXPIRATION_MS,
130
+ };
131
+ }
132
+ if (result.status === "denied")
133
+ throw new Error("Authorization denied by user.");
134
+ if (result.status === "expired")
135
+ throw new Error("Authorization code expired. Please try again.");
136
+
137
+ const remaining = Math.ceil((deadline - Date.now()) / 1000);
138
+ callbacks.onProgress?.(
139
+ `Waiting for browser authorization... (${remaining}s remaining)`,
140
+ );
141
+ }
142
+ throw new Error("Authentication timed out. Please try again.");
143
+ }
144
+
145
+ export async function refreshKiloToken(
146
+ credentials: OAuthCredentials,
147
+ ): Promise<OAuthCredentials> {
148
+ if (credentials.expires > Date.now()) return credentials;
149
+ throw new Error(
150
+ "Kilo token expired. Please run /login kilo to re-authenticate.",
151
+ );
152
+ }
@@ -12,10 +12,15 @@
12
12
  * # Free models visible immediately; /login kilo for paid access
13
13
  */
14
14
 
15
- import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
16
15
  import type {
17
- ExtensionAPI,
18
- ProviderModelConfig,
16
+ Api,
17
+ Model,
18
+ OAuthCredentials,
19
+ } from "@earendil-works/pi-ai/compat";
20
+ import {
21
+ readStoredCredential,
22
+ type ExtensionAPI,
23
+ type ProviderModelConfig,
19
24
  } from "@earendil-works/pi-coding-agent";
20
25
  import {
21
26
  getKiloApiKey,
@@ -33,6 +38,7 @@ import {
33
38
  createCtxReRegister,
34
39
  createReRegister,
35
40
  enhanceWithCI,
41
+ isOAuthCredential,
36
42
  loadCachedOrFetchModels,
37
43
  type StoredModels,
38
44
  } from "../../provider-helper.ts";
@@ -226,12 +232,7 @@ export default async function kiloProvider(pi: ExtensionAPI) {
226
232
 
227
233
  // Register with global toggle system
228
234
  const hasKiloKey = !!kiloApiKey;
229
- registerWithGlobalToggle(
230
- PROVIDER_KILO,
231
- stored,
232
- reRegister,
233
- hasKiloKey,
234
- );
235
+ registerWithGlobalToggle(PROVIDER_KILO, stored, reRegister, hasKiloKey);
235
236
 
236
237
  // OAuth config for Kilo
237
238
  const oauthConfig = {
@@ -349,8 +350,8 @@ export default async function kiloProvider(pi: ExtensionAPI) {
349
350
  // ToS notice (once)
350
351
  if (tosShown) return;
351
352
  tosShown = true;
352
- const cred = ctx.modelRegistry.authStorage.get(PROVIDER_KILO);
353
- if (cred?.type === "oauth") return;
353
+ const cred = readStoredCredential(PROVIDER_KILO);
354
+ if (isOAuthCredential(cred)) return;
354
355
  const paidCount = allModels.length - freeModels.length;
355
356
  if (paidCount > 0) {
356
357
  ctx.ui.notify(
@@ -447,8 +448,8 @@ export default async function kiloProvider(pi: ExtensionAPI) {
447
448
  pi.on(
448
449
  "session_start",
449
450
  wrapSessionStartHandler("kilo", (_event, ctx) => {
450
- const cred = ctx.modelRegistry.authStorage.get(PROVIDER_KILO);
451
- if (cred?.type !== "oauth" || refreshInFlight) return Promise.resolve();
451
+ const cred = readStoredCredential(PROVIDER_KILO);
452
+ if (!isOAuthCredential(cred) || refreshInFlight) return Promise.resolve();
452
453
 
453
454
  refreshInFlight = fetchKiloModels({ token: cred.access, freeOnly: false })
454
455
  .then((newModels) => {