berget 1.3.1 → 2.0.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 (67) hide show
  1. package/.env.example +5 -0
  2. package/.github/workflows/publish.yml +56 -0
  3. package/.github/workflows/test.yml +38 -0
  4. package/AGENTS.md +184 -0
  5. package/README.md +177 -38
  6. package/TODO.md +2 -0
  7. package/blog-post.md +176 -0
  8. package/dist/index.js +11 -8
  9. package/dist/package.json +14 -3
  10. package/dist/src/commands/api-keys.js +4 -2
  11. package/dist/src/commands/chat.js +182 -23
  12. package/dist/src/commands/code.js +1424 -0
  13. package/dist/src/commands/index.js +2 -0
  14. package/dist/src/constants/command-structure.js +12 -0
  15. package/dist/src/schemas/opencode-schema.json +1121 -0
  16. package/dist/src/services/chat-service.js +10 -10
  17. package/dist/src/services/cluster-service.js +1 -1
  18. package/dist/src/utils/default-api-key.js +2 -2
  19. package/dist/src/utils/env-manager.js +86 -0
  20. package/dist/src/utils/error-handler.js +10 -3
  21. package/dist/src/utils/markdown-renderer.js +4 -4
  22. package/dist/src/utils/opencode-validator.js +122 -0
  23. package/dist/src/utils/token-manager.js +2 -2
  24. package/dist/tests/commands/chat.test.js +109 -0
  25. package/dist/tests/commands/code.test.js +414 -0
  26. package/dist/tests/utils/env-manager.test.js +148 -0
  27. package/dist/tests/utils/opencode-validator.test.js +103 -0
  28. package/dist/vitest.config.js +9 -0
  29. package/index.ts +67 -32
  30. package/opencode.json +182 -0
  31. package/package.json +14 -3
  32. package/src/client.ts +20 -20
  33. package/src/commands/api-keys.ts +93 -60
  34. package/src/commands/auth.ts +4 -2
  35. package/src/commands/billing.ts +6 -3
  36. package/src/commands/chat.ts +291 -97
  37. package/src/commands/clusters.ts +2 -2
  38. package/src/commands/code.ts +1696 -0
  39. package/src/commands/index.ts +2 -0
  40. package/src/commands/models.ts +3 -3
  41. package/src/commands/users.ts +2 -2
  42. package/src/constants/command-structure.ts +112 -58
  43. package/src/schemas/opencode-schema.json +991 -0
  44. package/src/services/api-key-service.ts +1 -1
  45. package/src/services/auth-service.ts +27 -25
  46. package/src/services/chat-service.ts +37 -44
  47. package/src/services/cluster-service.ts +5 -5
  48. package/src/services/collaborator-service.ts +3 -3
  49. package/src/services/flux-service.ts +2 -2
  50. package/src/services/helm-service.ts +2 -2
  51. package/src/services/kubectl-service.ts +3 -6
  52. package/src/types/api.d.ts +1032 -1010
  53. package/src/types/json.d.ts +3 -3
  54. package/src/utils/default-api-key.ts +54 -42
  55. package/src/utils/env-manager.ts +98 -0
  56. package/src/utils/error-handler.ts +24 -15
  57. package/src/utils/logger.ts +12 -12
  58. package/src/utils/markdown-renderer.ts +18 -18
  59. package/src/utils/opencode-validator.ts +134 -0
  60. package/src/utils/token-manager.ts +35 -23
  61. package/tests/commands/chat.test.ts +129 -0
  62. package/tests/commands/code.test.ts +505 -0
  63. package/tests/utils/env-manager.test.ts +199 -0
  64. package/tests/utils/opencode-validator.test.ts +118 -0
  65. package/tsconfig.json +8 -8
  66. package/vitest.config.ts +8 -0
  67. package/-27b-it +0 -0
@@ -108,7 +108,7 @@ export class ApiKeyService {
108
108
  '/v1/api-keys/{id}/rotate',
109
109
  {
110
110
  params: { path: { id } },
111
- }
111
+ },
112
112
  )
113
113
  if (error) throw new Error(JSON.stringify(error))
114
114
  return data!
