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.
- package/.github/workflows/publish.yml +6 -6
- package/.github/workflows/test.yml +1 -1
- package/.prettierrc +5 -3
- package/dist/index.js +24 -25
- package/dist/package.json +5 -3
- package/dist/src/agents/app.js +8 -8
- package/dist/src/agents/backend.js +3 -3
- package/dist/src/agents/devops.js +8 -8
- package/dist/src/agents/frontend.js +3 -3
- package/dist/src/agents/fullstack.js +3 -3
- package/dist/src/agents/index.js +18 -18
- package/dist/src/agents/quality.js +8 -8
- package/dist/src/agents/security.js +8 -8
- package/dist/src/client.js +115 -127
- package/dist/src/commands/api-keys.js +195 -202
- package/dist/src/commands/auth.js +16 -25
- package/dist/src/commands/autocomplete.js +8 -8
- package/dist/src/commands/billing.js +10 -19
- package/dist/src/commands/chat.js +139 -170
- package/dist/src/commands/clusters.js +21 -30
- package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
- package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
- package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
- package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
- package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
- package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
- package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
- package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
- package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
- package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
- package/dist/src/commands/code/auth-sync.js +215 -228
- package/dist/src/commands/code/errors.js +15 -12
- package/dist/src/commands/code/setup.js +390 -425
- package/dist/src/commands/code.js +279 -294
- package/dist/src/commands/index.js +5 -5
- package/dist/src/commands/models.js +16 -25
- package/dist/src/commands/users.js +9 -18
- package/dist/src/constants/command-structure.js +138 -138
- package/dist/src/services/api-key-service.js +132 -152
- package/dist/src/services/auth-service.js +81 -95
- package/dist/src/services/browser-auth.js +121 -131
- package/dist/src/services/chat-service.js +369 -386
- package/dist/src/services/cluster-service.js +47 -62
- package/dist/src/services/collaborator-service.js +9 -21
- package/dist/src/services/flux-service.js +13 -25
- package/dist/src/services/helm-service.js +9 -21
- package/dist/src/services/kubectl-service.js +15 -29
- package/dist/src/utils/config-checker.js +7 -7
- package/dist/src/utils/config-loader.js +109 -109
- package/dist/src/utils/default-api-key.js +129 -139
- package/dist/src/utils/env-manager.js +55 -66
- package/dist/src/utils/error-handler.js +62 -62
- package/dist/src/utils/logger.js +74 -67
- package/dist/src/utils/markdown-renderer.js +28 -28
- package/dist/src/utils/opencode-validator.js +67 -69
- package/dist/src/utils/token-manager.js +67 -65
- package/dist/tests/commands/chat.test.js +30 -39
- package/dist/tests/commands/code.test.js +186 -195
- package/dist/tests/utils/config-loader.test.js +107 -107
- package/dist/tests/utils/env-manager.test.js +81 -90
- package/dist/tests/utils/opencode-validator.test.js +42 -41
- package/dist/vitest.config.js +1 -1
- package/eslint.config.mjs +50 -30
- package/index.ts +30 -31
- package/package.json +5 -3
- package/src/agents/app.ts +9 -9
- package/src/agents/backend.ts +4 -4
- package/src/agents/devops.ts +9 -9
- package/src/agents/frontend.ts +4 -4
- package/src/agents/fullstack.ts +4 -4
- package/src/agents/index.ts +27 -25
- package/src/agents/quality.ts +9 -9
- package/src/agents/security.ts +9 -9
- package/src/agents/types.ts +10 -10
- package/src/client.ts +85 -77
- package/src/commands/api-keys.ts +190 -185
- package/src/commands/auth.ts +15 -14
- package/src/commands/autocomplete.ts +10 -10
- package/src/commands/billing.ts +13 -12
- package/src/commands/chat.ts +145 -142
- package/src/commands/clusters.ts +20 -19
- package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
- package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
- package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
- package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
- package/src/commands/code/__tests__/fake-file-store.ts +15 -15
- package/src/commands/code/__tests__/fake-prompter.ts +86 -85
- package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
- package/src/commands/code/adapters/clack-prompter.ts +32 -30
- package/src/commands/code/adapters/fs-file-store.ts +18 -17
- package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
- package/src/commands/code/auth-sync.ts +210 -210
- package/src/commands/code/errors.ts +11 -11
- package/src/commands/code/ports/auth-services.ts +7 -7
- package/src/commands/code/ports/command-runner.ts +2 -2
- package/src/commands/code/ports/file-store.ts +3 -3
- package/src/commands/code/ports/prompter.ts +13 -13
- package/src/commands/code/setup.ts +408 -406
- package/src/commands/code.ts +288 -287
- package/src/commands/index.ts +11 -10
- package/src/commands/models.ts +19 -18
- package/src/commands/users.ts +11 -10
- package/src/constants/command-structure.ts +159 -159
- package/src/services/api-key-service.ts +85 -85
- package/src/services/auth-service.ts +55 -54
- package/src/services/browser-auth.ts +62 -62
- package/src/services/chat-service.ts +169 -170
- package/src/services/cluster-service.ts +28 -28
- package/src/services/collaborator-service.ts +6 -6
- package/src/services/flux-service.ts +17 -17
- package/src/services/helm-service.ts +11 -11
- package/src/services/kubectl-service.ts +12 -12
- package/src/types/api.d.ts +1933 -1933
- package/src/types/json.d.ts +1 -1
- package/src/utils/config-checker.ts +6 -6
- package/src/utils/config-loader.ts +130 -129
- package/src/utils/default-api-key.ts +81 -80
- package/src/utils/env-manager.ts +37 -37
- package/src/utils/error-handler.ts +64 -64
- package/src/utils/logger.ts +72 -66
- package/src/utils/markdown-renderer.ts +28 -28
- package/src/utils/opencode-validator.ts +72 -71
- package/src/utils/token-manager.ts +69 -68
- package/tests/commands/chat.test.ts +32 -31
- package/tests/commands/code.test.ts +182 -181
- package/tests/utils/config-loader.test.ts +111 -110
- package/tests/utils/env-manager.test.ts +83 -79
- package/tests/utils/opencode-validator.test.ts +43 -42
- package/tsconfig.json +2 -1
- package/vitest.config.ts +2 -2
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import Ajv from
|
|
2
|
-
import addFormats from
|
|
3
|
-
import { readFileSync } from
|
|
4
|
-
import { join } from
|
|
5
|
-
import { dirname } from
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import addFormats from 'ajv-formats';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
6
|
|
|
7
7
|
// Load the official OpenCode JSON Schema
|
|
8
8
|
const __dirname = dirname(__filename);
|
|
9
|
-
const schemaPath = join(__dirname,
|
|
9
|
+
const schemaPath = join(__dirname, '..', 'schemas', 'opencode-schema.json');
|
|
10
10
|
|
|
11
11
|
let ajv: Ajv;
|
|
12
12
|
let openCodeSchema: any;
|
|
13
13
|
let validateFunction: any;
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
|
-
const schemaContent = readFileSync(schemaPath,
|
|
16
|
+
const schemaContent = readFileSync(schemaPath, 'utf-8');
|
|
17
17
|
openCodeSchema = JSON.parse(schemaContent);
|
|
18
18
|
|
|
19
19
|
// Initialize AJV with formats and options
|
|
20
20
|
ajv = new Ajv({
|
|
21
21
|
allErrors: true,
|
|
22
|
-
verbose: true,
|
|
23
|
-
strict: false,
|
|
24
22
|
allowUnionTypes: true,
|
|
25
23
|
removeAdditional: false,
|
|
24
|
+
strict: false,
|
|
25
|
+
verbose: true,
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// Add JSON Schema formats
|
|
@@ -31,43 +31,12 @@ try {
|
|
|
31
31
|
// Compile the schema
|
|
32
32
|
validateFunction = ajv.compile(openCodeSchema);
|
|
33
33
|
} catch (error) {
|
|
34
|
-
console.error(
|
|
35
|
-
throw new Error(
|
|
34
|
+
console.error('Failed to load OpenCode schema:', error);
|
|
35
|
+
throw new Error('Could not initialize OpenCode validator');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type OpenCodeConfig = any;
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Validate OpenCode configuration against the official JSON Schema
|
|
42
|
-
*/
|
|
43
|
-
export function validateOpenCodeConfig(config: any): {
|
|
44
|
-
valid: boolean;
|
|
45
|
-
errors?: string[];
|
|
46
|
-
} {
|
|
47
|
-
try {
|
|
48
|
-
if (!validateFunction) {
|
|
49
|
-
return { valid: false, errors: ["Schema validator not initialized"] };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const isValid = validateFunction(config);
|
|
53
|
-
|
|
54
|
-
if (isValid) {
|
|
55
|
-
return { valid: true };
|
|
56
|
-
} else {
|
|
57
|
-
const errors = validateFunction.errors?.map((err: any) => {
|
|
58
|
-
const path = err.instancePath || err.schemaPath || "root";
|
|
59
|
-
const message = err.message || "Unknown error";
|
|
60
|
-
return `${path}: ${message}`;
|
|
61
|
-
}) || ["Unknown validation error"];
|
|
62
|
-
|
|
63
|
-
return { valid: false, errors };
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error("Validation error:", error);
|
|
67
|
-
return { valid: false, errors: ["Validation process failed"] };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
40
|
/**
|
|
72
41
|
* Fix common OpenCode configuration issues
|
|
73
42
|
*/
|
|
@@ -75,50 +44,51 @@ export function fixOpenCodeConfig(config: any): OpenCodeConfig {
|
|
|
75
44
|
const fixed = { ...config };
|
|
76
45
|
|
|
77
46
|
// Fix tools.compact - should be boolean, not object
|
|
78
|
-
if (fixed.tools && typeof fixed.tools.compact ===
|
|
79
|
-
console.warn(
|
|
47
|
+
if (fixed.tools && typeof fixed.tools.compact === 'object') {
|
|
48
|
+
console.warn('⚠️ Converting tools.compact from object to boolean');
|
|
80
49
|
// If it has properties, assume it should be enabled
|
|
81
50
|
fixed.tools.compact = true;
|
|
82
51
|
}
|
|
83
52
|
|
|
84
53
|
// Remove invalid properties
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (fixed[
|
|
88
|
-
console.warn(`⚠️ Removing invalid property: ${
|
|
89
|
-
delete fixed[
|
|
54
|
+
const invalidProperties = ['maxTokens', 'contextWindow'];
|
|
55
|
+
for (const property of invalidProperties) {
|
|
56
|
+
if (fixed[property] !== undefined) {
|
|
57
|
+
console.warn(`⚠️ Removing invalid property: ${property}`);
|
|
58
|
+
delete fixed[property];
|
|
90
59
|
}
|
|
91
|
-
}
|
|
60
|
+
}
|
|
92
61
|
|
|
93
62
|
// Fix provider models with invalid properties
|
|
94
63
|
if (fixed.provider) {
|
|
95
64
|
Object.values(fixed.provider).forEach((provider: any) => {
|
|
96
65
|
if (provider?.models) {
|
|
97
66
|
Object.values(provider.models).forEach((model: any) => {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Set a reasonable default for output if not present
|
|
113
|
-
// (typically 1/4 to 1/8 of context window)
|
|
114
|
-
if (!model.limit.output && model.limit.context) {
|
|
115
|
-
model.limit.output = Math.floor(model.limit.context / 4);
|
|
67
|
+
if (
|
|
68
|
+
model &&
|
|
69
|
+
typeof model === 'object' && // Move maxTokens/contextWindow to proper structure if needed
|
|
70
|
+
(model.maxTokens || model.contextWindow)
|
|
71
|
+
) {
|
|
72
|
+
if (!model.limit) model.limit = {};
|
|
73
|
+
|
|
74
|
+
// Use the larger of maxTokens/contextWindow for context
|
|
75
|
+
const contextValues = [model.maxTokens, model.contextWindow].filter(Boolean);
|
|
76
|
+
if (contextValues.length > 0) {
|
|
77
|
+
const newContext = Math.max(...contextValues);
|
|
78
|
+
if (!model.limit.context || newContext > model.limit.context) {
|
|
79
|
+
model.limit.context = newContext;
|
|
116
80
|
}
|
|
81
|
+
}
|
|
117
82
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
83
|
+
// Set a reasonable default for output if not present
|
|
84
|
+
// (typically 1/4 to 1/8 of context window)
|
|
85
|
+
if (!model.limit.output && model.limit.context) {
|
|
86
|
+
model.limit.output = Math.floor(model.limit.context / 4);
|
|
121
87
|
}
|
|
88
|
+
|
|
89
|
+
delete model.maxTokens;
|
|
90
|
+
delete model.contextWindow;
|
|
91
|
+
console.warn('⚠️ Moved maxTokens/contextWindow to limit.context/output');
|
|
122
92
|
}
|
|
123
93
|
});
|
|
124
94
|
}
|
|
@@ -127,3 +97,34 @@ export function fixOpenCodeConfig(config: any): OpenCodeConfig {
|
|
|
127
97
|
|
|
128
98
|
return fixed;
|
|
129
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate OpenCode configuration against the official JSON Schema
|
|
103
|
+
*/
|
|
104
|
+
export function validateOpenCodeConfig(config: any): {
|
|
105
|
+
errors?: string[];
|
|
106
|
+
valid: boolean;
|
|
107
|
+
} {
|
|
108
|
+
try {
|
|
109
|
+
if (!validateFunction) {
|
|
110
|
+
return { errors: ['Schema validator not initialized'], valid: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isValid = validateFunction(config);
|
|
114
|
+
|
|
115
|
+
if (isValid) {
|
|
116
|
+
return { valid: true };
|
|
117
|
+
} else {
|
|
118
|
+
const errors = validateFunction.errors?.map((error: any) => {
|
|
119
|
+
const path = error.instancePath || error.schemaPath || 'root';
|
|
120
|
+
const message = error.message || 'Unknown error';
|
|
121
|
+
return `${path}: ${message}`;
|
|
122
|
+
}) || ['Unknown validation error'];
|
|
123
|
+
|
|
124
|
+
return { errors, valid: false };
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Validation error:', error);
|
|
128
|
+
return { errors: ['Validation process failed'], valid: false };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,32 +1,13 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { logger } from './logger';
|
|
5
6
|
|
|
6
7
|
interface TokenData {
|
|
7
8
|
access_token: string;
|
|
8
|
-
refresh_token: string;
|
|
9
9
|
expires_at: number; // timestamp in milliseconds
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extract the expiration time from a JWT token
|
|
14
|
-
* @param accessToken The JWT access token
|
|
15
|
-
* @returns The expiration timestamp in milliseconds, or 0 if invalid
|
|
16
|
-
*/
|
|
17
|
-
function extractJwtExpiresAt(accessToken: string): number {
|
|
18
|
-
try {
|
|
19
|
-
const parts = accessToken.split(".");
|
|
20
|
-
if (parts.length !== 3) return 0;
|
|
21
|
-
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
22
|
-
const decoded = JSON.parse(payload);
|
|
23
|
-
if (typeof decoded.exp === "number") {
|
|
24
|
-
return decoded.exp * 1000; // JWT exp is in seconds, convert to milliseconds
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
// If decoding fails, return 0 (treated as expired)
|
|
28
|
-
}
|
|
29
|
-
return 0;
|
|
10
|
+
refresh_token: string;
|
|
30
11
|
}
|
|
31
12
|
|
|
32
13
|
/**
|
|
@@ -34,16 +15,16 @@ function extractJwtExpiresAt(accessToken: string): number {
|
|
|
34
15
|
*/
|
|
35
16
|
export class TokenManager {
|
|
36
17
|
private static instance: TokenManager;
|
|
18
|
+
private tokenData: null | TokenData = null;
|
|
37
19
|
private tokenFilePath: string;
|
|
38
|
-
private tokenData: TokenData | null = null;
|
|
39
20
|
|
|
40
21
|
private constructor() {
|
|
41
22
|
// Set up token file path in user's home directory
|
|
42
|
-
const bergetDir = path.join(os.homedir(),
|
|
23
|
+
const bergetDir = path.join(os.homedir(), '.berget');
|
|
43
24
|
if (!fs.existsSync(bergetDir)) {
|
|
44
25
|
fs.mkdirSync(bergetDir, { recursive: true });
|
|
45
26
|
}
|
|
46
|
-
this.tokenFilePath = path.join(bergetDir,
|
|
27
|
+
this.tokenFilePath = path.join(bergetDir, 'auth.json');
|
|
47
28
|
this.loadToken();
|
|
48
29
|
}
|
|
49
30
|
|
|
@@ -55,45 +36,18 @@ export class TokenManager {
|
|
|
55
36
|
}
|
|
56
37
|
|
|
57
38
|
/**
|
|
58
|
-
*
|
|
59
|
-
*/
|
|
60
|
-
private loadToken(): void {
|
|
61
|
-
try {
|
|
62
|
-
if (fs.existsSync(this.tokenFilePath)) {
|
|
63
|
-
const data = fs.readFileSync(this.tokenFilePath, "utf8");
|
|
64
|
-
this.tokenData = JSON.parse(data);
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
logger.error("Failed to load authentication token");
|
|
68
|
-
this.tokenData = null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Save token data to file
|
|
39
|
+
* Clear all token data
|
|
74
40
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.tokenData, null, 2));
|
|
79
|
-
// Set file permissions to be readable only by the owner
|
|
80
|
-
fs.chmodSync(this.tokenFilePath, 0o600);
|
|
81
|
-
} else {
|
|
82
|
-
// If token data is null, remove the file
|
|
83
|
-
if (fs.existsSync(this.tokenFilePath)) {
|
|
84
|
-
fs.unlinkSync(this.tokenFilePath);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
logger.error("Failed to save authentication token");
|
|
89
|
-
}
|
|
41
|
+
public clearTokens(): void {
|
|
42
|
+
this.tokenData = null;
|
|
43
|
+
this.saveToken();
|
|
90
44
|
}
|
|
91
45
|
|
|
92
46
|
/**
|
|
93
47
|
* Get the current access token
|
|
94
48
|
* @returns The access token or null if not available
|
|
95
49
|
*/
|
|
96
|
-
public getAccessToken():
|
|
50
|
+
public getAccessToken(): null | string {
|
|
97
51
|
if (!this.tokenData) return null;
|
|
98
52
|
return this.tokenData.access_token;
|
|
99
53
|
}
|
|
@@ -102,7 +56,7 @@ export class TokenManager {
|
|
|
102
56
|
* Get the refresh token
|
|
103
57
|
* @returns The refresh token or null if not available
|
|
104
58
|
*/
|
|
105
|
-
public getRefreshToken():
|
|
59
|
+
public getRefreshToken(): null | string {
|
|
106
60
|
if (!this.tokenData) return null;
|
|
107
61
|
return this.tokenData.refresh_token;
|
|
108
62
|
}
|
|
@@ -129,7 +83,7 @@ export class TokenManager {
|
|
|
129
83
|
|
|
130
84
|
if (isExpired) {
|
|
131
85
|
logger.debug(
|
|
132
|
-
`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(expiresAt).toISOString()}
|
|
86
|
+
`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(expiresAt).toISOString()}`,
|
|
133
87
|
);
|
|
134
88
|
}
|
|
135
89
|
|
|
@@ -137,7 +91,7 @@ export class TokenManager {
|
|
|
137
91
|
} catch (error) {
|
|
138
92
|
// If there's any error checking expiration, assume token is expired
|
|
139
93
|
logger.error(
|
|
140
|
-
`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}
|
|
94
|
+
`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
95
|
);
|
|
142
96
|
return true;
|
|
143
97
|
}
|
|
@@ -156,8 +110,8 @@ export class TokenManager {
|
|
|
156
110
|
|
|
157
111
|
this.tokenData = {
|
|
158
112
|
access_token: accessToken,
|
|
159
|
-
refresh_token: refreshToken,
|
|
160
113
|
expires_at: expiresAt,
|
|
114
|
+
refresh_token: refreshToken,
|
|
161
115
|
};
|
|
162
116
|
this.saveToken();
|
|
163
117
|
}
|
|
@@ -180,10 +134,57 @@ export class TokenManager {
|
|
|
180
134
|
}
|
|
181
135
|
|
|
182
136
|
/**
|
|
183
|
-
*
|
|
137
|
+
* Load token data from file
|
|
184
138
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
139
|
+
private loadToken(): void {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
142
|
+
const data = fs.readFileSync(this.tokenFilePath, 'utf8');
|
|
143
|
+
this.tokenData = JSON.parse(data);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
logger.error('Failed to load authentication token');
|
|
147
|
+
this.tokenData = null;
|
|
148
|
+
}
|
|
188
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Save token data to file
|
|
153
|
+
*/
|
|
154
|
+
private saveToken(): void {
|
|
155
|
+
try {
|
|
156
|
+
if (this.tokenData) {
|
|
157
|
+
fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.tokenData, null, 2));
|
|
158
|
+
// Set file permissions to be readable only by the owner
|
|
159
|
+
fs.chmodSync(this.tokenFilePath, 0o600);
|
|
160
|
+
} else {
|
|
161
|
+
// If token data is null, remove the file
|
|
162
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
163
|
+
fs.unlinkSync(this.tokenFilePath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
logger.error('Failed to save authentication token');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract the expiration time from a JWT token
|
|
174
|
+
* @param accessToken The JWT access token
|
|
175
|
+
* @returns The expiration timestamp in milliseconds, or 0 if invalid
|
|
176
|
+
*/
|
|
177
|
+
function extractJwtExpiresAt(accessToken: string): number {
|
|
178
|
+
try {
|
|
179
|
+
const parts = accessToken.split('.');
|
|
180
|
+
if (parts.length !== 3) return 0;
|
|
181
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
182
|
+
const decoded = JSON.parse(payload);
|
|
183
|
+
if (typeof decoded.exp === 'number') {
|
|
184
|
+
return decoded.exp * 1000; // JWT exp is in seconds, convert to milliseconds
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// If decoding fails, return 0 (treated as expired)
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
189
190
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { registerChatCommands } from '../../src/commands/chat';
|
|
5
|
+
import { ChatService } from '../../src/services/chat-service';
|
|
6
|
+
import { DefaultApiKeyManager } from '../../src/utils/default-api-key';
|
|
6
7
|
|
|
7
8
|
// Mock dependencies
|
|
8
|
-
vi.mock(
|
|
9
|
-
vi.mock(
|
|
10
|
-
vi.mock(
|
|
9
|
+
vi.mock('../../src/services/chat-service');
|
|
10
|
+
vi.mock('../../src/utils/default-api-key');
|
|
11
|
+
vi.mock('readline', () => ({
|
|
11
12
|
createInterface: vi.fn(() => ({
|
|
12
|
-
question: vi.fn(),
|
|
13
13
|
close: vi.fn(),
|
|
14
|
+
question: vi.fn(),
|
|
14
15
|
})),
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
|
-
describe(
|
|
18
|
+
describe('Chat Commands', () => {
|
|
18
19
|
let program: Command;
|
|
19
20
|
let mockChatService: any;
|
|
20
21
|
let mockDefaultApiKeyManager: any;
|
|
@@ -43,39 +44,39 @@ describe("Chat Commands", () => {
|
|
|
43
44
|
vi.clearAllMocks();
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
describe(
|
|
47
|
-
it(
|
|
48
|
-
const chatCommand = program.commands.find(cmd => cmd.name() ===
|
|
49
|
-
const runCommand = chatCommand?.commands.find(cmd => cmd.name() ===
|
|
47
|
+
describe('chat run command', () => {
|
|
48
|
+
it('should use berget/glm-4.7 as default model', () => {
|
|
49
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
50
|
+
const runCommand = chatCommand?.commands.find((cmd) => cmd.name() === 'run');
|
|
50
51
|
|
|
51
52
|
expect(runCommand).toBeDefined();
|
|
52
53
|
|
|
53
54
|
// Check the help text which contains the default model
|
|
54
55
|
const helpText = runCommand?.helpInformation();
|
|
55
|
-
expect(helpText).toContain(
|
|
56
|
+
expect(helpText).toContain('glm-4.7');
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
it(
|
|
59
|
-
const chatCommand = program.commands.find(cmd => cmd.name() ===
|
|
60
|
-
const runCommand = chatCommand?.commands.find(cmd => cmd.name() ===
|
|
59
|
+
it('should have streaming enabled by default', () => {
|
|
60
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
61
|
+
const runCommand = chatCommand?.commands.find((cmd) => cmd.name() === 'run');
|
|
61
62
|
|
|
62
63
|
expect(runCommand).toBeDefined();
|
|
63
64
|
|
|
64
65
|
// Check that the option is --no-stream (meaning streaming is default)
|
|
65
|
-
const streamOption = runCommand?.options.find(opt => opt.long ===
|
|
66
|
+
const streamOption = runCommand?.options.find((opt) => opt.long === '--no-stream');
|
|
66
67
|
expect(streamOption).toBeDefined();
|
|
67
|
-
expect(streamOption?.description).toContain(
|
|
68
|
+
expect(streamOption?.description).toContain('Disable streaming');
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
it(
|
|
71
|
+
it('should create completion with correct default options', async () => {
|
|
71
72
|
// Mock API key
|
|
72
|
-
process.env.BERGET_API_KEY =
|
|
73
|
+
process.env.BERGET_API_KEY = 'test-key';
|
|
73
74
|
|
|
74
75
|
// Mock successful completion
|
|
75
76
|
mockChatService.createCompletion.mockResolvedValue({
|
|
76
77
|
choices: [
|
|
77
78
|
{
|
|
78
|
-
message: { content:
|
|
79
|
+
message: { content: 'Test response' },
|
|
79
80
|
},
|
|
80
81
|
],
|
|
81
82
|
});
|
|
@@ -90,30 +91,30 @@ describe("Chat Commands", () => {
|
|
|
90
91
|
});
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
describe(
|
|
94
|
-
it(
|
|
94
|
+
describe('chat list command', () => {
|
|
95
|
+
it('should list available models', async () => {
|
|
95
96
|
const mockModels = {
|
|
96
97
|
data: [
|
|
97
98
|
{
|
|
98
|
-
id: "gpt-oss",
|
|
99
|
-
owned_by: "openai",
|
|
100
99
|
active: true,
|
|
101
100
|
capabilities: {
|
|
102
|
-
vision: false,
|
|
103
101
|
function_calling: true,
|
|
104
102
|
json_mode: true,
|
|
103
|
+
vision: false,
|
|
105
104
|
},
|
|
105
|
+
id: 'gpt-oss',
|
|
106
|
+
owned_by: 'openai',
|
|
106
107
|
},
|
|
107
108
|
],
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
mockChatService.listModels.mockResolvedValue(mockModels);
|
|
111
112
|
|
|
112
|
-
const chatCommand = program.commands.find(cmd => cmd.name() ===
|
|
113
|
-
const listCommand = chatCommand?.commands.find(cmd => cmd.name() ===
|
|
113
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
114
|
+
const listCommand = chatCommand?.commands.find((cmd) => cmd.name() === 'list');
|
|
114
115
|
|
|
115
116
|
expect(listCommand).toBeDefined();
|
|
116
|
-
expect(listCommand?.description()).toBe(
|
|
117
|
+
expect(listCommand?.description()).toBe('List available chat models');
|
|
117
118
|
});
|
|
118
119
|
});
|
|
119
120
|
});
|