berget 1.0.0 → 1.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/README.md +92 -0
- package/dist/index.js +49 -467
- package/dist/package.json +35 -0
- package/dist/src/client.js +210 -102
- package/dist/src/commands/api-keys.js +277 -0
- package/dist/src/commands/auth.js +65 -0
- package/dist/src/commands/autocomplete.js +24 -0
- package/dist/src/commands/billing.js +53 -0
- package/dist/src/commands/chat.js +342 -0
- package/dist/src/commands/clusters.js +69 -0
- package/dist/src/commands/index.js +25 -0
- package/dist/src/commands/models.js +69 -0
- package/dist/src/commands/users.js +43 -0
- package/dist/src/constants/command-structure.js +14 -0
- package/dist/src/services/api-key-service.js +6 -16
- package/dist/src/services/auth-service.js +49 -47
- package/dist/src/services/chat-service.js +300 -0
- package/dist/src/utils/config-checker.js +50 -0
- package/dist/src/utils/default-api-key.js +237 -0
- package/dist/src/utils/error-handler.js +4 -4
- package/dist/src/utils/token-manager.js +165 -0
- package/index.ts +56 -566
- package/package.json +8 -2
- package/src/client.ts +279 -80
- package/src/commands/api-keys.ts +374 -0
- package/src/commands/auth.ts +58 -0
- package/src/commands/autocomplete.ts +19 -0
- package/src/commands/billing.ts +41 -0
- package/src/commands/chat.ts +445 -0
- package/src/commands/clusters.ts +65 -0
- package/src/commands/index.ts +23 -0
- package/src/commands/models.ts +63 -0
- package/src/commands/users.ts +37 -0
- package/src/constants/command-structure.ts +16 -0
- package/src/services/api-key-service.ts +12 -20
- package/src/services/auth-service.ts +90 -50
- package/src/services/chat-service.ts +295 -0
- package/src/types/api.d.ts +238 -178
- package/src/types/json.d.ts +4 -0
- package/src/utils/config-checker.ts +23 -0
- package/src/utils/default-api-key.ts +229 -0
- package/src/utils/error-handler.ts +4 -4
- package/src/utils/token-manager.ts +150 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { ApiKeyService } from '../services/api-key-service'
|
|
6
|
+
import readline from 'readline'
|
|
7
|
+
|
|
8
|
+
interface DefaultApiKeyData {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
prefix: string
|
|
12
|
+
key: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages the default API key for chat commands
|
|
17
|
+
*/
|
|
18
|
+
export class DefaultApiKeyManager {
|
|
19
|
+
private static instance: DefaultApiKeyManager
|
|
20
|
+
private configFilePath: string
|
|
21
|
+
private defaultApiKey: DefaultApiKeyData | null = null
|
|
22
|
+
|
|
23
|
+
private constructor() {
|
|
24
|
+
// Set up config file path in user's home directory
|
|
25
|
+
const bergetDir = path.join(os.homedir(), '.berget')
|
|
26
|
+
if (!fs.existsSync(bergetDir)) {
|
|
27
|
+
fs.mkdirSync(bergetDir, { recursive: true })
|
|
28
|
+
}
|
|
29
|
+
this.configFilePath = path.join(bergetDir, 'default-api-key.json')
|
|
30
|
+
this.loadConfig()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static getInstance(): DefaultApiKeyManager {
|
|
34
|
+
if (!DefaultApiKeyManager.instance) {
|
|
35
|
+
DefaultApiKeyManager.instance = new DefaultApiKeyManager()
|
|
36
|
+
}
|
|
37
|
+
return DefaultApiKeyManager.instance
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load default API key from file
|
|
42
|
+
*/
|
|
43
|
+
private loadConfig(): void {
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(this.configFilePath)) {
|
|
46
|
+
const data = fs.readFileSync(this.configFilePath, 'utf8')
|
|
47
|
+
this.defaultApiKey = JSON.parse(data)
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (process.argv.includes('--debug')) {
|
|
51
|
+
console.error(chalk.dim('Failed to load default API key configuration'))
|
|
52
|
+
}
|
|
53
|
+
this.defaultApiKey = null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save default API key to file
|
|
59
|
+
*/
|
|
60
|
+
private saveConfig(): void {
|
|
61
|
+
try {
|
|
62
|
+
if (this.defaultApiKey) {
|
|
63
|
+
fs.writeFileSync(this.configFilePath, JSON.stringify(this.defaultApiKey, null, 2))
|
|
64
|
+
// Set file permissions to be readable only by the owner
|
|
65
|
+
fs.chmodSync(this.configFilePath, 0o600)
|
|
66
|
+
} else {
|
|
67
|
+
// If default API key is null, remove the file
|
|
68
|
+
if (fs.existsSync(this.configFilePath)) {
|
|
69
|
+
fs.unlinkSync(this.configFilePath)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (process.argv.includes('--debug')) {
|
|
74
|
+
console.error(chalk.dim('Failed to save default API key configuration'))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set the default API key
|
|
81
|
+
*/
|
|
82
|
+
public setDefaultApiKey(id: string, name: string, prefix: string, key: string): void {
|
|
83
|
+
this.defaultApiKey = { id, name, prefix, key }
|
|
84
|
+
this.saveConfig()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the default API key string
|
|
89
|
+
*/
|
|
90
|
+
public getDefaultApiKey(): string | null {
|
|
91
|
+
return this.defaultApiKey?.key || null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the default API key data object
|
|
96
|
+
*/
|
|
97
|
+
public getDefaultApiKeyData(): DefaultApiKeyData | null {
|
|
98
|
+
return this.defaultApiKey
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clear the default API key
|
|
103
|
+
*/
|
|
104
|
+
public clearDefaultApiKey(): void {
|
|
105
|
+
this.defaultApiKey = null
|
|
106
|
+
this.saveConfig()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Prompts the user to select a default API key if none is set
|
|
111
|
+
* @returns The selected API key or null if none was selected
|
|
112
|
+
*/
|
|
113
|
+
public async promptForDefaultApiKey(): Promise<string | null> {
|
|
114
|
+
try {
|
|
115
|
+
const isDebug = process.argv.includes('--debug')
|
|
116
|
+
|
|
117
|
+
if (isDebug) {
|
|
118
|
+
console.log(chalk.yellow('DEBUG: promptForDefaultApiKey called'))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If we already have a default API key, return it
|
|
122
|
+
if (this.defaultApiKey) {
|
|
123
|
+
if (isDebug) {
|
|
124
|
+
console.log(chalk.yellow('DEBUG: Using existing default API key'))
|
|
125
|
+
}
|
|
126
|
+
return this.defaultApiKey.key
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isDebug) {
|
|
130
|
+
console.log(chalk.yellow('DEBUG: No default API key found, getting ApiKeyService'))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const apiKeyService = ApiKeyService.getInstance()
|
|
134
|
+
|
|
135
|
+
// Get all API keys
|
|
136
|
+
let apiKeys;
|
|
137
|
+
try {
|
|
138
|
+
if (isDebug) {
|
|
139
|
+
console.log(chalk.yellow('DEBUG: Calling apiKeyService.list()'))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
apiKeys = await apiKeyService.list()
|
|
143
|
+
|
|
144
|
+
if (isDebug) {
|
|
145
|
+
console.log(chalk.yellow(`DEBUG: Got ${apiKeys ? apiKeys.length : 0} API keys`))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!apiKeys || apiKeys.length === 0) {
|
|
149
|
+
console.log(chalk.yellow('No API keys found. Create one with:'))
|
|
150
|
+
console.log(chalk.blue(' berget api-keys create --name "My Key"'))
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// Check if this is an authentication error
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
156
|
+
const isAuthError = errorMessage.includes('Unauthorized') ||
|
|
157
|
+
errorMessage.includes('Authentication failed') ||
|
|
158
|
+
errorMessage.includes('AUTH_FAILED');
|
|
159
|
+
|
|
160
|
+
if (isAuthError) {
|
|
161
|
+
console.log(chalk.yellow('Authentication required. Please run `berget auth login` first.'));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.red('Error fetching API keys:'));
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
console.log(chalk.red(error.message));
|
|
166
|
+
if (isDebug) {
|
|
167
|
+
console.log(chalk.yellow(`DEBUG: API key list error: ${error.message}`));
|
|
168
|
+
console.log(chalk.yellow(`DEBUG: Stack: ${error.stack}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(chalk.blue('Select an API key to use as default:'))
|
|
176
|
+
|
|
177
|
+
// Display available API keys
|
|
178
|
+
apiKeys.forEach((key, index) => {
|
|
179
|
+
console.log(` ${index + 1}. ${key.name} (${key.prefix}...)`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Create readline interface for user input
|
|
183
|
+
const rl = readline.createInterface({
|
|
184
|
+
input: process.stdin,
|
|
185
|
+
output: process.stdout
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Prompt for selection
|
|
189
|
+
const selection = await new Promise<number>((resolve) => {
|
|
190
|
+
rl.question('Enter number (or press Enter to cancel): ', (answer) => {
|
|
191
|
+
rl.close()
|
|
192
|
+
const num = parseInt(answer.trim(), 10)
|
|
193
|
+
if (isNaN(num) || num < 1 || num > apiKeys.length) {
|
|
194
|
+
resolve(-1) // Invalid selection
|
|
195
|
+
} else {
|
|
196
|
+
resolve(num - 1) // Convert to zero-based index
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (selection === -1) {
|
|
202
|
+
console.log(chalk.yellow('No API key selected'))
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const selectedKey = apiKeys[selection]
|
|
207
|
+
|
|
208
|
+
// Create a new API key with the selected name
|
|
209
|
+
const newKey = await apiKeyService.create({
|
|
210
|
+
name: `CLI Default (copy of ${selectedKey.name})`,
|
|
211
|
+
description: 'Created automatically by the Berget CLI for default use'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Save the new key as default
|
|
215
|
+
this.setDefaultApiKey(
|
|
216
|
+
newKey.id.toString(),
|
|
217
|
+
newKey.name,
|
|
218
|
+
newKey.key.substring(0, 8), // Use first 8 chars as prefix
|
|
219
|
+
newKey.key
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
console.log(chalk.green(`✓ Default API key set to: ${newKey.name}`))
|
|
223
|
+
return newKey.key
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(chalk.red('Failed to set default API key:'), error)
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -30,11 +30,11 @@ export function handleError(message: string, error: any): void {
|
|
|
30
30
|
|
|
31
31
|
// Check for authentication errors
|
|
32
32
|
if (
|
|
33
|
-
(typeof error === 'string' && error.includes('Unauthorized')) ||
|
|
34
|
-
(error && error.message && error.message.includes('Unauthorized')) ||
|
|
35
|
-
(error && error.code && error.code === 401)
|
|
33
|
+
(typeof error === 'string' && (error.includes('Unauthorized') || error.includes('Authentication failed'))) ||
|
|
34
|
+
(error && error.message && (error.message.includes('Unauthorized') || error.message.includes('Authentication failed'))) ||
|
|
35
|
+
(error && error.code && (error.code === 401 || error.code === 'AUTH_FAILED'))
|
|
36
36
|
) {
|
|
37
37
|
console.error(chalk.yellow('\nYou need to be logged in to use this command.'));
|
|
38
|
-
console.error(chalk.yellow('Run `berget login` to authenticate.'));
|
|
38
|
+
console.error(chalk.yellow('Run `berget auth login` to authenticate.'));
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
interface TokenData {
|
|
7
|
+
access_token: string
|
|
8
|
+
refresh_token: string
|
|
9
|
+
expires_at: number // timestamp in milliseconds
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages authentication tokens including refresh functionality
|
|
14
|
+
*/
|
|
15
|
+
export class TokenManager {
|
|
16
|
+
private static instance: TokenManager
|
|
17
|
+
private tokenFilePath: string
|
|
18
|
+
private tokenData: TokenData | null = null
|
|
19
|
+
|
|
20
|
+
private constructor() {
|
|
21
|
+
// Set up token file path in user's home directory
|
|
22
|
+
const bergetDir = path.join(os.homedir(), '.berget')
|
|
23
|
+
if (!fs.existsSync(bergetDir)) {
|
|
24
|
+
fs.mkdirSync(bergetDir, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
this.tokenFilePath = path.join(bergetDir, 'auth.json')
|
|
27
|
+
this.loadToken()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static getInstance(): TokenManager {
|
|
31
|
+
if (!TokenManager.instance) {
|
|
32
|
+
TokenManager.instance = new TokenManager()
|
|
33
|
+
}
|
|
34
|
+
return TokenManager.instance
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load token data from file
|
|
39
|
+
*/
|
|
40
|
+
private loadToken(): void {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
43
|
+
const data = fs.readFileSync(this.tokenFilePath, 'utf8')
|
|
44
|
+
this.tokenData = JSON.parse(data)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.dim('Failed to load authentication token'))
|
|
48
|
+
this.tokenData = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save token data to file
|
|
54
|
+
*/
|
|
55
|
+
private saveToken(): void {
|
|
56
|
+
try {
|
|
57
|
+
if (this.tokenData) {
|
|
58
|
+
fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.tokenData, null, 2))
|
|
59
|
+
// Set file permissions to be readable only by the owner
|
|
60
|
+
fs.chmodSync(this.tokenFilePath, 0o600)
|
|
61
|
+
} else {
|
|
62
|
+
// If token data is null, remove the file
|
|
63
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
64
|
+
fs.unlinkSync(this.tokenFilePath)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(chalk.dim('Failed to save authentication token'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the current access token
|
|
74
|
+
* @returns The access token or null if not available
|
|
75
|
+
*/
|
|
76
|
+
public getAccessToken(): string | null {
|
|
77
|
+
if (!this.tokenData) return null
|
|
78
|
+
return this.tokenData.access_token
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the refresh token
|
|
83
|
+
* @returns The refresh token or null if not available
|
|
84
|
+
*/
|
|
85
|
+
public getRefreshToken(): string | null {
|
|
86
|
+
if (!this.tokenData) return null
|
|
87
|
+
return this.tokenData.refresh_token
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if the access token is expired
|
|
92
|
+
* @returns true if expired or about to expire (within 5 minutes), false otherwise
|
|
93
|
+
*/
|
|
94
|
+
public isTokenExpired(): boolean {
|
|
95
|
+
if (!this.tokenData || !this.tokenData.expires_at) return true
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Consider token expired if it's within 10 minutes of expiration
|
|
99
|
+
// Using a larger buffer to be more proactive about refreshing
|
|
100
|
+
const expirationBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds
|
|
101
|
+
const isExpired = Date.now() + expirationBuffer >= this.tokenData.expires_at;
|
|
102
|
+
|
|
103
|
+
if (isExpired && process.argv.includes('--debug')) {
|
|
104
|
+
console.log(chalk.yellow(`DEBUG: Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(this.tokenData.expires_at).toISOString()}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isExpired;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// If there's any error checking expiration, assume token is expired
|
|
110
|
+
console.error(chalk.dim(`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}`));
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set new token data
|
|
117
|
+
* @param accessToken The new access token
|
|
118
|
+
* @param refreshToken The new refresh token
|
|
119
|
+
* @param expiresIn Expiration time in seconds
|
|
120
|
+
*/
|
|
121
|
+
public setTokens(accessToken: string, refreshToken: string, expiresIn: number): void {
|
|
122
|
+
this.tokenData = {
|
|
123
|
+
access_token: accessToken,
|
|
124
|
+
refresh_token: refreshToken,
|
|
125
|
+
expires_at: Date.now() + (expiresIn * 1000)
|
|
126
|
+
}
|
|
127
|
+
this.saveToken()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Update just the access token and its expiration
|
|
132
|
+
* @param accessToken The new access token
|
|
133
|
+
* @param expiresIn Expiration time in seconds
|
|
134
|
+
*/
|
|
135
|
+
public updateAccessToken(accessToken: string, expiresIn: number): void {
|
|
136
|
+
if (!this.tokenData) return
|
|
137
|
+
|
|
138
|
+
this.tokenData.access_token = accessToken
|
|
139
|
+
this.tokenData.expires_at = Date.now() + (expiresIn * 1000)
|
|
140
|
+
this.saveToken()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Clear all token data
|
|
145
|
+
*/
|
|
146
|
+
public clearTokens(): void {
|
|
147
|
+
this.tokenData = null
|
|
148
|
+
this.saveToken()
|
|
149
|
+
}
|
|
150
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
|
40
40
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
|
41
41
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
|
42
|
-
|
|
42
|
+
"resolveJsonModule": true, /* Enable importing .json files. */
|
|
43
43
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
|
44
44
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
45
45
|
|