berget 2.2.7 → 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 (130) hide show
  1. package/.github/workflows/publish.yml +6 -6
  2. package/.github/workflows/test.yml +1 -1
  3. package/.prettierrc +5 -3
  4. package/dist/index.js +24 -25
  5. package/dist/package.json +5 -3
  6. package/dist/src/agents/app.js +8 -8
  7. package/dist/src/agents/backend.js +3 -3
  8. package/dist/src/agents/devops.js +8 -8
  9. package/dist/src/agents/frontend.js +3 -3
  10. package/dist/src/agents/fullstack.js +3 -3
  11. package/dist/src/agents/index.js +18 -18
  12. package/dist/src/agents/quality.js +8 -8
  13. package/dist/src/agents/security.js +8 -8
  14. package/dist/src/client.js +115 -127
  15. package/dist/src/commands/api-keys.js +195 -202
  16. package/dist/src/commands/auth.js +16 -25
  17. package/dist/src/commands/autocomplete.js +8 -8
  18. package/dist/src/commands/billing.js +10 -19
  19. package/dist/src/commands/chat.js +139 -170
  20. package/dist/src/commands/clusters.js +21 -30
  21. package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
  22. package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
  23. package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
  24. package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
  25. package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
  26. package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
  27. package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
  28. package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
  29. package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
  30. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
  31. package/dist/src/commands/code/auth-sync.js +215 -228
  32. package/dist/src/commands/code/errors.js +15 -12
  33. package/dist/src/commands/code/setup.js +390 -425
  34. package/dist/src/commands/code.js +279 -294
  35. package/dist/src/commands/index.js +5 -5
  36. package/dist/src/commands/models.js +16 -25
  37. package/dist/src/commands/users.js +9 -18
  38. package/dist/src/constants/command-structure.js +138 -138
  39. package/dist/src/services/api-key-service.js +132 -152
  40. package/dist/src/services/auth-service.js +81 -95
  41. package/dist/src/services/browser-auth.js +121 -131
  42. package/dist/src/services/chat-service.js +369 -386
  43. package/dist/src/services/cluster-service.js +47 -62
  44. package/dist/src/services/collaborator-service.js +9 -21
  45. package/dist/src/services/flux-service.js +13 -25
  46. package/dist/src/services/helm-service.js +9 -21
  47. package/dist/src/services/kubectl-service.js +15 -29
  48. package/dist/src/utils/config-checker.js +7 -7
  49. package/dist/src/utils/config-loader.js +109 -109
  50. package/dist/src/utils/default-api-key.js +129 -139
  51. package/dist/src/utils/env-manager.js +55 -66
  52. package/dist/src/utils/error-handler.js +62 -62
  53. package/dist/src/utils/logger.js +74 -67
  54. package/dist/src/utils/markdown-renderer.js +28 -28
  55. package/dist/src/utils/opencode-validator.js +67 -69
  56. package/dist/src/utils/token-manager.js +67 -65
  57. package/dist/tests/commands/chat.test.js +30 -39
  58. package/dist/tests/commands/code.test.js +186 -195
  59. package/dist/tests/utils/config-loader.test.js +107 -107
  60. package/dist/tests/utils/env-manager.test.js +81 -90
  61. package/dist/tests/utils/opencode-validator.test.js +42 -41
  62. package/dist/vitest.config.js +1 -1
  63. package/eslint.config.mjs +50 -30
  64. package/index.ts +30 -31
  65. package/package.json +5 -3
  66. package/src/agents/app.ts +9 -9
  67. package/src/agents/backend.ts +4 -4
  68. package/src/agents/devops.ts +9 -9
  69. package/src/agents/frontend.ts +4 -4
  70. package/src/agents/fullstack.ts +4 -4
  71. package/src/agents/index.ts +27 -25
  72. package/src/agents/quality.ts +9 -9
  73. package/src/agents/security.ts +9 -9
  74. package/src/agents/types.ts +10 -10
  75. package/src/client.ts +85 -77
  76. package/src/commands/api-keys.ts +190 -185
  77. package/src/commands/auth.ts +15 -14
  78. package/src/commands/autocomplete.ts +10 -10
  79. package/src/commands/billing.ts +13 -12
  80. package/src/commands/chat.ts +145 -142
  81. package/src/commands/clusters.ts +20 -19
  82. package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
  83. package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
  84. package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
  85. package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
  86. package/src/commands/code/__tests__/fake-file-store.ts +15 -15
  87. package/src/commands/code/__tests__/fake-prompter.ts +86 -85
  88. package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
  89. package/src/commands/code/adapters/clack-prompter.ts +32 -30
  90. package/src/commands/code/adapters/fs-file-store.ts +18 -17
  91. package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
  92. package/src/commands/code/auth-sync.ts +210 -210
  93. package/src/commands/code/errors.ts +11 -11
  94. package/src/commands/code/ports/auth-services.ts +7 -7
  95. package/src/commands/code/ports/command-runner.ts +2 -2
  96. package/src/commands/code/ports/file-store.ts +3 -3
  97. package/src/commands/code/ports/prompter.ts +13 -13
  98. package/src/commands/code/setup.ts +408 -406
  99. package/src/commands/code.ts +288 -287
  100. package/src/commands/index.ts +11 -10
  101. package/src/commands/models.ts +19 -18
  102. package/src/commands/users.ts +11 -10
  103. package/src/constants/command-structure.ts +159 -159
  104. package/src/services/api-key-service.ts +85 -85
  105. package/src/services/auth-service.ts +55 -54
  106. package/src/services/browser-auth.ts +62 -62
  107. package/src/services/chat-service.ts +169 -170
  108. package/src/services/cluster-service.ts +28 -28
  109. package/src/services/collaborator-service.ts +6 -6
  110. package/src/services/flux-service.ts +17 -17
  111. package/src/services/helm-service.ts +11 -11
  112. package/src/services/kubectl-service.ts +12 -12
  113. package/src/types/api.d.ts +1933 -1933
  114. package/src/types/json.d.ts +1 -1
  115. package/src/utils/config-checker.ts +6 -6
  116. package/src/utils/config-loader.ts +130 -129
  117. package/src/utils/default-api-key.ts +81 -80
  118. package/src/utils/env-manager.ts +37 -37
  119. package/src/utils/error-handler.ts +64 -64
  120. package/src/utils/logger.ts +72 -66
  121. package/src/utils/markdown-renderer.ts +28 -28
  122. package/src/utils/opencode-validator.ts +72 -71
  123. package/src/utils/token-manager.ts +69 -68
  124. package/tests/commands/chat.test.ts +32 -31
  125. package/tests/commands/code.test.ts +182 -181
  126. package/tests/utils/config-loader.test.ts +111 -110
  127. package/tests/utils/env-manager.test.ts +83 -79
  128. package/tests/utils/opencode-validator.test.ts +43 -42
  129. package/tsconfig.json +2 -1
  130. package/vitest.config.ts +2 -2
