berget 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.
Files changed (145) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/.github/workflows/test.yml +10 -4
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +7 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +10 -11
  9. package/dist/package.json +30 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +97 -117
  20. package/dist/src/commands/api-keys.js +75 -90
  21. package/dist/src/commands/auth.js +7 -16
  22. package/dist/src/commands/autocomplete.js +1 -1
  23. package/dist/src/commands/billing.js +6 -17
  24. package/dist/src/commands/chat.js +68 -101
  25. package/dist/src/commands/clusters.js +9 -18
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
  33. package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
  34. package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
  36. package/dist/src/commands/code/auth-sync.js +270 -0
  37. package/dist/src/commands/code/errors.js +12 -9
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +387 -281
  40. package/dist/src/commands/code.js +205 -332
  41. package/dist/src/commands/index.js +5 -5
  42. package/dist/src/commands/models.js +6 -17
  43. package/dist/src/commands/users.js +5 -16
  44. package/dist/src/constants/command-structure.js +104 -104
  45. package/dist/src/services/api-key-service.js +132 -157
  46. package/dist/src/services/auth-service.js +89 -342
  47. package/dist/src/services/browser-auth.js +268 -0
  48. package/dist/src/services/chat-service.js +371 -401
  49. package/dist/src/services/cluster-service.js +47 -62
  50. package/dist/src/services/collaborator-service.js +10 -25
  51. package/dist/src/services/flux-service.js +14 -29
  52. package/dist/src/services/helm-service.js +10 -25
  53. package/dist/src/services/kubectl-service.js +16 -33
  54. package/dist/src/utils/config-checker.js +3 -3
  55. package/dist/src/utils/config-loader.js +95 -95
  56. package/dist/src/utils/default-api-key.js +124 -134
  57. package/dist/src/utils/env-manager.js +55 -66
  58. package/dist/src/utils/error-handler.js +20 -21
  59. package/dist/src/utils/logger.js +72 -65
  60. package/dist/src/utils/markdown-renderer.js +27 -27
  61. package/dist/src/utils/opencode-validator.js +63 -68
  62. package/dist/src/utils/token-manager.js +74 -45
  63. package/dist/tests/commands/chat.test.js +16 -25
  64. package/dist/tests/commands/code.test.js +95 -104
  65. package/dist/tests/utils/config-loader.test.js +48 -48
  66. package/dist/tests/utils/env-manager.test.js +43 -52
  67. package/dist/tests/utils/opencode-validator.test.js +22 -21
  68. package/dist/vitest.config.js +1 -1
  69. package/eslint.config.mjs +67 -0
  70. package/index.ts +35 -42
  71. package/package.json +30 -2
  72. package/src/agents/app.ts +27 -0
  73. package/src/agents/backend.ts +24 -0
  74. package/src/agents/devops.ts +33 -0
  75. package/src/agents/frontend.ts +24 -0
  76. package/src/agents/fullstack.ts +24 -0
  77. package/src/agents/index.ts +73 -0
  78. package/src/agents/quality.ts +69 -0
  79. package/src/agents/security.ts +26 -0
  80. package/src/agents/types.ts +17 -0
  81. package/src/client.ts +118 -152
  82. package/src/commands/api-keys.ts +241 -333
  83. package/src/commands/auth.ts +22 -27
  84. package/src/commands/autocomplete.ts +9 -9
  85. package/src/commands/billing.ts +20 -24
  86. package/src/commands/chat.ts +248 -338
  87. package/src/commands/clusters.ts +27 -26
  88. package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
  89. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  90. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  91. package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
  92. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  93. package/src/commands/code/__tests__/fake-prompter.ts +116 -77
  94. package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
  95. package/src/commands/code/adapters/clack-prompter.ts +53 -39
  96. package/src/commands/code/adapters/fs-file-store.ts +32 -27
  97. package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
  98. package/src/commands/code/auth-sync.ts +329 -0
  99. package/src/commands/code/errors.ts +18 -18
  100. package/src/commands/code/ports/auth-services.ts +14 -0
  101. package/src/commands/code/ports/command-runner.ts +8 -4
  102. package/src/commands/code/ports/file-store.ts +5 -4
  103. package/src/commands/code/ports/prompter.ts +24 -18
  104. package/src/commands/code/setup.ts +570 -340
  105. package/src/commands/code.ts +338 -539
  106. package/src/commands/index.ts +20 -19
  107. package/src/commands/models.ts +28 -32
  108. package/src/commands/users.ts +15 -21
  109. package/src/constants/command-structure.ts +134 -157
  110. package/src/services/api-key-service.ts +105 -122
  111. package/src/services/auth-service.ts +99 -345
  112. package/src/services/browser-auth.ts +296 -0
  113. package/src/services/chat-service.ts +265 -299
  114. package/src/services/cluster-service.ts +42 -45
  115. package/src/services/collaborator-service.ts +14 -19
  116. package/src/services/flux-service.ts +23 -25
  117. package/src/services/helm-service.ts +19 -21
  118. package/src/services/kubectl-service.ts +17 -19
  119. package/src/types/api.d.ts +1905 -1907
  120. package/src/types/json.d.ts +2 -2
  121. package/src/utils/config-checker.ts +10 -10
  122. package/src/utils/config-loader.ts +162 -178
  123. package/src/utils/default-api-key.ts +114 -125
  124. package/src/utils/env-manager.ts +53 -57
  125. package/src/utils/error-handler.ts +61 -56
  126. package/src/utils/logger.ts +79 -73
  127. package/src/utils/markdown-renderer.ts +31 -31
  128. package/src/utils/opencode-validator.ts +85 -89
  129. package/src/utils/token-manager.ts +108 -87
  130. package/templates/agents/app.md +1 -0
  131. package/templates/agents/backend.md +1 -0
  132. package/templates/agents/devops.md +2 -0
  133. package/templates/agents/frontend.md +1 -0
  134. package/templates/agents/fullstack.md +1 -0
  135. package/templates/agents/quality.md +45 -40
  136. package/templates/agents/security.md +1 -0
  137. package/tests/commands/chat.test.ts +53 -62
  138. package/tests/commands/code.test.ts +265 -310
  139. package/tests/utils/config-loader.test.ts +189 -188
  140. package/tests/utils/env-manager.test.ts +110 -113
  141. package/tests/utils/opencode-validator.test.ts +52 -56
  142. package/tsconfig.json +4 -3
  143. package/vitest.config.ts +3 -3
  144. package/AGENTS.md +0 -374
  145. package/TODO.md +0 -19
