berget 2.2.6 → 2.2.7

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