@@ -37,7 +37,7 @@ export class AuthService {
37
37
  const { data: profile, error } = await this.client.GET('/v1/users/me')
38
38
  if (error) {
39
39
  throw new Error(
40
- error ? JSON.stringify(error) : 'Failed to get user profile'
40
+ error ? JSON.stringify(error) : 'Failed to get user profile',
41
41
  )
42
42
  }
43
43
  return profile
@@ -57,14 +57,14 @@ export class AuthService {
57
57
  // Step 1: Initiate device authorization
58
58
  const { data: deviceData, error: deviceError } = await apiClient.POST(
59
59
  '/v1/auth/device',
60
- {}
60
+ {},
61
61
  )
62
62
 
63
63
  if (deviceError || !deviceData) {
64
64
  throw new Error(
65
65
  deviceError
66
66
  ? JSON.stringify(deviceError)
67
- : 'Failed to get device authorization data'
67
+ : 'Failed to get device authorization data',
68
68
  )
69
69
  }
70
70
 
@@ -83,17 +83,17 @@ export class AuthService {
83
83
  chalk.cyan(
84
84
  `1. Open this URL: ${chalk.bold(
85
85
  typedDeviceData.verification_url ||
86
- 'https://keycloak.berget.ai/device'
87
- )}`
88
- )
86
+ 'https://keycloak.berget.ai/device',
87
+ )}`,
88
+ ),
89
89
  )
90
90
  if (!typedDeviceData.verification_url)
91
91
  console.log(
92
92
  chalk.cyan(
93
93
  `2. Enter this code: ${chalk.bold(
94
- typedDeviceData.user_code || ''
95
- )}\n`
96
- )
94
+ typedDeviceData.user_code || '',
95
+ )}\n`,
96
+ ),
97
97
  )
98
98
 
99
99
  // Try to open browser automatically
@@ -104,15 +104,15 @@ export class AuthService {
104
104
  await open(typedDeviceData.verification_url)
105
105
  console.log(
106
106
  chalk.dim(
107
- "Browser opened automatically. If it didn't open, please use the URL above."
108
- )
107
+ "Browser opened automatically. If it didn't open, please use the URL above.",
108
+ ),
109
109
  )
110
110
  }
111
111
  } catch (error) {
112
112
  console.log(
113
113
  chalk.yellow(
114
- 'Could not open browser automatically. Please open the URL manually.'
115
- )
114
+ 'Could not open browser automatically. Please open the URL manually.',
115
+ ),
116
116
  )
117
117
  }
118
118
 
@@ -136,7 +136,7 @@ export class AuthService {
136
136
 
137
137
  // Update spinner
138
138
  process.stdout.write(
139
- `\r${chalk.blue(spinner[spinnerIdx])} Waiting for authentication...`
139
+ `\r${chalk.blue(spinner[spinnerIdx])} Waiting for authentication...`,
140
140
  )
141
141
  spinnerIdx = (spinnerIdx + 1) % spinner.length
142
142
 
@@ -148,7 +148,7 @@ export class AuthService {
148
148
  body: {
149
149
  device_code: deviceCode,
150
150
  },
151
- }
151
+ },
152
152
  )
153
153
 