@@ -1,20 +1,20 @@
1
- import { createAuthenticatedClient } from '../client'
2
- import { logger } from '../utils/logger'
1
+ import { createAuthenticatedClient } from '../client';
2
+ import { logger } from '../utils/logger';
3
3
 
4
- export interface ChatMessage {
5
- role: 'system' | 'user' | 'assistant'
6
- content: string
4
+ export interface ChatCompletionOptions {
5
+ apiKey?: string;
6
+ max_tokens?: number;
7
+ messages: ChatMessage[];
8
+ model?: string;
9
+ onChunk?: (chunk: any) => void;
10
+ stream?: boolean;
11
+ temperature?: number;
12
+ top_p?: number;
7
13
  }
8
14
 
9
- export interface ChatCompletionOptions {
10
- model?: string
11
- messages: ChatMessage[]
12
- temperature?: number
13
- max_tokens?: number
14
- stream?: boolean
15
- top_p?: number
16
- apiKey?: string
17
- onChunk?: (chunk: any) => void
15
+ export interface ChatMessage {
16
+ content: string;
17
+ role: 'assistant' | 'system' | 'user';
18
18
  }
19
19
 
20
20
  /**
@@ -22,25 +22,25 @@ export interface ChatCompletionOptions {
22
22
  * Command group: chat
23
23
  */
24
24
  export class ChatService {
25
- private static instance: ChatService
26
- private client = createAuthenticatedClient()
27
-
28
25
  // Command group name for this service
29
- public static readonly COMMAND_GROUP = 'chat'
30
-
26
+ public static readonly COMMAND_GROUP = 'chat';
31
27
  // Subcommands for this service
32
28
  public static readonly COMMANDS = {
33
- RUN: 'run',
34
29
  LIST: 'list',
35
- }
30
+ RUN: 'run',
31
+ };
32
+
33
+ private static instance: ChatService;
34
+
35
+ private client = createAuthenticatedClient();
36
36
 
37
37
  private constructor() {}
38
38
 
39
39
  public static getInstance(): ChatService {
40
40
  if (!ChatService.instance) {
41
- ChatService.instance = new ChatService()
41
+ ChatService.instance = new ChatService();
42
42
  }
43
- return ChatService.instance
43
+ return ChatService.instance;
44
44
  }
45
45
 
46
46
  /**
@@ -49,19 +49,19 @@ export class ChatService {
49
49
  */
50
50
  public async createCompletion(options: ChatCompletionOptions): Promise<any> {
51
51
  try {
52
- logger.debug('Starting createCompletion method')
52
+ logger.debug('Starting createCompletion method');
53
53
 
54
54
  // Initialize options if undefined
55
- const optionsCopy = options ? { ...options } : { messages: [] }
55
+ const optionsCopy = options ? { ...options } : { messages: [] };
56
56
 
57
57
  // Check if messages are defined
58
58
  if (!optionsCopy.messages || !Array.isArray(optionsCopy.messages)) {
59
- logger.error('messages is undefined or not an array')
60
- optionsCopy.messages = []
59
+ logger.error('messages is undefined or not an array');
60
+ optionsCopy.messages = [];
61
61
  }
62
62
 
63
63
  // Log the options object
64
- logger.debug('Starting createCompletion with options:')
64
+ logger.debug('Starting createCompletion with options:');
65
65
  try {
66
66
  logger.debug(
67
67
  JSON.stringify(
@@ -75,120 +75,108 @@ export class ChatService {
75
75
  null,
76
76
  2,
77
77
  ),
78
- )
78
+ );
79
79
  } catch (error) {
80
- logger.error('Failed to stringify options:', error)
80
+ logger.error('Failed to stringify options:', error);
81
81
  }
82
82
 
83
- const headers: Record<string, string> = {}
83
+ const headers: Record<string, string> = {};
84
84
 
85
85
  // First try to use the authenticated client (with refresh token support)
86
86
  // Only fall back to API key flow if explicitly requested or no auth tokens available
87
- const { TokenManager } = await import('../utils/token-manager')
88
- const tokenManagerInstance = TokenManager.getInstance()
89
- const hasValidAuth = tokenManagerInstance.getAccessToken() && !tokenManagerInstance.isTokenExpired()
90
-
91
- const envApiKeyForAuth = process.env.BERGET_API_KEY
92
- const hasExplicitApiKey = !!optionsCopy.apiKey || !!envApiKeyForAuth
87
+ const { TokenManager } = await import('../utils/token-manager');
88
+ const tokenManagerInstance = TokenManager.getInstance();
89
+ const hasValidAuth =
90
+ tokenManagerInstance.getAccessToken() && !tokenManagerInstance.isTokenExpired();
91
+
92
+ const environmentApiKeyForAuth = process.env.BERGET_API_KEY;
93
+ const hasExplicitApiKey = !!optionsCopy.apiKey || !!environmentApiKeyForAuth;
93
94
 
94
95
  // If we have valid auth tokens and no explicit API key, use authenticated client
95
96
  if (hasValidAuth && !hasExplicitApiKey) {
96
- logger.debug('Using authenticated client with refresh token support')
97
+ logger.debug('Using authenticated client with refresh token support');
97
98
  // Create a copy without apiKey to let the authenticated client handle auth automatically
98
- const { apiKey, ...optionsWithoutKey } = optionsCopy
99
- return this.executeCompletion(optionsWithoutKey, {})
99
+ const { apiKey: _apiKey, ...optionsWithoutKey } = optionsCopy;
100
+ return this.executeCompletion(optionsWithoutKey, {});
100
101
  }
101
102
 
102
103
  // Check for environment variables first - prioritize this over everything else
103
- const envApiKey = process.env.BERGET_API_KEY
104
- if (envApiKey) {
105
- logger.debug('Using API key from BERGET_API_KEY environment variable')
106
- optionsCopy.apiKey = envApiKey
104
+ const environmentApiKey = process.env.BERGET_API_KEY;
105
+ if (environmentApiKey) {
106
+ logger.debug('Using API key from BERGET_API_KEY environment variable');
107
+ optionsCopy.apiKey = environmentApiKey;
107
108
  // Skip the default API key logic if we already have a key
108
- return this.executeCompletion(optionsCopy, headers)
109
+ return this.executeCompletion(optionsCopy, headers);
109
110
  }
110
111
  // If API key is already provided, use it directly
111
112
  else if (optionsCopy.apiKey) {
112
- logger.debug('Using API key provided in options')
113
+ logger.debug('Using API key provided in options');
113
114
  // Skip the default API key logic if we already have a key
114
- return this.executeCompletion(optionsCopy, headers)
115
+ return this.executeCompletion(optionsCopy, headers);
115
116
  }
116
117
  // Only try to get the default API key if no API key is provided and no env var is set
117
118
  else {
118
- logger.debug('No API key provided, trying to get default')
119
+ logger.debug('No API key provided, trying to get default');
119
120
 
120
121
  try {
121
122
  // Import the DefaultApiKeyManager directly
122
- logger.debug('Importing DefaultApiKeyManager')
123
+ logger.debug('Importing DefaultApiKeyManager');
123
124
 
124
- const DefaultApiKeyManager = (
125
- await import('../utils/default-api-key')
126
- ).DefaultApiKeyManager
127
- const defaultApiKeyManager = DefaultApiKeyManager.getInstance()
125
+ const { DefaultApiKeyManager } = await import('../utils/default-api-key');
126
+ const defaultApiKeyManager = DefaultApiKeyManager.getInstance();
128
127
 
129
- logger.debug('Got DefaultApiKeyManager instance')
128
+ logger.debug('Got DefaultApiKeyManager instance');
130
129
 
131
130
  // Try to get the default API key
132
- logger.debug('Calling promptForDefaultApiKey')
131
+ logger.debug('Calling promptForDefaultApiKey');
133
132
 
134
- const defaultApiKeyData = defaultApiKeyManager.getDefaultApiKeyData()
133
+ const defaultApiKeyData = defaultApiKeyManager.getDefaultApiKeyData();
135
134
  const apiKey =
136
- defaultApiKeyData?.key ||
137
- (await defaultApiKeyManager.promptForDefaultApiKey())
135
+ defaultApiKeyData?.key || (await defaultApiKeyManager.promptForDefaultApiKey());
138
136
 
139
- logger.debug(`Default API key data exists: ${!!defaultApiKeyData}`)
140
- logger.debug(
141
- `promptForDefaultApiKey returned: ${apiKey ? 'a key' : 'null'}`,
142
- )
137
+ logger.debug(`Default API key data exists: ${!!defaultApiKeyData}`);
138
+ logger.debug(`promptForDefaultApiKey returned: ${apiKey ? 'a key' : 'null'}`);
143
139
 
144
140
  if (apiKey) {
145
- logger.debug('Using API key from default API key manager')
146
- optionsCopy.apiKey = apiKey
141
+ logger.debug('Using API key from default API key manager');
142
+ optionsCopy.apiKey = apiKey;
147
143
  } else {
148
- logger.warn('No API key available. You need to either:')
149
- logger.warn(
150
- '1. Create an API key with: berget api-keys create --name "My Key"',
151
- )
152
- logger.warn(
153
- '2. Set a default API key with: berget api-keys set-default <id>',
154
- )
155
- logger.warn('3. Provide an API key with the --api-key option')
156
- logger.warn('4. Set the BERGET_API_KEY environment variable')
157
- logger.warn('\nExample:')
158
- logger.warn(' export BERGET_API_KEY=your_api_key_here')
159
- logger.warn(' # or for a single command:')
160
- logger.warn(
161
- ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it',
162
- )
163
- throw new Error('No API key provided and no default API key set')
144
+ logger.warn('No API key available. You need to either:');
145
+ logger.warn('1. Create an API key with: berget api-keys create --name "My Key"');
146
+ logger.warn('2. Set a default API key with: berget api-keys set-default <id>');
147
+ logger.warn('3. Provide an API key with the --api-key option');
148
+ logger.warn('4. Set the BERGET_API_KEY environment variable');
149
+ logger.warn('\nExample:');
150
+ logger.warn(' export BERGET_API_KEY=your_api_key_here');
151
+ logger.warn(' # or for a single command:');
152
+ logger.warn(' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it');
153
+ throw new Error('No API key provided and no default API key set');
164
154
  }
165
155
 
166
156
  // Set the API key in the options
167
- logger.debug('Setting API key in options')
157
+ logger.debug('Setting API key in options');
168
158
 
169
159
  // Only set the API key if it's not null
170
160
  if (apiKey) {
171
- optionsCopy.apiKey = apiKey
161
+ optionsCopy.apiKey = apiKey;
172
162
  }
173
163
  } catch (error) {
174
- logger.error('Error getting API key:')
164
+ logger.error('Error getting API key:');
175
165
  if (error instanceof Error) {
176
- logger.error(error.message)
166
+ logger.error(error.message);
177
167
  }
178
- logger.warn(
179
- 'Please create an API key with: berget api-keys create --name "My Key"',
180
- )
181
- throw new Error('Failed to get API key')
168
+ logger.warn('Please create an API key with: berget api-keys create --name "My Key"');
169
+ throw new Error('Failed to get API key');
182
170
  }
183
171
  }
184
172
 
185
173
  // Set default model if not provided
186
174
  if (!optionsCopy.model) {
187
- logger.debug('No model specified, using default: google/gemma-3-27b-it')
188
- optionsCopy.model = 'google/gemma-3-27b-it'
175
+ logger.debug('No model specified, using default: google/gemma-3-27b-it');
176
+ optionsCopy.model = 'google/gemma-3-27b-it';
189
177
  }
190
178
 
191
- logger.debug('Chat completion options:')
179
+ logger.debug('Chat completion options:');
192
180
  logger.debug(
193
181
  JSON.stringify(
194
182
  {
@@ -198,28 +186,77 @@ export class ChatService {
198
186
  null,
199
187
  2,
200
188
  ),
201
- )
189
+ );
202
190
 
203
- return this.executeCompletion(optionsCopy, headers)
191
+ return this.executeCompletion(optionsCopy, headers);
204
192
  } catch (error) {
205
193
  // Improved error handling
206
- let errorMessage = 'Failed to create chat completion'
194
+ let errorMessage = 'Failed to create chat completion';
207
195
 
208
196
  if (error instanceof Error) {
209
197
  try {
210
198
  // Try to parse the error message as JSON
211
- const parsedError = JSON.parse(error.message)
199
+ const parsedError = JSON.parse(error.message);
212
200
  if (parsedError.error && parsedError.error.message) {
213
- errorMessage = `Chat error: ${parsedError.error.message}`
201
+ errorMessage = `Chat error: ${parsedError.error.message}`;
202
+ }
203
+ } catch {
204
+ // If parsing fails, use the original error message
205
+ errorMessage = `Chat error: ${error.message}`;
206
+ }
207
+ }
208
+
209
+ logger.error(errorMessage);
210
+ throw new Error(errorMessage);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * List available models
216
+ * Command: berget chat list
217
+ */
218
+ public async listModels(apiKey?: string): Promise<any> {
219
+ try {
220
+ // Check for environment variable first, then fallback to provided API key
221
+ const environmentApiKey = process.env.BERGET_API_KEY;
222
+ const effectiveApiKey = environmentApiKey || apiKey;
223
+
224
+ if (effectiveApiKey) {
225
+ const headers = {
226
+ Authorization: effectiveApiKey,
227
+ };
228
+
229
+ const { data, error } = await this.client.GET('/v1/models', { headers });
230
+ if (error) throw new Error(JSON.stringify(error));
231
+ return data;
232
+ } else {
233
+ const { data, error } = await this.client.GET('/v1/models');
234
+ if (error) throw new Error(JSON.stringify(error));
235
+ return data;
236
+ }
237
+ } catch (error) {
238
+ // Improved error handling
239
+ let errorMessage = 'Failed to list models';
240
+
241
+ if (error instanceof Error) {
242
+ try {
243
+ // Try to parse the error message as JSON
244
+ const parsedError = JSON.parse(error.message);
245
+ if (parsedError.error) {
246
+ errorMessage = `Models error: ${
247
+ typeof parsedError.error === 'string'
248
+ ? parsedError.error
249
+ : parsedError.error.message || JSON.stringify(parsedError.error)
250
+ }`;
214
251
  }
215
- } catch (e) {
252
+ } catch {
216
253
  // If parsing fails, use the original error message
217
- errorMessage = `Chat error: ${error.message}`
254
+ errorMessage = `Models error: ${error.message}`;
218
255
  }
219
256
  }
220
257
 
221
- logger.error(errorMessage)
222
- throw new Error(errorMessage)
258
+ logger.error(errorMessage);
259
+ throw new Error(errorMessage);
223
260
  }
224
261
  }
225
262
 
@@ -237,13 +274,13 @@ export class ChatService {
237
274
  // If an API key is provided, use it for this request
238
275
  if (options.apiKey) {
239
276
  // API keys should be sent directly, not with Bearer prefix
240
- headers['Authorization'] = options.apiKey
277
+ headers['Authorization'] = options.apiKey;
241
278
  }
242
279
 
243
280
  // Remove apiKey and onChunk from options before sending to API
244
- const { apiKey, onChunk, ...requestOptions } = options
281
+ const { apiKey: _apiKey, onChunk, ...requestOptions } = options;
245
282
 
246
- logger.debug('Request options:')
283
+ logger.debug('Request options:');
247
284
  logger.debug(
248
285
  JSON.stringify(
249
286
  {
@@ -255,80 +292,52 @@ export class ChatService {
255
292
  null,
256
293
  2,
257
294
  ),
258
- )
295
+ );
259
296
 
260
297
  // Handle streaming responses differently
261
298
  if (requestOptions.stream && onChunk) {
262
- return await this.handleStreamingResponse(
263
- { ...requestOptions, onChunk },
264
- headers,
265
- )
299
+ return await this.handleStreamingResponse({ ...requestOptions, onChunk }, headers);
266
300
  } else {
267
301
  // Ensure model is always defined for the API call
268
302
  const requestBody = {
269
303
  ...requestOptions,
270
304
  model: requestOptions.model || 'google/gemma-3-27b-it',
271
- }
305
+ };
272
306
 
273
307
  // Debug the headers being sent
274
- logger.debug('Headers being sent:')
275
- logger.debug(JSON.stringify(headers, null, 2))
308
+ logger.debug('Headers being sent:');
309
+ logger.debug(JSON.stringify(headers, null, 2));
276
310
 
277
311
  const response = await this.client.POST('/v1/chat/completions', {
278
312
  body: requestBody,
279
313
  headers,
280
- })
314
+ });
281
315
 
282
316
  // Check if response has an error property
283
- const responseAny = response as any
284
- if (responseAny && responseAny.error)
285
- throw new Error(JSON.stringify(responseAny.error))
317
+ const responseAny = response as any;
318
+ if (responseAny && responseAny.error) throw new Error(JSON.stringify(responseAny.error));
286
319
 
287
- logger.debug('API response:')
288
- logger.debug(JSON.stringify(response, null, 2))
320
+ logger.debug('API response:');
321
+ logger.debug(JSON.stringify(response, null, 2));
289
322
 
290
323
  // Output the complete response data for debugging
291
- logger.debug('Complete response data:')
292
- logger.debug(JSON.stringify(response.data, null, 2))
324
+ logger.debug('Complete response data:');
325
+ logger.debug(JSON.stringify(response.data, null, 2));
293
326
 
294
- return response.data
327
+ return response.data;
295
328
  }
296
329
  } catch (requestError) {
297
330
  logger.debug(
298
331
  `Request error: ${
299
- requestError instanceof Error
300
- ? requestError.message
301
- : String(requestError)
332
+ requestError instanceof Error ? requestError.message : String(requestError)
302
333
  }`,
303
- )
304
- throw requestError
334
+ );
335
+ throw requestError;
305
336
  }
306
337
  }
307
338
 
308
339
  /**
309
- * Handle the case when no API key is available
310
- */
311
- private handleNoApiKey(): never {
312
- // We've exhausted all options for getting an API key
313
- logger.warn('No API key available. You need to either:')
314
- logger.warn(
315
- '1. Create an API key with: berget api-keys create --name "My Key"',
316
- )
317
- logger.warn(
318
- '2. Set a default API key with: berget api-keys set-default <id>',
319
- )
320
- logger.warn('3. Provide an API key with the --api-key option')
321
- logger.warn('4. Set the BERGET_API_KEY environment variable')
322
- logger.warn('\nExample:')
323
- logger.warn(' export BERGET_API_KEY=your_api_key_here')
324
- logger.warn(' # or for a single command:')
325
- logger.warn(
326
- ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it',
327
- )
328
- throw new Error(
329
- 'No API key available. Please provide an API key or set a default API key.',
330
- )
331
- }
340
+
332
341
 
333
342
  /**
334
343
  * Handle streaming response from the API
@@ -341,175 +350,185 @@ export class ChatService {
341
350
  headers: Record<string, string>,
342
351
  ): Promise<any> {
343
352
  // Use the same base URL as the client
344
- const baseUrl = process.env.API_BASE_URL || 'https://api.berget.ai'
345
- const url = new URL(`${baseUrl}/v1/chat/completions`)
353
+ const baseUrl = process.env.API_BASE_URL || 'https://api.berget.ai';
354
+ const url = new URL(`${baseUrl}/v1/chat/completions`);
346
355
 
347
356
  try {
348
- logger.debug(`Making streaming request to: ${url.toString()}`)
349
- logger.debug(`Headers:`, JSON.stringify(headers, null, 2))
350
- logger.debug(`Body:`, JSON.stringify(options, null, 2))
357
+ logger.debug(`Making streaming request to: ${url.toString()}`);
358
+ logger.debug(`Headers:`, JSON.stringify(headers, null, 2));
359
+ logger.debug(`Body:`, JSON.stringify(options, null, 2));
351
360
 
352
361
  // Make fetch request directly to handle streaming
353
362
  const response = await fetch(url.toString(), {
354
- method: 'POST',
363
+ body: JSON.stringify(options),
355
364
  headers: {
356
- 'Content-Type': 'application/json',
357
365
  Accept: 'text/event-stream',
366
+ 'Content-Type': 'application/json',
358
367
  ...headers,
359
368
  },
360
- body: JSON.stringify(options),
361
- })
369
+ method: 'POST',
370
+ });
362
371
 
363
- logger.debug(`Response status: ${response.status}`)
372
+ logger.debug(`Response status: ${response.status}`);
364
373
  logger.debug(
365
374
  `Response headers:`,
366
375
  JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2),
367
- )
376
+ );
368
377
 
369
378
  if (!response.ok) {
370
- const errorText = await response.text()
371
- logger.error(
372
- `Stream request failed: ${response.status} ${response.statusText}`,
373
- )
374
- logger.error(`Error response: ${errorText}`)
379
+ const errorText = await response.text();
380
+ logger.error(`Stream request failed: ${response.status} ${response.statusText}`);
381
+ logger.error(`Error response: ${errorText}`);
375
382
  throw new Error(
376
383
  `Stream request failed: ${response.status} ${response.statusText} - ${errorText}`,
377
- )
384
+ );
378
385
  }
379
386
 
380
387
  if (!response.body) {
381
- throw new Error('No response body received')
388
+ throw new Error('No response body received');
382
389
  }
383
390
 
384
391
  // Process the stream
385
- const reader = response.body.getReader()
386
- const decoder = new TextDecoder()
387
- let fullContent = ''
388
- let fullResponse: any = null
389
- let buffer = '' // Buffer to accumulate partial JSON data
392
+ const reader = response.body.getReader();
393
+ const decoder = new TextDecoder();
394
+ let fullContent = '';
395
+ let fullResponse: any = null;
396
+ let buffer = ''; // Buffer to accumulate partial JSON data
390
397
 
391
398
  while (true) {
392
- const { done, value } = await reader.read()
393
- if (done) break
399
+ const { done, value } = await reader.read();
400
+ if (done) break;
394
401
 
395
- const chunk = decoder.decode(value, { stream: true })
396
- logger.debug(`Received chunk: ${chunk.length} bytes`)
402
+ const chunk = decoder.decode(value, { stream: true });
403
+ logger.debug(`Received chunk: ${chunk.length} bytes`);
397
404
 
398
405
  // Add chunk to buffer
399
- buffer += chunk
400
- logger.debug(`Added chunk to buffer. Buffer length: ${buffer.length}`)
406
+ buffer += chunk;
407
+ logger.debug(`Added chunk to buffer. Buffer length: ${buffer.length}`);
401
408
 
402
409
  // Process the buffer - it may contain multiple SSE events
403
- const lines = buffer.split('\n')
404
- logger.debug(`Processing ${lines.length} lines from buffer`)
405
-
410
+ const lines = buffer.split('\n');
411
+ logger.debug(`Processing ${lines.length} lines from buffer`);
412
+
406
413
  // Keep track of processed lines to update buffer
407
- let processedLines = 0
408
-
409
- for (let i = 0; i < lines.length; i++) {
410
- const line = lines[i]
411
- logger.debug(`Line ${i}: "${line}"`)
412
-
414
+ let processedLines = 0;
415
+
416
+ for (const [index, line] of lines.entries()) {
417
+ logger.debug(`Line ${index}: "${line}"`);
418
+
413
419
  if (line.startsWith('data:')) {
414
- const jsonData = line.slice(5).trim()
415
- logger.debug(`Extracted JSON data: "${jsonData}"`)
420
+ const jsonData = line.slice(5).trim();
421
+ logger.debug(`Extracted JSON data: "${jsonData}"`);
416
422
 
417
423
  // Skip empty data or [DONE] marker
418
424
  if (jsonData === '' || jsonData === '[DONE]') {
419
- logger.debug(`Skipping empty data or [DONE] marker`)
420
- processedLines = i + 1
421
- continue
425
+ logger.debug(`Skipping empty data or [DONE] marker`);
426
+ processedLines = index + 1;
427
+ continue;
422
428
  }
423
429
 
424
430
  // Check if JSON looks complete (basic validation)
425
431
  if (!jsonData.startsWith('{')) {
426
- logger.warn(`JSON data doesn't start with '{', might be partial: "${jsonData.substring(0, 50)}..."`)
432
+ logger.warn(
433
+ `JSON data doesn't start with '{', might be partial: "${jsonData.slice(0, 50)}..."`,
434
+ );
427
435
  // Don't process this line yet, keep it in buffer
428
- break
436
+ break;
429
437
  }
430
438
 
431
439
  // Count braces to check if JSON is complete
432
- let braceCount = 0
433
- let inString = false
434
- let escaped = false
435
-
436
- for (let j = 0; j < jsonData.length; j++) {
437
- const char = jsonData[j]
440
+ let braceCount = 0;
441
+ let inString = false;
442
+ let escaped = false;
443
+
444
+ for (const char of jsonData) {
438
445
  if (escaped) {
439
- escaped = false
440
- continue
446
+ escaped = false;
447
+ continue;
441
448
  }
442
449
  if (char === '\\') {
443
- escaped = true
444
- continue
450
+ escaped = true;
451
+ continue;
445
452
  }
446
453
  if (char === '"') {
447
- inString = !inString
448
- continue
454
+ inString = !inString;
455
+ continue;
449
456
  }
450
457
  if (!inString && char === '{') {
451
- braceCount++
458
+ braceCount++;
452
459
  } else if (!inString && char === '}') {
453
- braceCount--
460
+ braceCount--;
454
461
  }
455
462
  }
456
-
463
+
457
464
  if (braceCount !== 0) {
458
- logger.warn(`JSON braces don't balance (${braceCount}), treating as partial: "${jsonData.substring(0, 50)}..."`)
465
+ logger.warn(
466
+ `JSON braces don't balance (${braceCount}), treating as partial: "${jsonData.slice(0, 50)}..."`,
467
+ );
459
468
  // Don't process this line yet, keep it in buffer
460
- break
469
+ break;
461
470
  }
462
471
 
463
472
  try {
464
- logger.debug(`Attempting to parse JSON of length: ${jsonData.length}`)
465
- const parsedData = JSON.parse(jsonData)
466
- logger.debug(`Successfully parsed JSON: ${JSON.stringify(parsedData, null, 2)}`)
467
- processedLines = i + 1 // Mark this line as processed
473
+ logger.debug(`Attempting to parse JSON of length: ${jsonData.length}`);
474
+ const parsedData = JSON.parse(jsonData);
475
+ logger.debug(`Successfully parsed JSON: ${JSON.stringify(parsedData, null, 2)}`);
476
+ processedLines = index + 1; // Mark this line as processed
468
477
 
469
478
  // Call the onChunk callback with the parsed data
470
479
  if (options.onChunk) {
471
- options.onChunk(parsedData)
480
+ options.onChunk(parsedData);
472
481
  }
473
482
 
474
483
  // Keep track of the full response
475
484
  if (!fullResponse) {
476
- fullResponse = parsedData
485
+ fullResponse = parsedData;
477
486
  } else if (
478
487
  parsedData.choices &&
479
488
  parsedData.choices[0] &&
480
- parsedData.choices[0].delta
489
+ parsedData.choices[0].delta && // Accumulate content for the full response
490
+ parsedData.choices[0].delta.content
481
491
  ) {
482
- // Accumulate content for the full response
483
- if (parsedData.choices[0].delta.content) {
484
- fullContent += parsedData.choices[0].delta.content
485
- }
492
+ fullContent += parsedData.choices[0].delta.content;
486
493
  }
487
- } catch (e) {
488
- logger.error(`Error parsing chunk: ${e}`)
489
- logger.error(`JSON parse error at position ${(e as any).message?.match(/position (\d+)/)?.[1] || 'unknown'}`)
490
- logger.error(`Problematic chunk length: ${jsonData.length}`)
491
- logger.error(`Problematic chunk content: "${jsonData}"`)
492
- logger.error(`Chunk starts with: "${jsonData.substring(0, 50)}..."`)
493
- logger.error(`Chunk ends with: "...${jsonData.substring(jsonData.length - 50)}"`)
494
-
494
+ } catch (error) {
495
+ logger.error(`Error parsing chunk: ${error}`);
496
+ logger.error(
497
+ `JSON parse error at position ${(error as any).message?.match(/position (\d+)/)?.[1] || 'unknown'}`,
498
+ );
499
+ logger.error(`Problematic chunk length: ${jsonData.length}`);
500
+ logger.error(`Problematic chunk content: "${jsonData}"`);
501
+ logger.error(`Chunk starts with: "${jsonData.slice(0, 50)}..."`);
502
+ logger.error(
503
+ `Chunk ends with: "...${jsonData.slice(Math.max(0, jsonData.length - 50))}"`,
504
+ );
505
+
495
506
  // Show character codes around the error position
496
- const errorPos = parseInt((e as any).message?.match(/position (\d+)/)?.[1] || '0')
507
+ const errorPos = Number.parseInt(
508
+ (error as any).message?.match(/position (\d+)/)?.[1] || '0',
509
+ );
497
510
  if (errorPos > 0) {
498
- const start = Math.max(0, errorPos - 20)
499
- const end = Math.min(jsonData.length, errorPos + 20)
500
- logger.error(`Context around error position ${errorPos}:`)
501
- logger.error(`"${jsonData.substring(start, end)}"`)
502
- logger.error(`Character codes: ${Array.from(jsonData.substring(start, end)).map(c => c.charCodeAt(0)).join(' ')}`)
511
+ const start = Math.max(0, errorPos - 20);
512
+ const end = Math.min(jsonData.length, errorPos + 20);
513
+ logger.error(`Context around error position ${errorPos}:`);
514
+ logger.error(`"${jsonData.substring(start, end)}"`);
515
+ logger.error(
516
+ `Character codes: ${[...jsonData.substring(start, end)]
517
+ .map((c) => c.charCodeAt(0))
518
+ .join(' ')}`,
519
+ );
503
520
  }
504
521
  }
505
522
  }
506
523
  }
507
-
524
+
508
525
  // Update buffer to only contain unprocessed lines
509
526
  if (processedLines > 0) {
510
- const remainingLines = lines.slice(processedLines)
511
- buffer = remainingLines.join('\n')
512
- logger.debug(`Updated buffer. Remaining lines: ${remainingLines.length}, Buffer length: ${buffer.length}`)
527
+ const remainingLines = lines.slice(processedLines);
528
+ buffer = remainingLines.join('\n');
529
+ logger.debug(
530
+ `Updated buffer. Remaining lines: ${remainingLines.length}, Buffer length: ${buffer.length}`,
531
+ );
513
532
  }
514
533
  }
515
534
 
@@ -517,72 +536,19 @@ export class ChatService {
517
536
  if (fullResponse) {
518
537
  if (fullContent) {
519
538
  fullResponse.choices[0].message = {
520
- role: 'assistant',
521
539
  content: fullContent,
522
- }
540
+ role: 'assistant',
541
+ };
523
542
  }
524
- return fullResponse
543
+ return fullResponse;
525
544
  }
526
545
 
527
546
  return {
528
- choices: [{ message: { role: 'assistant', content: fullContent } }],
529
- }
530
- } catch (error) {
531
- logger.error(
532
- `Streaming error: ${
533
- error instanceof Error ? error.message : String(error)
534
- }`,
535
- )
536
- throw error
537
- }
538
- }
539
-
540
- /**
541
- * List available models
542
- * Command: berget chat list
543
- */
544
- public async listModels(apiKey?: string): Promise<any> {
545
- try {
546
- // Check for environment variable first, then fallback to provided API key
547
- const envApiKey = process.env.BERGET_API_KEY
548
- const effectiveApiKey = envApiKey || apiKey
549
-
550
- if (effectiveApiKey) {
551
- const headers = {
552
- Authorization: effectiveApiKey,
553
- }
554
-
555
- const { data, error } = await this.client.GET('/v1/models', { headers })
556
- if (error) throw new Error(JSON.stringify(error))
557
- return data
558
- } else {
559
- const { data, error } = await this.client.GET('/v1/models')
560
- if (error) throw new Error(JSON.stringify(error))
561
- return data
562
- }
547
+ choices: [{ message: { content: fullContent, role: 'assistant' } }],
548
+ };
563
549
  } catch (error) {
564
- // Improved error handling
565
- let errorMessage = 'Failed to list models'
566
-
567
- if (error instanceof Error) {
568
- try {
569
- // Try to parse the error message as JSON
570
- const parsedError = JSON.parse(error.message)
571
- if (parsedError.error) {
572
- errorMessage = `Models error: ${
573
- typeof parsedError.error === 'string'
574
- ? parsedError.error
575
- : parsedError.error.message || JSON.stringify(parsedError.error)
576
- }`
577
- }
578
- } catch (e) {
579
- // If parsing fails, use the original error message
580
- errorMessage = `Models error: ${error.message}`
581
- }
582
- }
583
-
584
- logger.error(errorMessage)
585
- throw new Error(errorMessage)
550
+ logger.error(`Streaming error: ${error instanceof Error ? error.message : String(error)}`);
551
+ throw error;
586
552
  }
587
553
  }
588
554
  }