berget 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ commander_1.program
25
25
  Version: ${package_json_1.version}`)
26
26
  .version(package_json_1.version, '-v, --version')
27
27
  .option('--local', 'Use local API endpoint (hidden)', false)
28
+ .option('--stage', 'Use stage API endpoint', false)
28
29
  .option('--debug', 'Enable debug output', false);
29
30
  // Register all commands
30
31
  (0, commands_1.registerCommands)(commander_1.program);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "berget",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "berget": "dist/index.js"
@@ -19,12 +19,19 @@ const token_manager_1 = require("./utils/token-manager");
19
19
  const logger_1 = require("./utils/logger");
20
20
  // API Base URL
21
21
  // Use --local flag to test against local API
22
+ // Use --stage flag to test against stage API
22
23
  const isLocalMode = process.argv.includes('--local');
24
+ const isStageMode = process.argv.includes('--stage');
23
25
  exports.API_BASE_URL = process.env.BERGET_API_URL ||
24
- (isLocalMode ? 'http://localhost:3000' : 'https://api.berget.ai');
26
+ (isLocalMode ? 'http://localhost:3000' :
27
+ isStageMode ? 'https://api.stage.berget.ai' :
28
+ 'https://api.berget.ai'); // production default
25
29
  if (isLocalMode && !process.env.BERGET_API_URL) {
26
30
  logger_1.logger.debug('Using local API endpoint: http://localhost:3000');
27
31
  }
32
+ else if (isStageMode && !process.env.BERGET_API_URL) {
33
+ logger_1.logger.debug('Using stage API endpoint: https://api.stage.berget.ai');
34
+ }
28
35
  // Create a typed client for the Berget API
29
36
  exports.apiClient = (0, openapi_fetch_1.default)({
30
37
  baseUrl: exports.API_BASE_URL,
@@ -177,6 +184,12 @@ const createAuthenticatedClient = () => {
177
184
  });
178
185
  };
179
186
  exports.createAuthenticatedClient = createAuthenticatedClient;
187
+ // Keycloak configuration for token refresh (must match auth-service.ts)
188
+ const KEYCLOAK_URL = (isStageMode || isLocalMode)
189
+ ? 'https://keycloak.stage.berget.ai'
190
+ : 'https://keycloak.berget.ai';
191
+ const KEYCLOAK_REALM = 'berget';
192
+ const KEYCLOAK_CLIENT_ID = 'berget-code';
180
193
  // Helper function to refresh the access token
181
194
  function refreshAccessToken(tokenManager) {
182
195
  return __awaiter(this, void 0, void 0, function* () {
@@ -185,15 +198,18 @@ function refreshAccessToken(tokenManager) {
185
198
  if (!refreshToken)
186
199
  return false;
187
200
  logger_1.logger.debug('Attempting to refresh access token');
188
- // Use fetch directly since this endpoint might not be in the OpenAPI spec
201
+ // Refresh directly against Keycloak (berget-code is a public PKCE client)
189
202
  try {
190
- const response = yield fetch(`${exports.API_BASE_URL}/v1/auth/refresh`, {
203
+ const response = yield fetch(`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`, {
191
204
  method: 'POST',
192
205
  headers: {
193
- 'Content-Type': 'application/json',
194
- Accept: 'application/json',
206
+ 'Content-Type': 'application/x-www-form-urlencoded',
195
207
  },
196
- body: JSON.stringify({ refresh_token: refreshToken }),
208
+ body: new URLSearchParams({
209
+ grant_type: 'refresh_token',
210
+ client_id: KEYCLOAK_CLIENT_ID,
211
+ refresh_token: refreshToken,
212
+ }),
197
213
  });
198
214
  // Handle HTTP errors
199
215
  if (!response.ok) {
@@ -711,18 +711,19 @@ function registerCodeCommands(program) {
711
711
  npm: '@ai-sdk/openai-compatible',
712
712
  name: 'Berget AI',
713
713
  options: {
714
- baseURL: 'https://api.berget.ai/v1',
714
+ baseURL: '{env:BERGET_API_URL}',
715
715
  apiKey: '{env:BERGET_API_KEY}',
716
716
  },
717
717
  models: {
718
718
  'glm-4.7': {
719
719
  name: 'GLM-4.7',
720
- limit: { output: 4000, context: 90000 },
720
+ limit: { output: 4000, context: 200000 },
721
+ modalities: { input: ['text'], output: ['text'] },
721
722
  },
722
723
  'gpt-oss': {
723
724
  name: 'GPT-OSS',
724
725
  limit: { output: 4000, context: 128000 },
725
- modalities: ['text', 'image'],
726
+ modalities: { input: ['text', 'image'], output: ['text'] },
726
727
  },
727
728
  'llama-8b': {
728
729
  name: 'llama-3.1-8b',
@@ -1002,6 +1003,34 @@ All agents follow these principles:
1002
1003
  }
1003
1004
  // Set environment variables for opencode
1004
1005
  const env = Object.assign({}, process.env);
1006
+ // Set API base URL based on flags (default to production)
1007
+ const isLocalMode = process.argv.includes('--local');
1008
+ const isStageMode = process.argv.includes('--stage');
1009
+ if (isLocalMode) {
1010
+ env.BERGET_API_URL = 'http://localhost:3000/v1';
1011
+ console.log(chalk_1.default.dim('Using local API: http://localhost:3000/v1'));
1012
+ }
1013
+ else if (isStageMode) {
1014
+ env.BERGET_API_URL = 'https://api.stage.berget.ai/v1';
1015
+ console.log(chalk_1.default.dim('Using stage API: https://api.stage.berget.ai/v1'));
1016
+ }
1017
+ else {
1018
+ env.BERGET_API_URL = 'https://api.berget.ai/v1';
1019
+ }
1020
+ // Auth resolution: JWT first (if valid), then API-key
1021
+ // This ensures seat-based users get proper tracking
1022
+ const jwtToken = (0, client_1.getAuthToken)();
1023
+ if (jwtToken) {
1024
+ env.BERGET_API_KEY = jwtToken;
1025
+ console.log(chalk_1.default.dim('Using JWT token for authentication'));
1026
+ }
1027
+ else if (env.BERGET_API_KEY) {
1028
+ console.log(chalk_1.default.dim('Using API key for authentication'));
1029
+ }
1030
+ else {
1031
+ console.log(chalk_1.default.yellow('Warning: No authentication found'));
1032
+ console.log(chalk_1.default.dim(' Run `berget auth login` or set BERGET_API_KEY'));
1033
+ }
1005
1034
  // Prepare opencode command
1006
1035
  const opencodeArgs = [];
1007
1036
  if (prompt) {
@@ -1251,7 +1280,7 @@ All agents follow these principles:
1251
1280
  npm: '@ai-sdk/openai-compatible',
1252
1281
  name: 'Berget AI',
1253
1282
  options: {
1254
- baseURL: 'https://api.berget.ai/v1',
1283
+ baseURL: '{env:BERGET_API_URL}',
1255
1284
  apiKey: '{env:BERGET_API_KEY}',
1256
1285
  },
1257
1286
  models: (0, config_loader_1.getProviderModels)(),
@@ -50,7 +50,7 @@ class ApiKeyService {
50
50
  * Command: berget api-keys create
51
51
  */
52
52
  create(options) {
53
- var _a, _b, _c, _d;
53
+ var _a, _b, _c, _d, _e;
54
54
  return __awaiter(this, void 0, void 0, function* () {
55
55
  try {
56
56
  // Validate input before sending request
@@ -89,13 +89,16 @@ class ApiKeyService {
89
89
  detailedMessage += '5. Contact support if the problem persists';
90
90
  throw new Error(detailedMessage);
91
91
  }
92
- if (((_b = errorObj.error) === null || _b === void 0 ? void 0 : _b.code) === 'QUOTA_EXCEEDED') {
92
+ if (((_b = errorObj.error) === null || _b === void 0 ? void 0 : _b.code) === 'USER_NOT_FOUND') {
93
+ throw new Error('Your account is still being set up. Please wait a moment and try again.\n\nIf this issue persists, please contact support at support@berget.ai');
94
+ }
95
+ if (((_c = errorObj.error) === null || _c === void 0 ? void 0 : _c.code) === 'QUOTA_EXCEEDED') {
93
96
  throw new Error('You have reached your API key limit. Please delete existing keys or contact support to increase your quota.');
94
97
  }
95
- if (((_c = errorObj.error) === null || _c === void 0 ? void 0 : _c.code) === 'INSUFFICIENT_PERMISSIONS') {
98
+ if (((_d = errorObj.error) === null || _d === void 0 ? void 0 : _d.code) === 'INSUFFICIENT_PERMISSIONS') {
96
99
  throw new Error('Your account does not have permission to create API keys. Please contact your administrator.');
97
100
  }
98
- if (((_d = errorObj.error) === null || _d === void 0 ? void 0 : _d.code) === 'BILLING_REQUIRED') {
101
+ if (((_e = errorObj.error) === null || _e === void 0 ? void 0 : _e.code) === 'BILLING_REQUIRED') {
99
102
  throw new Error('A valid billing method is required to create API keys. Please add a payment method.');
100
103
  }
101
104
  }
@@ -41,6 +41,30 @@ const client_1 = require("../client");
41
41
  const chalk_1 = __importDefault(require("chalk"));
42
42
  const error_handler_1 = require("../utils/error-handler");
43
43
  const command_structure_1 = require("../constants/command-structure");
44
+ const http = __importStar(require("http"));
45
+ const crypto = __importStar(require("crypto"));
46
+ const url = __importStar(require("url"));
47
+ // Keycloak configuration based on environment
48
+ const isStageMode = process.argv.includes('--stage');
49
+ const isLocalMode = process.argv.includes('--local');
50
+ const KEYCLOAK_URL = (isStageMode || isLocalMode)
51
+ ? 'https://keycloak.stage.berget.ai'
52
+ : 'https://keycloak.berget.ai';
53
+ const KEYCLOAK_REALM = 'berget';
54
+ const KEYCLOAK_CLIENT_ID = 'berget-code';
55
+ const CALLBACK_PORT = 8787;
56
+ /**
57
+ * Generate a random string for PKCE code_verifier
58
+ */
59
+ function generateCodeVerifier() {
60
+ return crypto.randomBytes(32).toString('base64url');
61
+ }
62
+ /**
63
+ * Generate code_challenge from code_verifier using S256 method
64
+ */
65
+ function generateCodeChallenge(verifier) {
66
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
67
+ }
44
68
  /**
45
69
  * Service for authentication operations
46
70
  * Command group: auth
@@ -58,7 +82,9 @@ class AuthService {
58
82
  whoami() {
59
83
  return __awaiter(this, void 0, void 0, function* () {
60
84
  try {
61
- const { data: profile, error } = yield this.client.GET('/v1/users/me');
85
+ // Create fresh client to ensure we have the latest token
86
+ const client = (0, client_1.createAuthenticatedClient)();
87
+ const { data: profile, error } = yield client.GET('/v1/users/me');
62
88
  if (error) {
63
89
  return null;
64
90
  }
@@ -75,123 +101,267 @@ class AuthService {
75
101
  // Clear any existing token to ensure a fresh login
76
102
  (0, client_1.clearAuthToken)();
77
103
  console.log(chalk_1.default.blue('Initiating login process...'));
78
- // Step 1: Initiate device authorization
79
- const { data: deviceData, error: deviceError } = yield client_1.apiClient.POST('/v1/auth/device', {});
80
- if (deviceError || !deviceData) {
81
- throw new Error(deviceError
82
- ? JSON.stringify(deviceError)
83
- : 'Failed to get device authorization data');
84
- }
85
- // Type assertion for deviceData
86
- const typedDeviceData = deviceData;
87
- // Display information to user
88
- console.log(chalk_1.default.cyan('\nTo complete login:'));
89
- console.log(chalk_1.default.cyan(`1. Open this URL: ${chalk_1.default.bold(typedDeviceData.verification_url ||
90
- 'https://keycloak.berget.ai/device')}`));
91
- if (!typedDeviceData.verification_url)
92
- console.log(chalk_1.default.cyan(`2. Enter this code: ${chalk_1.default.bold(typedDeviceData.user_code || '')}\n`));
93
- // Try to open browser automatically
94
- try {
95
- if (typedDeviceData.verification_url) {
96
- // Use dynamic import for the 'open' package
97
- const open = yield Promise.resolve().then(() => __importStar(require('open'))).then((m) => m.default);
98
- yield open(typedDeviceData.verification_url);
99
- console.log(chalk_1.default.dim("Browser opened automatically. If it didn't open, please use the URL above."));
104
+ // Generate PKCE code verifier and challenge
105
+ const codeVerifier = generateCodeVerifier();
106
+ const codeChallenge = generateCodeChallenge(codeVerifier);
107
+ const state = crypto.randomBytes(16).toString('hex');
108
+ const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`;
109
+ // Build authorization URL
110
+ const authUrl = new URL(`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`);
111
+ authUrl.searchParams.set('client_id', KEYCLOAK_CLIENT_ID);
112
+ authUrl.searchParams.set('response_type', 'code');
113
+ authUrl.searchParams.set('redirect_uri', redirectUri);
114
+ authUrl.searchParams.set('scope', 'openid email profile');
115
+ authUrl.searchParams.set('state', state);
116
+ authUrl.searchParams.set('code_challenge', codeChallenge);
117
+ authUrl.searchParams.set('code_challenge_method', 'S256');
118
+ // Create a promise that resolves when we receive the callback
119
+ const authResult = yield new Promise((resolve) => {
120
+ const server = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
121
+ const parsedUrl = url.parse(req.url || '', true);
122
+ if (parsedUrl.pathname === '/callback') {
123
+ const receivedState = parsedUrl.query.state;
124
+ const code = parsedUrl.query.code;
125
+ const error = parsedUrl.query.error;
126
+ const errorPage = (title, message) => `
127
+ <!DOCTYPE html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="UTF-8">
131
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
132
+ <title>Berget - Authentication Failed</title>
133
+ <style>
134
+ * { margin: 0; padding: 0; box-sizing: border-box; }
135
+ body {
136
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
137
+ display: flex;
138
+ justify-content: center;
139
+ align-items: center;
140
+ min-height: 100vh;
141
+ background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
142
+ color: #fff;
100
143
  }
101
- }
102
- catch (error) {
103
- console.log(chalk_1.default.yellow('Could not open browser automatically. Please open the URL manually.'));
104
- }
105
- console.log(chalk_1.default.dim('\nWaiting for authentication to complete...'));
106
- // Step 2: Poll for completion
107
- const startTime = Date.now();
108
- const expiresIn = typedDeviceData.expires_in !== undefined
109
- ? typedDeviceData.expires_in
110
- : 900;
111
- const expiresAt = startTime + expiresIn * 1000;
112
- let pollInterval = (typedDeviceData.interval || 5) * 1000;
113
- const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
114
- let spinnerIdx = 0;
115
- while (Date.now() < expiresAt) {
116
- // Wait for the polling interval
117
- yield new Promise((resolve) => setTimeout(resolve, pollInterval));
118
- // Update spinner
119
- process.stdout.write(`\r${chalk_1.default.blue(spinner[spinnerIdx])} Waiting for authentication...`);
120
- spinnerIdx = (spinnerIdx + 1) % spinner.length;
121
- // Check if authentication is complete
122
- const deviceCode = typedDeviceData.device_code || '';
123
- const { data: tokenData, error: tokenError } = yield client_1.apiClient.POST('/v1/auth/device/token', {
124
- body: {
125
- device_code: deviceCode,
126
- },
127
- });
128
- if (tokenError) {
129
- // Parse the error to get status and other details
130
- const errorObj = typeof tokenError === 'string' ? JSON.parse(tokenError) : tokenError;
131
- const status = errorObj.status || 0;
132
- const errorCode = errorObj.code || '';
133
- if (status === 401 || errorCode === 'AUTHORIZATION_PENDING') {
134
- // Still waiting for user to complete authorization
135
- continue;
136
- }
137
- else if (status === 429) {
138
- // Slow down
139
- pollInterval *= 2;
140
- continue;
141
- }
142
- else if (status === 400) {
143
- // Error or expired
144
- if (errorCode === 'EXPIRED_TOKEN') {
145
- console.log(chalk_1.default.red('\n\nAuthentication timed out. Please try again.'));
146
- }
147
- else if (errorCode !== 'AUTHORIZATION_PENDING') {
148
- // Only show error if it's not the expected "still waiting" error
149
- const errorMessage = errorObj.message || JSON.stringify(errorObj);
150
- console.log(chalk_1.default.red(`\n\nError: ${errorMessage}`));
151
- return false;
144
+ .container {
145
+ text-align: center;
146
+ padding: 3rem;
147
+ max-width: 400px;
148
+ }
149
+ .icon {
150
+ width: 80px;
151
+ height: 80px;
152
+ background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
153
+ border-radius: 50%;
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ margin: 0 auto 1.5rem;
158
+ box-shadow: 0 4px 20px rgba(248, 113, 113, 0.3);
159
+ }
160
+ .icon svg {
161
+ width: 40px;
162
+ height: 40px;
163
+ stroke: #fff;
164
+ stroke-width: 3;
165
+ }
166
+ h1 {
167
+ font-size: 1.5rem;
168
+ font-weight: 600;
169
+ margin-bottom: 0.75rem;
170
+ color: #fff;
171
+ }
172
+ p {
173
+ color: #94a3b8;
174
+ font-size: 0.95rem;
175
+ line-height: 1.5;
176
+ }
177
+ .brand {
178
+ margin-top: 2rem;
179
+ opacity: 0.5;
180
+ font-size: 0.8rem;
181
+ letter-spacing: 0.05em;
182
+ }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="container">
187
+ <div class="icon">
188
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
189
+ <line x1="18" y1="6" x2="6" y2="18"></line>
190
+ <line x1="6" y1="6" x2="18" y2="18"></line>
191
+ </svg>
192
+ </div>
193
+ <h1>${title}</h1>
194
+ <p>${message}</p>
195
+ <div class="brand">BERGET</div>
196
+ </div>
197
+ </body>
198
+ </html>
199
+ `;
200
+ if (error) {
201
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
202
+ res.end(errorPage('Authentication Failed', String(parsedUrl.query.error_description || error)));
203
+ server.close();
204
+ resolve({ success: false, error });
205
+ return;
152
206
  }
153
- else {
154
- // If it's AUTHORIZATION_PENDING, continue polling
155
- continue;
207
+ if (receivedState !== state) {
208
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
209
+ res.end(errorPage('Authentication Failed', 'Invalid state parameter. Please try again.'));
210
+ server.close();
211
+ resolve({ success: false, error: 'Invalid state parameter' });
212
+ return;
156
213
  }
157
- return false;
214
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
215
+ res.end(`
216
+ <!DOCTYPE html>
217
+ <html lang="en">
218
+ <head>
219
+ <meta charset="UTF-8">
220
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
221
+ <title>Berget - Authentication Successful</title>
222
+ <style>
223
+ * { margin: 0; padding: 0; box-sizing: border-box; }
224
+ body {
225
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
226
+ display: flex;
227
+ justify-content: center;
228
+ align-items: center;
229
+ min-height: 100vh;
230
+ background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
231
+ color: #fff;
232
+ }
233
+ .container {
234
+ text-align: center;
235
+ padding: 3rem;
236
+ max-width: 400px;
237
+ }
238
+ .icon {
239
+ width: 80px;
240
+ height: 80px;
241
+ background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
242
+ border-radius: 50%;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ margin: 0 auto 1.5rem;
247
+ box-shadow: 0 4px 20px rgba(74, 222, 128, 0.3);
248
+ }
249
+ .icon svg {
250
+ width: 40px;
251
+ height: 40px;
252
+ stroke: #fff;
253
+ stroke-width: 3;
254
+ }
255
+ h1 {
256
+ font-size: 1.5rem;
257
+ font-weight: 600;
258
+ margin-bottom: 0.75rem;
259
+ color: #fff;
260
+ }
261
+ p {
262
+ color: #94a3b8;
263
+ font-size: 0.95rem;
264
+ line-height: 1.5;
265
+ }
266
+ .brand {
267
+ margin-top: 2rem;
268
+ opacity: 0.5;
269
+ font-size: 0.8rem;
270
+ letter-spacing: 0.05em;
271
+ }
272
+ </style>
273
+ </head>
274
+ <body>
275
+ <div class="container">
276
+ <div class="icon">
277
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
278
+ <polyline points="20 6 9 17 4 12"></polyline>
279
+ </svg>
280
+ </div>
281
+ <h1>Authentication Successful</h1>
282
+ <p>You can close this window and return to your terminal.</p>
283
+ <div class="brand">BERGET</div>
284
+ </div>
285
+ </body>
286
+ </html>
287
+ `);
288
+ server.close();
289
+ resolve({ success: true, code });
158
290
  }
159
- else {
160
- // For any other error, log it but continue polling
161
- // This makes the flow more resilient to temporary issues
162
- if (process.env.DEBUG) {
163
- console.log(chalk_1.default.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`));
164
- console.log(chalk_1.default.yellow('Continuing to wait for authentication...'));
165
- process.stdout.write(`\r${chalk_1.default.blue(spinner[spinnerIdx])} Waiting for authentication...`);
166
- }
167
- continue;
291
+ }));
292
+ server.listen(CALLBACK_PORT, () => {
293
+ if (process.argv.includes('--debug')) {
294
+ console.log(chalk_1.default.dim(`Callback server listening on port ${CALLBACK_PORT}`));
168
295
  }
169
- }
170
- else if (tokenData) {
171
- // Type assertion for tokenData
172
- const typedTokenData = tokenData;
173
- if (typedTokenData.token) {
174
- // Success!
175
- (0, client_1.saveAuthToken)(typedTokenData.token, typedTokenData.refresh_token || '', typedTokenData.expires_in || 3600);
176
- if (process.argv.includes('--debug')) {
177
- console.log(chalk_1.default.yellow('DEBUG: Token data received:'));
178
- console.log(chalk_1.default.yellow(JSON.stringify({
179
- expires_in: typedTokenData.expires_in,
180
- refresh_expires_in: typedTokenData.refresh_expires_in,
181
- }, null, 2)));
182
- }
183
- process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear the spinner line
184
- console.log(chalk_1.default.green('✓ Successfully logged in to Berget'));
185
- if (typedTokenData.user) {
186
- const user = typedTokenData.user;
187
- console.log(chalk_1.default.green(`Logged in as ${user.name || user.email || 'User'}`));
188
- }
189
- return true;
296
+ });
297
+ // Set timeout for the server
298
+ setTimeout(() => {
299
+ server.close();
300
+ resolve({ success: false, error: 'Authentication timed out' });
301
+ }, 5 * 60 * 1000) // 5 minute timeout
302
+ ;
303
+ (() => __awaiter(this, void 0, void 0, function* () {
304
+ try {
305
+ const open = yield Promise.resolve().then(() => __importStar(require('open'))).then((m) => m.default);
306
+ yield open(authUrl.toString());
307
+ console.log(chalk_1.default.dim('Browser opened for authentication...'));
308
+ }
309
+ catch (_a) {
310
+ console.log(chalk_1.default.cyan('\nPlease open this URL in your browser:'));
311
+ console.log(chalk_1.default.bold(authUrl.toString()));
190
312
  }
313
+ }))();
314
+ });
315
+ if (!authResult.success || !authResult.code) {
316
+ console.log(chalk_1.default.red(`\nAuthentication failed: ${authResult.error || 'Unknown error'}`));
317
+ return false;
318
+ }
319
+ // Exchange authorization code for tokens
320
+ console.log(chalk_1.default.dim('Exchanging authorization code for tokens...'));
321
+ const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`;
322
+ const tokenResponse = yield fetch(tokenUrl, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/x-www-form-urlencoded',
326
+ },
327
+ body: new URLSearchParams({
328
+ grant_type: 'authorization_code',
329
+ client_id: KEYCLOAK_CLIENT_ID,
330
+ code: authResult.code,
331
+ redirect_uri: redirectUri,
332
+ code_verifier: codeVerifier,
333
+ }).toString(),
334
+ });
335
+ if (!tokenResponse.ok) {
336
+ const errorText = yield tokenResponse.text();
337
+ console.log(chalk_1.default.red(`\nFailed to exchange code for tokens: ${errorText}`));
338
+ return false;
339
+ }
340
+ const tokenData = (yield tokenResponse.json());
341
+ // Save tokens
342
+ (0, client_1.saveAuthToken)(tokenData.access_token, tokenData.refresh_token, tokenData.expires_in);
343
+ if (process.argv.includes('--debug')) {
344
+ console.log(chalk_1.default.yellow('DEBUG: Token data received:'));
345
+ console.log(chalk_1.default.yellow(JSON.stringify({
346
+ expires_in: tokenData.expires_in,
347
+ refresh_expires_in: tokenData.refresh_expires_in,
348
+ }, null, 2)));
349
+ }
350
+ console.log(chalk_1.default.green('\n✓ Successfully logged in to Berget'));
351
+ // Try to get user info
352
+ try {
353
+ const profile = yield this.whoami();
354
+ if (profile === null || profile === void 0 ? void 0 : profile.email) {
355
+ console.log(chalk_1.default.green(`Logged in as ${profile.name || profile.email}`));
191
356
  }
192
357
  }
193
- console.log(chalk_1.default.red('\n\nAuthentication timed out. Please try again.'));
194
- return false;
358
+ catch (_a) {
359
+ // Ignore errors fetching profile
360
+ }
361
+ console.log(chalk_1.default.cyan('\nNext steps:'));
362
+ console.log(chalk_1.default.cyan(' • Create an API key: berget api-keys create'));
363
+ console.log(chalk_1.default.cyan(' • Setup OpenCode: berget code init'));
364
+ return true;
195
365
  }
196
366
  catch (error) {
197
367
  (0, error_handler_1.handleError)('Login failed', error);
@@ -104,18 +104,23 @@ class TokenManager {
104
104
  }
105
105
  /**
106
106
  * Check if the access token is expired
107
- * @returns true if expired or about to expire (within 5 minutes), false otherwise
107
+ * @returns true if expired or about to expire (within 10% of lifetime or 30 seconds), false otherwise
108
108
  */
109
109
  isTokenExpired() {
110
110
  if (!this.tokenData || !this.tokenData.expires_at)
111
111
  return true;
112
112
  try {
113
- // Consider token expired if it's within 10 minutes of expiration
114
- // Using a larger buffer to be more proactive about refreshing
115
- const expirationBuffer = 10 * 60 * 1000; // 10 minutes in milliseconds
116
- const isExpired = Date.now() + expirationBuffer >= this.tokenData.expires_at;
113
+ const now = Date.now();
114
+ const expiresAt = this.tokenData.expires_at;
115
+ const timeUntilExpiry = expiresAt - now;
116
+ // Use 10% of remaining lifetime or 30 seconds, whichever is smaller
117
+ // This ensures we don't refresh tokens that were just issued
118
+ const minBuffer = 30 * 1000; // 30 seconds minimum
119
+ const percentBuffer = timeUntilExpiry * 0.1; // 10% of lifetime
120
+ const expirationBuffer = Math.min(minBuffer, percentBuffer);
121
+ const isExpired = now + expirationBuffer >= expiresAt;
117
122
  if (isExpired) {
118
- logger_1.logger.debug(`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(this.tokenData.expires_at).toISOString()}`);
123
+ logger_1.logger.debug(`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(expiresAt).toISOString()}`);
119
124
  }
120
125
  return isExpired;
121
126
  }
package/index.ts CHANGED
@@ -24,6 +24,7 @@ Version: ${version}`
24
24
  )
25
25
  .version(version, '-v, --version')
26
26
  .option('--local', 'Use local API endpoint (hidden)', false)
27
+ .option('--stage', 'Use stage API endpoint', false)
27
28
  .option('--debug', 'Enable debug output', false)
28
29
 
29
30
  // Register all commands