154
154
  if (tokenError) {
@@ -170,7 +170,7 @@ export class AuthService {
170
170
  // Error or expired
171
171
  if (errorCode === 'EXPIRED_TOKEN') {
172
172
  console.log(
173
- chalk.red('\n\nAuthentication timed out. Please try again.')
173
+ chalk.red('\n\nAuthentication timed out. Please try again.'),
174
174
  )
175
175
  } else if (errorCode !== 'AUTHORIZATION_PENDING') {
176
176
  // Only show error if it's not the expected "still waiting" error
@@ -187,15 +187,15 @@ export class AuthService {
187
187
  // This makes the flow more resilient to temporary issues
188
188
  if (process.env.DEBUG) {
189
189
  console.log(
190
- chalk.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`)
190
+ chalk.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`),
191
191
  )
192
192
  console.log(
193
- chalk.yellow('Continuing to wait for authentication...')
193
+ chalk.yellow('Continuing to wait for authentication...'),
194
194
  )
195
195
  process.stdout.write(
196
196
  `\r${chalk.blue(
197
- spinner[spinnerIdx]
198
- )} Waiting for authentication...`
197
+ spinner[spinnerIdx],
198
+ )} Waiting for authentication...`,
199
199
  )
200
200
  }
201
201
  continue
@@ -219,7 +219,7 @@ export class AuthService {
219
219
  saveAuthToken(
220
220
  typedTokenData.token,
221
221
  typedTokenData.refresh_token || '',
222
- typedTokenData.expires_in || 3600
222
+ typedTokenData.expires_in || 3600,
223
223
  )
224
224
 
225
225
  if (process.argv.includes('--debug')) {
@@ -232,9 +232,9 @@ export class AuthService {
232
232
  refresh_expires_in: typedTokenData.refresh_expires_in,
233
233
  },
234
234
  null,
235
- 2
236
- )
237
- )
235
+ 2,
236
+ ),
237
+ ),
238
238
  )
239
239
  }
240
240
 
@@ -244,7 +244,9 @@ export class AuthService {
244
244
  if (typedTokenData.user) {
245
245
  const user = typedTokenData.user
246
246
  console.log(
247
- chalk.green(`Logged in as ${user.name || user.email || 'User'}`)
247
+ chalk.green(
248
+ `Logged in as ${user.name || user.email || 'User'}`,
249
+ ),
248
250
  )
249
251
  }
250
252
 
@@ -1,4 +1,4 @@
1
- import { createAuthenticatedClient, API_BASE_URL } from '../client'
1
+ import { createAuthenticatedClient } from '../client'
2
2
  import { logger } from '../utils/logger'
3
3
 
4
4
  export interface ChatMessage {
@@ -73,8 +73,8 @@ export class ChatService {
73
73
  : '0 messages',
74
74
  },
75
75
  null,
76
- 2
77
- )
76
+ 2,
77
+ ),
78
78
  )
79
79
  } catch (error) {
80
80
  logger.error('Failed to stringify options:', error)
@@ -121,7 +121,7 @@ export class ChatService {
121
121
 
122
122
  logger.debug(`Default API key data exists: ${!!defaultApiKeyData}`)
123
123
  logger.debug(
124
- `promptForDefaultApiKey returned: ${apiKey ? 'a key' : 'null'}`
124
+ `promptForDefaultApiKey returned: ${apiKey ? 'a key' : 'null'}`,
125
125
  )
126
126
 
127
127
  if (apiKey) {
@@ -130,10 +130,10 @@ export class ChatService {
130
130
  } else {
131
131
  logger.warn('No API key available. You need to either:')
132
132
  logger.warn(
133
- '1. Create an API key with: berget api-keys create --name "My Key"'
133
+ '1. Create an API key with: berget api-keys create --name "My Key"',
134
134
  )
135
135
  logger.warn(
136
- '2. Set a default API key with: berget api-keys set-default <id>'
136
+ '2. Set a default API key with: berget api-keys set-default <id>',
137
137
  )
138
138
  logger.warn('3. Provide an API key with the --api-key option')
139
139
  logger.warn('4. Set the BERGET_API_KEY environment variable')
@@ -141,7 +141,7 @@ export class ChatService {
141
141
  logger.warn(' export BERGET_API_KEY=your_api_key_here')
142
142
  logger.warn(' # or for a single command:')
143
143
  logger.warn(
144
- ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it'
144
+ ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it',
145
145
  )
146
146
  throw new Error('No API key provided and no default API key set')
147
147
  }
@@ -159,7 +159,7 @@ export class ChatService {
159
159
  logger.error(error.message)
160
160
  }
161
161
  logger.warn(
162
- 'Please create an API key with: berget api-keys create --name "My Key"'
162
+ 'Please create an API key with: berget api-keys create --name "My Key"',
163
163
  )
164
164
  throw new Error('Failed to get API key')
165
165
  }
@@ -179,8 +179,8 @@ export class ChatService {
179
179
  apiKey: optionsCopy.apiKey ? '***' : undefined, // Hide the actual API key in debug output
180
180
  },
181
181
  null,
182
- 2
183
- )
182
+ 2,
183
+ ),
184
184
  )
185
185
 
186
186
  return this.executeCompletion(optionsCopy, headers)
@@ -214,7 +214,7 @@ export class ChatService {
214
214
  */
215
215
  private async executeCompletion(
216
216
  options: ChatCompletionOptions,
217
- headers: Record<string, string> = {}
217
+ headers: Record<string, string> = {},
218
218
  ): Promise<any> {
219
219
  try {
220
220
  // If an API key is provided, use it for this request
@@ -236,15 +236,15 @@ export class ChatService {
236
236
  : '0 messages',
237
237
  },
238
238
  null,
239
- 2
240
- )
239
+ 2,
240
+ ),
241
241
  )
242
242
 
243
243
  // Handle streaming responses differently
244
244
  if (requestOptions.stream && onChunk) {
245
245
  return await this.handleStreamingResponse(
246
246
  { ...requestOptions, onChunk },
247
- headers
247
+ headers,
248
248
  )
249
249
  } else {
250
250
  // Ensure model is always defined for the API call
@@ -282,7 +282,7 @@ export class ChatService {
282
282
  requestError instanceof Error
283
283
  ? requestError.message
284
284
  : String(requestError)
285
- }`
285
+ }`,
286
286
  )
287
287
  throw requestError
288
288
  }
@@ -295,10 +295,10 @@ export class ChatService {
295
295
  // We've exhausted all options for getting an API key
296
296
  logger.warn('No API key available. You need to either:')
297
297
  logger.warn(
298
- '1. Create an API key with: berget api-keys create --name "My Key"'
298
+ '1. Create an API key with: berget api-keys create --name "My Key"',
299
299
  )
300
300
  logger.warn(
301
- '2. Set a default API key with: berget api-keys set-default <id>'
301
+ '2. Set a default API key with: berget api-keys set-default <id>',
302
302
  )
303
303
  logger.warn('3. Provide an API key with the --api-key option')
304
304
  logger.warn('4. Set the BERGET_API_KEY environment variable')
@@ -306,10 +306,10 @@ export class ChatService {
306
306
  logger.warn(' export BERGET_API_KEY=your_api_key_here')
307
307
  logger.warn(' # or for a single command:')
308
308
  logger.warn(
309
- ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it'
309
+ ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it',
310
310
  )
311
311
  throw new Error(
312
- 'No API key available. Please provide an API key or set a default API key.'
312
+ 'No API key available. Please provide an API key or set a default API key.',
313
313
  )
314
314
  }
315
315
 
@@ -321,30 +321,17 @@ export class ChatService {
321
321
  */
322
322
  private async handleStreamingResponse(
323
323
  options: any,
324
- headers: Record<string, string>
324
+ headers: Record<string, string>,
325
325
  ): Promise<any> {
326
- logger.debug('Handling streaming response')
327
-
328
- // Create URL with query parameters
329
- const url = new URL(`${API_BASE_URL}/v1/chat/completions`)
330
-
331
- // Debug the headers and options
332
- logger.debug('Streaming headers:')
333
- logger.debug(JSON.stringify(headers, null, 2))
334
-
335
- logger.debug('Streaming options:')
336
- logger.debug(
337
- JSON.stringify(
338
- {
339
- ...options,
340
- onChunk: options.onChunk ? 'function present' : 'no function',
341
- },
342
- null,
343
- 2
344
- )
345
- )
326
+ // Use the same base URL as the client
327
+ const baseUrl = process.env.API_BASE_URL || 'https://api.berget.ai'
328
+ const url = new URL(`${baseUrl}/v1/chat/completions`)
346
329
 
347
330
  try {
331
+ logger.debug(`Making streaming request to: ${url.toString()}`)
332
+ logger.debug(`Headers:`, JSON.stringify(headers, null, 2))
333
+ logger.debug(`Body:`, JSON.stringify(options, null, 2))
334
+
348
335
  // Make fetch request directly to handle streaming
349
336
  const response = await fetch(url.toString(), {
350
337
  method: 'POST',
@@ -356,14 +343,20 @@ export class ChatService {
356
343
  body: JSON.stringify(options),
357
344
  })
358
345
 
346
+ logger.debug(`Response status: ${response.status}`)
347
+ logger.debug(
348
+ `Response headers:`,
349
+ JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2),
350
+ )
351
+
359
352
  if (!response.ok) {
360
353
  const errorText = await response.text()
361
354
  logger.error(
362
- `Stream request failed: ${response.status} ${response.statusText}`
355
+ `Stream request failed: ${response.status} ${response.statusText}`,
363
356
  )
364
- logger.debug(`Error response: ${errorText}`)
357
+ logger.error(`Error response: ${errorText}`)
365
358
  throw new Error(
366
- `Stream request failed: ${response.status} ${response.statusText}`
359
+ `Stream request failed: ${response.status} ${response.statusText} - ${errorText}`,
367
360
  )
368
361
  }
369
362
 
@@ -440,7 +433,7 @@ export class ChatService {
440
433
  logger.error(
441
434
  `Streaming error: ${
442
435
  error instanceof Error ? error.message : String(error)
443
- }`
436
+ }`,
444
437
  )
445
438
  throw error
446
439
  }