package/src/client.ts CHANGED
@@ -1,40 +1,44 @@
1
- import createClient from "openapi-fetch";
2
- import type { paths } from "./types/api";
3
- import chalk from "chalk";
4
- import { TokenManager } from "./utils/token-manager";
5
- import { logger } from "./utils/logger";
1
+ import chalk from 'chalk';
2
+ import createClient from 'openapi-fetch';
3
+
4
+ import type { paths } from './types/api';
5
+
6
+ import { logger } from './utils/logger';
7
+ import { TokenManager } from './utils/token-manager';
8
+
9
+ type ApiMethod = (...args: unknown[]) => Promise<any>;
6
10
 
7
11
  // API Base URL
8
12
  // Use --local flag to test against local API
9
13
  // Use --stage flag to test against stage API
10
- const isLocalMode = process.argv.includes("--local");
11
- const isStageMode = process.argv.includes("--stage");
14
+ const isLocalMode = process.argv.includes('--local');
15
+ const isStageMode = process.argv.includes('--stage');
12
16
 
13
17
  export const API_BASE_URL =
14
18
  process.env.BERGET_API_URL ||
15
19
  (isLocalMode
16
- ? "http://localhost:3000"
20
+ ? 'http://localhost:3000'
17
21
  : isStageMode
18
- ? "https://api.stage.berget.ai"
19
- : "https://api.berget.ai"); // production default
22
+ ? 'https://api.stage.berget.ai'
23
+ : 'https://api.berget.ai'); // production default
20
24
 
21
25
  if (isLocalMode && !process.env.BERGET_API_URL) {
22
- logger.debug("Using local API endpoint: http://localhost:3000");
26
+ logger.debug('Using local API endpoint: http://localhost:3000');
23
27
  } else if (isStageMode && !process.env.BERGET_API_URL) {
24
- logger.debug("Using stage API endpoint: https://api.stage.berget.ai");
28
+ logger.debug('Using stage API endpoint: https://api.stage.berget.ai');
25
29
  }
26
30
 
27
31
  // Create a typed client for the Berget API
