berget 1.0.0 → 1.1.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 (37) hide show
  1. package/README.md +92 -0
  2. package/dist/index.js +7 -471
  3. package/dist/src/client.js +193 -102
  4. package/dist/src/commands/api-keys.js +271 -0
  5. package/dist/src/commands/auth.js +65 -0
  6. package/dist/src/commands/autocomplete.js +24 -0
  7. package/dist/src/commands/billing.js +53 -0
  8. package/dist/src/commands/chat.js +276 -0
  9. package/dist/src/commands/clusters.js +69 -0
  10. package/dist/src/commands/index.js +25 -0
  11. package/dist/src/commands/models.js +69 -0
  12. package/dist/src/commands/users.js +43 -0
  13. package/dist/src/constants/command-structure.js +14 -0
  14. package/dist/src/services/auth-service.js +49 -47
  15. package/dist/src/services/chat-service.js +177 -0
  16. package/dist/src/utils/config-checker.js +50 -0
  17. package/dist/src/utils/default-api-key.js +111 -0
  18. package/dist/src/utils/token-manager.js +165 -0
  19. package/index.ts +5 -566
  20. package/package.json +6 -1
  21. package/src/client.ts +262 -80
  22. package/src/commands/api-keys.ts +364 -0
  23. package/src/commands/auth.ts +58 -0
  24. package/src/commands/autocomplete.ts +19 -0
  25. package/src/commands/billing.ts +41 -0
  26. package/src/commands/chat.ts +345 -0
  27. package/src/commands/clusters.ts +65 -0
  28. package/src/commands/index.ts +23 -0
  29. package/src/commands/models.ts +63 -0
  30. package/src/commands/users.ts +37 -0
  31. package/src/constants/command-structure.ts +16 -0
  32. package/src/services/auth-service.ts +90 -50
  33. package/src/services/chat-service.ts +177 -0
  34. package/src/types/api.d.ts +58 -192
  35. package/src/utils/config-checker.ts +23 -0
  36. package/src/utils/default-api-key.ts +94 -0
  37. package/src/utils/token-manager.ts +150 -0
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,244 @@ 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
+ const isAuthError =
130
+ // Standard 401 Unauthorized
131
+ (typeof result.error === 'object' && result.error.status === 401) ||
132
+ // OAuth specific errors
133
+ (result.error.error &&
134
+ (result.error.error.code === 'invalid_token' ||
135
+ result.error.error.code === 'token_expired' ||
136
+ result.error.error.message === 'Invalid API key' ||
137
+ result.error.error.message?.toLowerCase().includes('token') ||
138
+ result.error.error.message?.toLowerCase().includes('unauthorized'))) ||
139
+ // Message-based detection as fallback
140
+ (typeof result.error === 'string' &&
141
+ (result.error.toLowerCase().includes('unauthorized') ||
142
+ result.error.toLowerCase().includes('token') ||
143
+ result.error.toLowerCase().includes('auth')))
144
+
145
+ if (isAuthError && tokenManager.getRefreshToken()) {
146
+ if (process.argv.includes('--debug')) {
147
+ console.log(
148
+ chalk.yellow(
149
+ 'DEBUG: Auth error detected, attempting token refresh'
150
+ )
151
+ )
152
+ console.log(
153
+ chalk.yellow(
154
+ `DEBUG: Error details: ${JSON.stringify(
155
+ result.error,
156
+ null,
157
+ 2
158
+ )}`
159
+ )
160
+ )
161
+ }
162
+
163
+ const refreshed = await refreshAccessToken(tokenManager)
164
+ if (refreshed) {
165
+ if (process.argv.includes('--debug')) {
166
+ console.log(
167
+ chalk.green(
168
+ 'DEBUG: Token refreshed successfully, retrying request'
169
+ )
170
+ )
171
+ }
172
+
173
+ // Update the Authorization header with the new token
174
+ if (!args[1]) args[1] = {}
175
+ if (!args[1].headers) args[1].headers = {}
176
+ args[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`
177
+
178
+ // Retry the request
179
+ return await (target[prop as keyof typeof target] as Function)(
180
+ ...args
181
+ )
182
+ } else {
183
+ if (process.argv.includes('--debug')) {
184
+ console.log(chalk.red('DEBUG: Token refresh failed'))
185
+ }
186
+
187
+ // Add a more helpful error message for users
188
+ if (typeof result.error === 'object') {
189
+ result.error.userMessage = 'Your session has expired. Please run `berget auth login` to log in again.'
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ return result
196
+ }
197
+ }
198
+
199
+ // For other properties, just return the original
200
+ return target[prop as keyof typeof target]
201
+ },
202
+ })
203
+ }
204
+
205
+ // Helper function to refresh the access token
206
+ async function refreshAccessToken(
207
+ tokenManager: TokenManager
208
+ ): Promise<boolean> {
209
+ try {
210
+ const refreshToken = tokenManager.getRefreshToken()
211
+ if (!refreshToken) return false
212
+
213
+ if (process.argv.includes('--debug')) {
214
+ console.log(chalk.yellow('DEBUG: Attempting to refresh access token'))
215
+ }
216
+
217
+ // Use fetch directly since this endpoint might not be in the OpenAPI spec
218
+ try {
219
+ const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ Accept: 'application/json',
224
+ },
225
+ body: JSON.stringify({ refresh_token: refreshToken }),
226
+ });
227
+
228
+ // Handle HTTP errors
229
+ if (!response.ok) {
230
+ if (process.argv.includes('--debug')) {
231
+ console.log(
232
+ chalk.yellow(`DEBUG: Token refresh error: HTTP ${response.status} ${response.statusText}`)
233
+ )
234
+ }
235
+
236
+ // Check if the refresh token itself is expired or invalid
237
+ if (response.status === 401 || response.status === 403) {
238
+ console.warn(
239
+ chalk.yellow(
240
+ 'Your refresh token has expired. Please run `berget auth login` again.'
241
+ )
242
+ )
243
+ // Clear tokens if unauthorized - they're invalid
244
+ tokenManager.clearTokens()
245
+ } else {
246
+ console.warn(
247
+ chalk.yellow(
248
+ `Failed to refresh token: ${response.status} ${response.statusText}`
249
+ )
250
+ )
251
+ }
252
+ return false
253
+ }
254
+
255
+ // Parse the response
256
+ const contentType = response.headers.get('content-type')
257
+ if (!contentType || !contentType.includes('application/json')) {
258
+ console.warn(
259
+ chalk.yellow(`Unexpected content type in response: ${contentType}`)
260
+ )
261
+ return false
262
+ }
263
+
264
+ const data = await response.json()
265
+
266
+ // Validate the response data
267
+ if (!data || !data.token) {
268
+ console.warn(
269
+ chalk.yellow(
270
+ 'Invalid token response. Please run `berget auth login` again.'
271
+ )
272
+ )
273
+ return false
274
+ }
275
+
276
+ if (process.argv.includes('--debug')) {
277
+ console.log(chalk.green('DEBUG: Token refreshed successfully'))
278
+ }
279
+
280
+ // Update the token
281
+ tokenManager.updateAccessToken(
282
+ data.token,
283
+ data.expires_in || 3600
284
+ )
285
+
286
+ // If a new refresh token was provided, update that too
287
+ if (data.refresh_token) {
288
+ tokenManager.setTokens(data.token, data.refresh_token, data.expires_in || 3600)
289
+ if (process.argv.includes('--debug')) {
290
+ console.log(chalk.green('DEBUG: Refresh token also updated'))
291
+ }
292
+ }
293
+ } catch (fetchError) {
294
+ console.warn(
295
+ chalk.yellow(
296
+ `Failed to refresh token: ${
297
+ fetchError instanceof Error ? fetchError.message : String(fetchError)
298
+ }`
299
+ )
300
+ )
301
+ return false
302
+ }
303
+
304
+ return true
305
+ } catch (error) {
306
+ console.warn(
307
+ chalk.yellow(
308
+ `Failed to refresh authentication token: ${
309
+ error instanceof Error ? error.message : String(error)
310
+ }`
311
+ )
312
+ )
313
+ return false
314
+ }
133
315
  }