@@ -16,10 +16,10 @@ export interface Cluster {
16
16
  export class ClusterService {
17
17
  private static instance: ClusterService
18
18
  private client = createAuthenticatedClient()
19
-
19
+
20
20
  // Command group name for this service
21
21
  public static readonly COMMAND_GROUP = COMMAND_GROUPS.CLUSTERS
22
-
22
+
23
23
  // Subcommands for this service
24
24
  public static readonly COMMANDS = SUBCOMMANDS.CLUSTERS
25
25
 
@@ -42,7 +42,7 @@ export class ClusterService {
42
42
  '/v1/clusters/{clusterId}/usage',
43
43
  {
44
44
  params: { path: { clusterId } },
45
- }
45
+ },
46
46
  )
47
47
  if (error) throw new Error(JSON.stringify(error))
48
48
  return data
@@ -66,7 +66,7 @@ export class ClusterService {
66
66
  throw error
67
67
  }
68
68
  }
69
-
69
+
70
70
  /**
71
71
  * Get detailed information about a cluster
72
72
  * Command: berget clusters describe
@@ -76,7 +76,7 @@ export class ClusterService {
76
76
  // This is a placeholder since the API doesn't have a specific endpoint
77
77
  // In a real implementation, this would call a specific endpoint
78
78
  const clusters = await this.list()
79
- return clusters.find(cluster => cluster.id === clusterId) || null
79
+ return clusters.find((cluster) => cluster.id === clusterId) || null
80
80
  } catch (error) {
81
81
  console.error('Failed to describe cluster:', error)
82
82
  throw error
@@ -14,10 +14,10 @@ export interface Collaborator {
14
14
  export class CollaboratorService {
15
15
  private static instance: CollaboratorService
16
16
  private client = createAuthenticatedClient()
17
-
17
+
18
18
  // Command group name for this service
19
19
  public static readonly COMMAND_GROUP = COMMAND_GROUPS.USERS
20
-
20
+
21
21
  // Subcommands for this service
22
22
  public static readonly COMMANDS = SUBCOMMANDS.USERS
23
23
 
@@ -37,7 +37,7 @@ export class CollaboratorService {
37
37
  */
