berget 1.0.0 → 1.2.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.
Files changed (44) hide show
  1. package/README.md +92 -0
  2. package/dist/index.js +49 -467
  3. package/dist/package.json +35 -0
  4. package/dist/src/client.js +210 -102
  5. package/dist/src/commands/api-keys.js +277 -0
  6. package/dist/src/commands/auth.js +65 -0
  7. package/dist/src/commands/autocomplete.js +24 -0
  8. package/dist/src/commands/billing.js +53 -0
  9. package/dist/src/commands/chat.js +342 -0
  10. package/dist/src/commands/clusters.js +69 -0
  11. package/dist/src/commands/index.js +25 -0
  12. package/dist/src/commands/models.js +69 -0
  13. package/dist/src/commands/users.js +43 -0
  14. package/dist/src/constants/command-structure.js +14 -0
  15. package/dist/src/services/api-key-service.js +6 -16
  16. package/dist/src/services/auth-service.js +49 -47
  17. package/dist/src/services/chat-service.js +300 -0
  18. package/dist/src/utils/config-checker.js +50 -0
  19. package/dist/src/utils/default-api-key.js +237 -0
  20. package/dist/src/utils/error-handler.js +4 -4
  21. package/dist/src/utils/token-manager.js +165 -0
  22. package/index.ts +56 -566
  23. package/package.json +8 -2
  24. package/src/client.ts +279 -80
  25. package/src/commands/api-keys.ts +374 -0
  26. package/src/commands/auth.ts +58 -0
  27. package/src/commands/autocomplete.ts +19 -0
  28. package/src/commands/billing.ts +41 -0
  29. package/src/commands/chat.ts +445 -0
  30. package/src/commands/clusters.ts +65 -0
  31. package/src/commands/index.ts +23 -0
  32. package/src/commands/models.ts +63 -0
  33. package/src/commands/users.ts +37 -0
  34. package/src/constants/command-structure.ts +16 -0
  35. package/src/services/api-key-service.ts +12 -20
  36. package/src/services/auth-service.ts +90 -50
  37. package/src/services/chat-service.ts +295 -0
  38. package/src/types/api.d.ts +238 -178
  39. package/src/types/json.d.ts +4 -0
  40. package/src/utils/config-checker.ts +23 -0
  41. package/src/utils/default-api-key.ts +229 -0
  42. package/src/utils/error-handler.ts +4 -4
  43. package/src/utils/token-manager.ts +150 -0
  44. package/tsconfig.json +1 -1
package/src/client.ts CHANGED
@@ -4,10 +4,7 @@ import * as fs from 'fs'
4
4
  import * as path from 'path'
5
5
  import * as os from 'os'
6
6
  import chalk from 'chalk'
7
-
8
- // Configuration directory
9
- const CONFIG_DIR = path.join(os.homedir(), '.berget')
10
- const TOKEN_FILE = path.join(CONFIG_DIR, 'token.json')
7
+ import { TokenManager } from './utils/token-manager'
11
8
 
12
9
  // API Base URL
13
10
  // Use --local flag to test against local API
@@ -16,7 +13,7 @@ const API_BASE_URL =
16
13
  process.env.BERGET_API_URL ||
17
14
  (isLocalMode ? 'http://localhost:3000' : 'https://api.berget.ai')
18
15
 
