pi-free 2.0.2 → 2.0.4

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,155 @@
1
- /**
2
- * Kilo device authorization flow and token management.
3
- */
4
-
5
- import type {
6
- OAuthCredentials,
7
- OAuthLoginCallbacks,
8
- } from "@mariozechner/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 "@mariozechner/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
+ }