38
38
  public async invite(
39
39
  clusterId: string,
40
- githubUsername: string
40
+ githubUsername: string,
41
41
  ): Promise<Collaborator[]> {
42
42
  throw new Error('This functionality is not available in the API')
43
43
  }
@@ -20,10 +20,10 @@ export interface FluxBootstrapOptions {
20
20
  export class FluxService {
21
21
  private static instance: FluxService
22
22
  private client = createAuthenticatedClient()
23
-
23
+
24
24
  // Command group name for this service
25
25
  public static readonly COMMAND_GROUP = COMMAND_GROUPS.FLUX
26
-
26
+
27
27
  // Subcommands for this service
28
28
  public static readonly COMMANDS = SUBCOMMANDS.FLUX
29
29
 
@@ -20,10 +20,10 @@ export interface HelmInstallOptions {
20
20
  export class HelmService {
21
21
  private static instance: HelmService
22
22
  private client = createAuthenticatedClient()
23
-
23
+
24
24
  // Command group name for this service
25
25
  public static readonly COMMAND_GROUP = COMMAND_GROUPS.HELM
26
-
26
+
27
27
  // Subcommands for this service
28
28
  public static readonly COMMANDS = SUBCOMMANDS.HELM
29
29
 
@@ -8,10 +8,10 @@ import { COMMAND_GROUPS, SUBCOMMANDS } from '../constants/command-structure'
8
8
  export class KubectlService {
9
9
  private static instance: KubectlService
10
10
  private client = createAuthenticatedClient()
11
-
11
+
12
12
  // Command group name for this service
13
13
  public static readonly COMMAND_GROUP = COMMAND_GROUPS.KUBECTL
14
-
14
+
15
15
  // Subcommands for this service
16
16
  public static readonly COMMANDS = SUBCOMMANDS.KUBECTL
17
17
 
@@ -47,10 +47,7 @@ export class KubectlService {
47
47
  * Command: berget kubectl get
48
48
  * This endpoint is not available in the API
49
49
  */
50
- public async get(
51
- resource: string,
52
- namespace?: string
53
- ): Promise<any[]> {
50
+ public async get(resource: string, namespace?: string): Promise<any[]> {
54
51
  throw new Error('This functionality is not available in the API')
55
52
  }
56
53
  }