19
- if (isLocalMode && !process.env.BERGET_API_URL) {
16
+ if (isLocalMode && !process.env.BERGET_API_URL && process.argv.includes('--debug')) {
20
17
  console.log(chalk.yellow('Using local API endpoint: http://localhost:3000'))
21
18
  }
22
19
 
@@ -31,97 +28,42 @@ export const apiClient = createClient<paths>({
31
28
 
32
29
  // Authentication functions
33
30
  export const getAuthToken = (): string | null => {
34
- try {
35
- if (fs.existsSync(TOKEN_FILE)) {
36
- const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'))
37
- return tokenData.accessToken
38
- }
39
- } catch (error) {
40
- console.error('Error reading auth token:', error)
41
- }
42
- return null
31
+ const tokenManager = TokenManager.getInstance()
32
+ return tokenManager.getAccessToken()
43
33
  }
44
34
 
45
- // Check if token is expired (JWT tokens have an exp claim)
46
- export const isTokenExpired = (token: string): boolean => {
47
- try {
48
- const base64Url = token.split('.')[1]
49
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
50
- const jsonPayload = decodeURIComponent(
51
- atob(base64)
52
- .split('')
53
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
54
- .join('')
55
- )
56
- const payload = JSON.parse(jsonPayload)
57
-
58
- // Check if token has expired
59
- if (payload.exp) {
60
- return payload.exp * 1000 < Date.now()
61
- }
62
- } catch (error) {
63
- // If we can't decode the token, assume it's expired
64
- return true
65
- }
66
-
67
- // If there's no exp claim, assume it's valid
68
- return false
69
- }
70
-
71
- export const saveAuthToken = (token: string): void => {
72
- try {
73
- if (!fs.existsSync(CONFIG_DIR)) {
74
- fs.mkdirSync(CONFIG_DIR, { recursive: true })
75
- }
76
- fs.writeFileSync(TOKEN_FILE, JSON.stringify({ accessToken: token }), 'utf8')
77
- // Set file permissions to be readable only by the owner
78
- fs.chmodSync(TOKEN_FILE, 0o600)
79
- } catch (error) {
80
- console.error('Error saving auth token:', error)
81
- }
35
+ export const saveAuthToken = (
36
+ accessToken: string,
37
+ refreshToken: string,
38
+ expiresIn: number = 3600
39
+ ): void => {
40
+ const tokenManager = TokenManager.getInstance()
41
+ tokenManager.setTokens(accessToken, refreshToken, expiresIn)
82
42
  }
83
43
 
84
44
  export const clearAuthToken = (): void => {
85
- try {
86
- if (fs.existsSync(TOKEN_FILE)) {
87
- fs.unlinkSync(TOKEN_FILE)
88
- }
89
- } catch (error) {
90
- console.error('Error clearing auth token:', error)
91
- }
45
+ const tokenManager = TokenManager.getInstance()
46
+ tokenManager.clearTokens()
92
47
  }
93
48
 
94
- // Create an authenticated client
49
+ // Create an authenticated client with refresh token support
95
50
  export const createAuthenticatedClient = () => {
96
- const token = getAuthToken()
97
- if (!token) {
98
- console.warn(
99
- chalk.yellow(
100
- 'No authentication token found. Please run `berget login` first.'
101
- )
102
- )
103
- } else if (isTokenExpired(token)) {
51
+ const tokenManager = TokenManager.getInstance()
52
+
53
+ if (!tokenManager.getAccessToken() && process.argv.includes('--debug')) {
104
54
  console.warn(
105
55
  chalk.yellow(
106
- 'Your authentication token has expired. Please run `berget login` to get a new token.'
56
+ 'No authentication token found. Please run `berget auth login` first.'
107
57
  )
108
58
  )
109
- // Optionally clear the expired token
110
- clearAuthToken()
111
- return createClient<paths>({
112
- baseUrl: API_BASE_URL,
113
- headers: {
114
- 'Content-Type': 'application/json',
115
- Accept: 'application/json',
116
- },
117
- })
118
59
  }
119
60
 
120
- return createClient<paths>({
61
+ // Create the base client
62
+ const client = createClient<paths>({
121
63
  baseUrl: API_BASE_URL,
122
- headers: token
64
+ headers: tokenManager.getAccessToken()
123
65
  ? {
124
- Authorization: `Bearer ${token}`,
66
+ Authorization: `Bearer ${tokenManager.getAccessToken()}`,
125
67
  'Content-Type': 'application/json',
126
68
  Accept: 'application/json',
127
69
  }
@@ -130,4 +72,261 @@ export const createAuthenticatedClient = () => {
130
72
  Accept: 'application/json',
131
73
  },
132
74
  })
75
+
76
+ // Wrap the client to handle token refresh
77
+ return new Proxy(client, {
78
+ get(target, prop: string | symbol) {
79
+ // For HTTP methods (GET, POST, etc.), add token refresh logic
80
+ if (
81
+ typeof target[prop as keyof typeof target] === 'function' &&
82
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(String(prop))
83
+ ) {
84
+ return async (...args: any[]) => {
85
+ // Check if token is expired before making the request
86
+ if (tokenManager.isTokenExpired() && tokenManager.getRefreshToken()) {
87
+ await refreshAccessToken(tokenManager)
88
+ }
89
+
90
+ // Update the Authorization header with the current token
91
+ if (tokenManager.getAccessToken()) {
92
+ if (!args[1]) args[1] = {}
93
+ if (!args[1].headers) args[1].headers = {}
94
+ args[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`
95
+ }
96
+
97
+ // Make the original request
98
+ let result
99
+ try {
100
+ result = await (target[prop as keyof typeof target] as Function)(
101
+ ...args
102
+ )
103
+ } catch (requestError) {
104
+ if (process.argv.includes('--debug')) {
105
+ console.log(
106
+ chalk.red(
107
+ `DEBUG: Request error: ${
108
+ requestError instanceof Error
109
+ ? requestError.message
110
+ : String(requestError)
111
+ }`
112
+ )
113
+ )
114
+ }
115
+ return {
116
+ error: {
117
+ message: `Request failed: ${
118
+ requestError instanceof Error
119
+ ? requestError.message
120
+ : String(requestError)
121
+ }`,
122
+ },
123
+ }
124
+ }
125
+
126
+ // If we get an auth error, try to refresh the token and retry
127
+ if (result.error) {
128
+ // Detect various forms of authentication errors
129
+ let isAuthError = false;
130
+
131
+ try {
132
+ // Standard 401 Unauthorized
133
+ if (typeof result.error === 'object' && result.error.status === 401) {
134
+ isAuthError = true;
135
+ }
136
+ // OAuth specific errors
137
+ else if (result.error.error &&
138
+ (result.error.error.code === 'invalid_token' ||
139
+ result.error.error.code === 'token_expired' ||
140
+ result.error.error.message === 'Invalid API key' ||
141
+ result.error.error.message?.toLowerCase().includes('token') ||
142
+ result.error.error.message?.toLowerCase().includes('unauthorized'))) {
143
+ isAuthError = true;
144
+ }
145
+ // Message-based detection as fallback
146
+ else if (typeof result.error === 'string' &&
147
+ (result.error.toLowerCase().includes('unauthorized') ||
148
+ result.error.toLowerCase().includes('token') ||
149
+ result.error.toLowerCase().includes('auth'))) {
150
+ isAuthError = true;
151
+ }
152
+ } catch (parseError) {
153
+ // If we can't parse the error structure, do a simple string check
154
+ const errorStr = String(result.error);
155
+ if (errorStr.toLowerCase().includes('unauthorized') ||
156
+ errorStr.toLowerCase().includes('token') ||
157
+ errorStr.toLowerCase().includes('auth')) {
158
+ isAuthError = true;
159
+ }
160
+ }
161
+
162
+ if (isAuthError && tokenManager.getRefreshToken()) {
163
+ if (process.argv.includes('--debug')) {
164
+ console.log(
165
+ chalk.yellow(
166
+ 'DEBUG: Auth error detected, attempting token refresh'
167
+ )
168
+ )
169
+ console.log(
170
+ chalk.yellow(
171
+ `DEBUG: Error details: ${JSON.stringify(
172
+ result.error,
173
+ null,
174
+ 2
175
+ )}`
176
+ )
177
+ )
178
+ }
179
+
180
+ const refreshed = await refreshAccessToken(tokenManager)
181
+ if (refreshed) {
182
+ if (process.argv.includes('--debug')) {
183
+ console.log(
184
+ chalk.green(
185
+ 'DEBUG: Token refreshed successfully, retrying request'
186
+ )
187
+ )
188
+ }
189
+
190
+ // Update the Authorization header with the new token
191
+ if (!args[1]) args[1] = {}
192
+ if (!args[1].headers) args[1].headers = {}
193
+ args[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`
194
+
195
+ // Retry the request
196
+ return await (target[prop as keyof typeof target] as Function)(
197
+ ...args
198
+ )
199
+ } else {
200
+ if (process.argv.includes('--debug')) {
201
+ console.log(chalk.red('DEBUG: Token refresh failed'))
202
+ }
203
+
204
+ // Add a more helpful error message for users
205
+ if (typeof result.error === 'object') {
206
+ result.error.userMessage = 'Your session has expired. Please run `berget auth login` to log in again.'
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ return result
213
+ }
214
+ }
215
+
216
+ // For other properties, just return the original
217
+ return target[prop as keyof typeof target]
218
+ },
219
+ })
220
+ }
221
+
222
+ // Helper function to refresh the access token
223
+ async function refreshAccessToken(
224
+ tokenManager: TokenManager
225
+ ): Promise<boolean> {
226
+ try {
227
+ const refreshToken = tokenManager.getRefreshToken()
228
+ if (!refreshToken) return false
229
+
230
+ if (process.argv.includes('--debug')) {
231
+ console.log(chalk.yellow('DEBUG: Attempting to refresh access token'))
232
+ }
233
+
234
+ // Use fetch directly since this endpoint might not be in the OpenAPI spec
235
+ try {
236
+ const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ Accept: 'application/json',
241
+ },
242
+ body: JSON.stringify({ refresh_token: refreshToken }),
243
+ });
244
+
245
+ // Handle HTTP errors
246
+ if (!response.ok) {
247
+ if (process.argv.includes('--debug')) {
248
+ console.log(
249
+ chalk.yellow(`DEBUG: Token refresh error: HTTP ${response.status} ${response.statusText}`)
250
+ )
251
+ }
252
+
253
+ // Check if the refresh token itself is expired or invalid
254
+ if (response.status === 401 || response.status === 403) {
255
+ console.warn(
256
+ chalk.yellow(
257
+ 'Your refresh token has expired. Please run `berget auth login` again.'
258
+ )
259
+ )
260
+ // Clear tokens if unauthorized - they're invalid
261
+ tokenManager.clearTokens()
262
+ } else {
263
+ console.warn(
264
+ chalk.yellow(
265
+ `Failed to refresh token: ${response.status} ${response.statusText}`
266
+ )
267
+ )
268
+ }
269
+ return false
270
+ }
271
+
272
+ // Parse the response
273
+ const contentType = response.headers.get('content-type')
274
+ if (!contentType || !contentType.includes('application/json')) {
275
+ console.warn(
276
+ chalk.yellow(`Unexpected content type in response: ${contentType}`)
277
+ )
278
+ return false
279
+ }
280
+
281
+ const data = await response.json()
282
+
283
+ // Validate the response data
284
+ if (!data || !data.token) {
285
+ console.warn(
286
+ chalk.yellow(
287
+ 'Invalid token response. Please run `berget auth login` again.'
288
+ )
289
+ )
290
+ return false
291
+ }
292
+
293
+ if (process.argv.includes('--debug')) {
294
+ console.log(chalk.green('DEBUG: Token refreshed successfully'))
295
+ }
296
+
297
+ // Update the token
298
+ tokenManager.updateAccessToken(
299
+ data.token,
300
+ data.expires_in || 3600
301
+ )
302
+
303
+ // If a new refresh token was provided, update that too
304
+ if (data.refresh_token) {
305
+ tokenManager.setTokens(data.token, data.refresh_token, data.expires_in || 3600)
306
+ if (process.argv.includes('--debug')) {
307
+ console.log(chalk.green('DEBUG: Refresh token also updated'))
308
+ }
309
+ }
310
+ } catch (fetchError) {
311
+ console.warn(
312
+ chalk.yellow(
313
+ `Failed to refresh token: ${
314
+ fetchError instanceof Error ? fetchError.message : String(fetchError)
315
+ }`
316
+ )
317
+ )
318
+ return false
319
+ }
320
+
321
+ return true
322
+ } catch (error) {
323
+ console.warn(
324
+ chalk.yellow(
325
+ `Failed to refresh authentication token: ${
326
+ error instanceof Error ? error.message : String(error)
327
+ }`
328
+ )
329
+ )
330
+ return false
331
+ }
133
332
  }