28
32
  export const apiClient = createClient<paths>({
29
33
  baseUrl: API_BASE_URL,
30
34
  headers: {
31
- "Content-Type": "application/json",
32
- Accept: "application/json",
35
+ Accept: 'application/json',
36
+ 'Content-Type': 'application/json',
33
37
  },
34
38
  });
35
39
 
36
40
  // Authentication functions
37
- export const getAuthToken = (): string | null => {
41
+ export const getAuthToken = (): null | string => {
38
42
  const tokenManager = TokenManager.getInstance();
39
43
  return tokenManager.getAccessToken();
40
44
  };
@@ -42,7 +46,7 @@ export const getAuthToken = (): string | null => {
42
46
  export const saveAuthToken = (
43
47
  accessToken: string,
44
48
  refreshToken: string,
45
- expiresIn: number = 3600
49
+ expiresIn: number = 3600,
46
50
  ): void => {
47
51
  const tokenManager = TokenManager.getInstance();
48
52
  tokenManager.setTokens(accessToken, refreshToken, expiresIn);
@@ -58,7 +62,7 @@ export const createAuthenticatedClient = () => {
58
62
  const tokenManager = TokenManager.getInstance();
59
63
 
60
64
  if (!tokenManager.getAccessToken()) {
61
- logger.debug("No authentication token found. Please run `berget auth login` first.");
65
+ logger.debug('No authentication token found. Please run `berget auth login` first.');
62
66
  }
63
67
 
64
68
  // Create the base client
@@ -66,46 +70,46 @@ export const createAuthenticatedClient = () => {
66
70
  baseUrl: API_BASE_URL,
67
71
  headers: tokenManager.getAccessToken()
68
72
  ? {
73
+ Accept: 'application/json',
69
74
  Authorization: `Bearer ${tokenManager.getAccessToken()}`,
70
- "Content-Type": "application/json",
71
- Accept: "application/json",
75
+ 'Content-Type': 'application/json',
72
76
  }
73
77
  : {
74
- "Content-Type": "application/json",
75
- Accept: "application/json",
78
+ Accept: 'application/json',
79
+ 'Content-Type': 'application/json',
76
80
  },
77
81
  });
78
82
 
79
83
  // Wrap the client to handle token refresh
80
84
  return new Proxy(client, {
81
- get(target, prop: string | symbol) {
85
+ get(target, property: string | symbol) {
82
86
  // For HTTP methods (GET, POST, etc.), add token refresh logic
83
87
  if (
84
- typeof target[prop as keyof typeof target] === "function" &&
85
- ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(String(prop))
88
+ typeof target[property as keyof typeof target] === 'function' &&
89
+ ['DELETE', 'GET', 'PATCH', 'POST', 'PUT'].includes(String(property))
86
90
  ) {
87
- return async (...args: any[]) => {
91
+ return async (...arguments_: any[]) => {
88
92
  // Check if token is expired before making the request
89
93
  if (tokenManager.isTokenExpired() && tokenManager.getRefreshToken()) {
90
94
  await refreshAccessToken(tokenManager);
91
95
  }
92
96
 
93
97
  // Update the Authorization header with the current token
94
- if (!args[1]?.headers?.Authorization && tokenManager.getAccessToken()) {
95
- if (!args[1]) args[1] = {};
96
- if (!args[1].headers) args[1].headers = {};
97
- args[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`;
98
+ if (!arguments_[1]?.headers?.Authorization && tokenManager.getAccessToken()) {
99
+ if (!arguments_[1]) arguments_[1] = {};
100
+ if (!arguments_[1].headers) arguments_[1].headers = {};
101
+ arguments_[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`;
98
102
  }
99
103
 
100
104
  // Make the original request
101
105
  let result;
102
106
  try {
103
- result = await (target[prop as keyof typeof target] as Function)(...args);
107
+ result = await (target[property as keyof typeof target] as ApiMethod)(...arguments_);
104
108
  } catch (requestError) {
105
109
  logger.debug(
106
110
  `Request error: ${
107
111
  requestError instanceof Error ? requestError.message : String(requestError)
108
- }`
112
+ }`,
109
113
  );
110
114
  return {
111
115
  error: {
@@ -123,63 +127,63 @@ export const createAuthenticatedClient = () => {
123
127
 
124
128
  try {
125
129
  // Standard 401 Unauthorized
126
- if (typeof result.error === "object" && result.error.status === 401) {
130
+ if (typeof result.error === 'object' && result.error.status === 401) {
127
131
  isAuthError = true;
128
132
  }
129
133
  // OAuth specific errors
130
134
  else if (
131
135
  result.error.error &&
132
- (result.error.error.code === "invalid_token" ||
133
- result.error.error.code === "token_expired" ||
134
- result.error.error.message === "Invalid API key" ||
135
- result.error.error.message?.toLowerCase().includes("token") ||
136
- result.error.error.message?.toLowerCase().includes("unauthorized"))
136
+ (result.error.error.code === 'invalid_token' ||
137
+ result.error.error.code === 'token_expired' ||
138
+ result.error.error.message === 'Invalid API key' ||
139
+ result.error.error.message?.toLowerCase().includes('token') ||
140
+ result.error.error.message?.toLowerCase().includes('unauthorized'))
137
141
  ) {
138
142
  isAuthError = true;
139
143
  }
140
144
  // Message-based detection as fallback
141
145
  else if (
142
- typeof result.error === "string" &&
143
- (result.error.toLowerCase().includes("unauthorized") ||
144
- result.error.toLowerCase().includes("token") ||
145
- result.error.toLowerCase().includes("auth"))
146
+ typeof result.error === 'string' &&
147
+ (result.error.toLowerCase().includes('unauthorized') ||
148
+ result.error.toLowerCase().includes('token') ||
149
+ result.error.toLowerCase().includes('auth'))
146
150
  ) {
147
151
  isAuthError = true;
148
152
  }
149
153
  } catch {
150
154
  // If we can't parse the error structure, do a simple string check
151
- const errorStr = String(result.error);
155
+ const errorString = String(result.error);
152
156
  if (
153
- errorStr.toLowerCase().includes("unauthorized") ||
154
- errorStr.toLowerCase().includes("token") ||
155
- errorStr.toLowerCase().includes("auth")
157
+ errorString.toLowerCase().includes('unauthorized') ||
158
+ errorString.toLowerCase().includes('token') ||
159
+ errorString.toLowerCase().includes('auth')
156
160
  ) {
157
161
  isAuthError = true;
158
162
  }
159
163
  }
160
164
 
161
165
  if (isAuthError && tokenManager.getRefreshToken()) {
162
- logger.debug("Auth error detected, attempting token refresh");
166
+ logger.debug('Auth error detected, attempting token refresh');
163
167
  logger.debug(`Error details: ${JSON.stringify(result.error, null, 2)}`);
164
168
 
165
169
  const refreshed = await refreshAccessToken(tokenManager);
166
170
  if (refreshed) {
167
- logger.debug("Token refreshed successfully, retrying request");
171
+ logger.debug('Token refreshed successfully, retrying request');
168
172
 
169
173
  // Update the Authorization header with the new token
170
- if (!args[1]) args[1] = {};
171
- if (!args[1].headers) args[1].headers = {};
172
- args[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`;
174
+ if (!arguments_[1]) arguments_[1] = {};
175
+ if (!arguments_[1].headers) arguments_[1].headers = {};
176
+ arguments_[1].headers.Authorization = `Bearer ${tokenManager.getAccessToken()}`;
173
177
 
174
178
  // Retry the request
175
- return await (target[prop as keyof typeof target] as Function)(...args);
179
+ return await (target[property as keyof typeof target] as ApiMethod)(...arguments_);
176
180
  } else {
177
- logger.debug("Token refresh failed");
181
+ logger.debug('Token refresh failed');
178
182
 
179
183
  // Add a more helpful error message for users
180
- if (typeof result.error === "object") {
184
+ if (typeof result.error === 'object') {
181
185
  result.error.userMessage =
182
- "Your session has expired. Please run `berget auth login` to log in again.";
186
+ 'Your session has expired. Please run `berget auth login` to log in again.';
183
187
  }
184
188
  }
185
189
  }
@@ -190,16 +194,16 @@ export const createAuthenticatedClient = () => {
190
194
  }
191
195
 
192
196
  // For other properties, just return the original
193
- return target[prop as keyof typeof target];
197
+ return target[property as keyof typeof target];
194
198
  },
195
199
  });
196
200
  };
197
201
 
198
202
  // Keycloak configuration for token refresh (must match auth-service.ts)
199
203
  const KEYCLOAK_URL =
200
- isStageMode || isLocalMode ? "https://keycloak.stage.berget.ai" : "https://keycloak.berget.ai";
201
- const KEYCLOAK_REALM = "berget";
202
- const KEYCLOAK_CLIENT_ID = "berget-code";
204
+ isStageMode || isLocalMode ? 'https://keycloak.stage.berget.ai' : 'https://keycloak.berget.ai';
205
+ const KEYCLOAK_REALM = 'berget';
206
+ const KEYCLOAK_CLIENT_ID = 'berget-code';
203
207
 
204
208
  // Helper function to refresh the access token
205
209
  async function refreshAccessToken(tokenManager: TokenManager): Promise<boolean> {
@@ -207,23 +211,23 @@ async function refreshAccessToken(tokenManager: TokenManager): Promise<boolean>
207
211
  const refreshToken = tokenManager.getRefreshToken();
208
212
  if (!refreshToken) return false;
209
213
 
210
- logger.debug("Attempting to refresh access token");
214
+ logger.debug('Attempting to refresh access token');
211
215
 
212
216
  // Refresh directly against Keycloak (berget-code is a public PKCE client)
213
217
  try {
214
218
  const response = await fetch(
215
219
  `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`,
216
220
  {
217
- method: "POST",
218
- headers: {
219
- "Content-Type": "application/x-www-form-urlencoded",
220
- },
221
221
  body: new URLSearchParams({
222
- grant_type: "refresh_token",
223
222
  client_id: KEYCLOAK_CLIENT_ID,
223
+ grant_type: 'refresh_token',
224
224
  refresh_token: refreshToken,
225
225
  }),
226
- }
226
+ headers: {
227
+ 'Content-Type': 'application/x-www-form-urlencoded',
228
+ },
229
+ method: 'POST',
230
+ },
227
231
  );
228
232
 
229
233
  // Handle HTTP errors
@@ -233,34 +237,38 @@ async function refreshAccessToken(tokenManager: TokenManager): Promise<boolean>
233
237
  // Check if the refresh token itself is expired or invalid
234
238
  if (response.status === 401 || response.status === 403) {
235
239
  console.warn(
236
- chalk.yellow("Your refresh token has expired. Please run `berget auth login` again.")
240
+ chalk.yellow('Your refresh token has expired. Please run `berget auth login` again.'),
237
241
  );
238
242
  // Clear tokens if unauthorized - they're invalid
239
243
  tokenManager.clearTokens();
240
244
  } else {
241
245
  console.warn(
242
- chalk.yellow(`Failed to refresh token: ${response.status} ${response.statusText}`)
246
+ chalk.yellow(`Failed to refresh token: ${response.status} ${response.statusText}`),
243
247
  );
244
248
  }
245
249
  return false;
246
250
  }
247
251
 
248
252
  // Parse the response
249
- const contentType = response.headers.get("content-type");
250
- if (!contentType || !contentType.includes("application/json")) {
253
+ const contentType = response.headers.get('content-type');
254
+ if (!contentType || !contentType.includes('application/json')) {
251
255
  console.warn(chalk.yellow(`Unexpected content type in response: ${contentType}`));
252
256
  return false;
253
257
  }
254
258
 
255
- const data = await response.json();
259
+ const data = (await response.json()) as {
260
+ expires_in?: number;
261
+ refresh_token?: string;
262
+ token: string;
263
+ };
256
264
 
257
265
  // Validate the response data
258
266
  if (!data || !data.token) {
259
- console.warn(chalk.yellow("Invalid token response. Please run `berget auth login` again."));
267
+ console.warn(chalk.yellow('Invalid token response. Please run `berget auth login` again.'));
260
268
  return false;
261
269
  }
262
270
 
263
- logger.debug("Token refreshed successfully");
271
+ logger.debug('Token refreshed successfully');
264
272
 
265
273
  // Update the token
266
274
  tokenManager.updateAccessToken(data.token, data.expires_in || 3600);
@@ -268,15 +276,15 @@ async function refreshAccessToken(tokenManager: TokenManager): Promise<boolean>
268
276
  // If a new refresh token was provided, update that too
269
277
  if (data.refresh_token) {
270
278
  tokenManager.setTokens(data.token, data.refresh_token, data.expires_in || 3600);
271
- logger.debug("Refresh token also updated");
279
+ logger.debug('Refresh token also updated');
272
280
  }
273
281
  } catch (fetchError) {
274
282
  console.warn(
275
283
  chalk.yellow(
276
284
  `Failed to refresh token: ${
277
285
  fetchError instanceof Error ? fetchError.message : String(fetchError)
278
- }`
279
- )
286
+ }`,
287
+ ),
280
288
  );
281
289
  return false;
282
290
  }
@@ -287,8 +295,8 @@ async function refreshAccessToken(tokenManager: TokenManager): Promise<boolean>
287
295
  chalk.yellow(
288
296
  `Failed to refresh authentication token: ${
289
297
  error instanceof Error ? error.message : String(error)
290
- }`
291
- )
298
+ }`,
299
+ ),
292
300
  );
293
301
  return false;
294
302
  }