berget 1.3.1 → 2.0.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/.env.example +5 -0
- package/.github/workflows/publish.yml +56 -0
- package/.github/workflows/test.yml +38 -0
- package/AGENTS.md +184 -0
- package/README.md +177 -38
- package/TODO.md +2 -0
- package/blog-post.md +176 -0
- package/dist/index.js +11 -8
- package/dist/package.json +14 -3
- package/dist/src/commands/api-keys.js +4 -2
- package/dist/src/commands/chat.js +182 -23
- package/dist/src/commands/code.js +1424 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/constants/command-structure.js +12 -0
- package/dist/src/schemas/opencode-schema.json +1121 -0
- package/dist/src/services/chat-service.js +10 -10
- package/dist/src/services/cluster-service.js +1 -1
- package/dist/src/utils/default-api-key.js +2 -2
- package/dist/src/utils/env-manager.js +86 -0
- package/dist/src/utils/error-handler.js +10 -3
- package/dist/src/utils/markdown-renderer.js +4 -4
- package/dist/src/utils/opencode-validator.js +122 -0
- package/dist/src/utils/token-manager.js +2 -2
- package/dist/tests/commands/chat.test.js +109 -0
- package/dist/tests/commands/code.test.js +414 -0
- package/dist/tests/utils/env-manager.test.js +148 -0
- package/dist/tests/utils/opencode-validator.test.js +103 -0
- package/dist/vitest.config.js +9 -0
- package/index.ts +67 -32
- package/opencode.json +182 -0
- package/package.json +14 -3
- package/src/client.ts +20 -20
- package/src/commands/api-keys.ts +93 -60
- package/src/commands/auth.ts +4 -2
- package/src/commands/billing.ts +6 -3
- package/src/commands/chat.ts +291 -97
- package/src/commands/clusters.ts +2 -2
- package/src/commands/code.ts +1696 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/models.ts +3 -3
- package/src/commands/users.ts +2 -2
- package/src/constants/command-structure.ts +112 -58
- package/src/schemas/opencode-schema.json +991 -0
- package/src/services/api-key-service.ts +1 -1
- package/src/services/auth-service.ts +27 -25
- package/src/services/chat-service.ts +37 -44
- package/src/services/cluster-service.ts +5 -5
- package/src/services/collaborator-service.ts +3 -3
- package/src/services/flux-service.ts +2 -2
- package/src/services/helm-service.ts +2 -2
- package/src/services/kubectl-service.ts +3 -6
- package/src/types/api.d.ts +1032 -1010
- package/src/types/json.d.ts +3 -3
- package/src/utils/default-api-key.ts +54 -42
- package/src/utils/env-manager.ts +98 -0
- package/src/utils/error-handler.ts +24 -15
- package/src/utils/logger.ts +12 -12
- package/src/utils/markdown-renderer.ts +18 -18
- package/src/utils/opencode-validator.ts +134 -0
- package/src/utils/token-manager.ts +35 -23
- package/tests/commands/chat.test.ts +129 -0
- package/tests/commands/code.test.ts +505 -0
- package/tests/utils/env-manager.test.ts +199 -0
- package/tests/utils/opencode-validator.test.ts +118 -0
- package/tsconfig.json +8 -8
- package/vitest.config.ts +8 -0
- package/-27b-it +0 -0
|
@@ -256,26 +256,26 @@ class ChatService {
|
|
|
256
256
|
*/
|
|
257
257
|
handleStreamingResponse(options, headers) {
|
|
258
258
|
return __awaiter(this, void 0, void 0, function* () {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const url = new URL(`${
|
|
262
|
-
// Debug the headers and options
|
|
263
|
-
logger_1.logger.debug('Streaming headers:');
|
|
264
|
-
logger_1.logger.debug(JSON.stringify(headers, null, 2));
|
|
265
|
-
logger_1.logger.debug('Streaming options:');
|
|
266
|
-
logger_1.logger.debug(JSON.stringify(Object.assign(Object.assign({}, options), { onChunk: options.onChunk ? 'function present' : 'no function' }), null, 2));
|
|
259
|
+
// Use the same base URL as the client
|
|
260
|
+
const baseUrl = process.env.API_BASE_URL || 'https://api.berget.ai';
|
|
261
|
+
const url = new URL(`${baseUrl}/v1/chat/completions`);
|
|
267
262
|
try {
|
|
263
|
+
logger_1.logger.debug(`Making streaming request to: ${url.toString()}`);
|
|
264
|
+
logger_1.logger.debug(`Headers:`, JSON.stringify(headers, null, 2));
|
|
265
|
+
logger_1.logger.debug(`Body:`, JSON.stringify(options, null, 2));
|
|
268
266
|
// Make fetch request directly to handle streaming
|
|
269
267
|
const response = yield fetch(url.toString(), {
|
|
270
268
|
method: 'POST',
|
|
271
269
|
headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'text/event-stream' }, headers),
|
|
272
270
|
body: JSON.stringify(options),
|
|
273
271
|
});
|
|
272
|
+
logger_1.logger.debug(`Response status: ${response.status}`);
|
|
273
|
+
logger_1.logger.debug(`Response headers:`, JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2));
|
|
274
274
|
if (!response.ok) {
|
|
275
275
|
const errorText = yield response.text();
|
|
276
276
|
logger_1.logger.error(`Stream request failed: ${response.status} ${response.statusText}`);
|
|
277
|
-
logger_1.logger.
|
|
278
|
-
throw new Error(`Stream request failed: ${response.status} ${response.statusText}`);
|
|
277
|
+
logger_1.logger.error(`Error response: ${errorText}`);
|
|
278
|
+
throw new Error(`Stream request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
279
279
|
}
|
|
280
280
|
if (!response.body) {
|
|
281
281
|
throw new Error('No response body received');
|
|
@@ -74,7 +74,7 @@ class ClusterService {
|
|
|
74
74
|
// This is a placeholder since the API doesn't have a specific endpoint
|
|
75
75
|
// In a real implementation, this would call a specific endpoint
|
|
76
76
|
const clusters = yield this.list();
|
|
77
|
-
return clusters.find(cluster => cluster.id === clusterId) || null;
|
|
77
|
+
return clusters.find((cluster) => cluster.id === clusterId) || null;
|
|
78
78
|
}
|
|
79
79
|
catch (error) {
|
|
80
80
|
console.error('Failed to describe cluster:', error);
|
|
@@ -179,7 +179,7 @@ class DefaultApiKeyManager {
|
|
|
179
179
|
// Create readline interface for user input
|
|
180
180
|
const rl = readline_1.default.createInterface({
|
|
181
181
|
input: process.stdin,
|
|
182
|
-
output: process.stdout
|
|
182
|
+
output: process.stdout,
|
|
183
183
|
});
|
|
184
184
|
// Prompt for selection
|
|
185
185
|
const selection = yield new Promise((resolve) => {
|
|
@@ -202,7 +202,7 @@ class DefaultApiKeyManager {
|
|
|
202
202
|
// Create a new API key with the selected name
|
|
203
203
|
const newKey = yield apiKeyService.create({
|
|
204
204
|
name: `CLI Default (copy of ${selectedKey.name})`,
|
|
205
|
-
description: 'Created automatically by the Berget CLI for default use'
|
|
205
|
+
description: 'Created automatically by the Berget CLI for default use',
|
|
206
206
|
});
|
|
207
207
|
// Save the new key as default
|
|
208
208
|
this.setDefaultApiKey(newKey.id.toString(), newKey.name, newKey.key.substring(0, 8), // Use first 8 chars as prefix
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.hasEnvKey = exports.updateEnvFile = void 0;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const promises_1 = require("fs/promises");
|
|
19
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
20
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
21
|
+
/**
|
|
22
|
+
* Safely updates .env file without overwriting existing keys
|
|
23
|
+
* Uses dotenv for proper parsing and formatting
|
|
24
|
+
*/
|
|
25
|
+
function updateEnvFile(options) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const { envPath = path_1.default.join(process.cwd(), '.env'), key, value, comment, force = false, } = options;
|
|
28
|
+
try {
|
|
29
|
+
let existingContent = '';
|
|
30
|
+
let parsed = {};
|
|
31
|
+
// Read existing .env file if it exists
|
|
32
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
33
|
+
existingContent = fs_1.default.readFileSync(envPath, 'utf8');
|
|
34
|
+
parsed = dotenv_1.default.parse(existingContent);
|
|
35
|
+
}
|
|
36
|
+
// Check if key already exists and we're not forcing
|
|
37
|
+
if (parsed[key] && !force) {
|
|
38
|
+
console.log(chalk_1.default.yellow(`⚠ ${key} already exists in .env - leaving unchanged`));
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Update the parsed object
|
|
42
|
+
parsed[key] = value;
|
|
43
|
+
// Generate new .env content
|
|
44
|
+
let newContent = '';
|
|
45
|
+
// Add comment at the top if this is a new file
|
|
46
|
+
if (!existingContent && comment) {
|
|
47
|
+
newContent += `# ${comment}\n`;
|
|
48
|
+
}
|
|
49
|
+
// Convert parsed object back to .env format
|
|
50
|
+
for (const [envKey, envValue] of Object.entries(parsed)) {
|
|
51
|
+
newContent += `${envKey}=${envValue}\n`;
|
|
52
|
+
}
|
|
53
|
+
// Write the updated content
|
|
54
|
+
yield (0, promises_1.writeFile)(envPath, newContent.trim() + '\n');
|
|
55
|
+
if (existingContent) {
|
|
56
|
+
console.log(chalk_1.default.green(`✓ Updated .env with ${key}`));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(chalk_1.default.green(`✓ Created .env with ${key}`));
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(chalk_1.default.red(`Failed to update .env file:`));
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
exports.updateEnvFile = updateEnvFile;
|
|
70
|
+
/**
|
|
71
|
+
* Checks if a .env file exists and contains a specific key
|
|
72
|
+
*/
|
|
73
|
+
function hasEnvKey(envPath = path_1.default.join(process.cwd(), '.env'), key) {
|
|
74
|
+
if (!fs_1.default.existsSync(envPath)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const content = fs_1.default.readFileSync(envPath, 'utf8');
|
|
79
|
+
const parsed = dotenv_1.default.parse(content);
|
|
80
|
+
return key in parsed;
|
|
81
|
+
}
|
|
82
|
+
catch (_a) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.hasEnvKey = hasEnvKey;
|
|
@@ -35,9 +35,16 @@ function handleError(message, error) {
|
|
|
35
35
|
console.error(chalk_1.default.dim(`Details: ${error.message}`));
|
|
36
36
|
}
|
|
37
37
|
// Check for authentication errors
|
|
38
|
-
if ((typeof error === 'string' &&
|
|
39
|
-
(error
|
|
40
|
-
|
|
38
|
+
if ((typeof error === 'string' &&
|
|
39
|
+
(error.includes('Unauthorized') ||
|
|
40
|
+
error.includes('Authentication failed'))) ||
|
|
41
|
+
(error &&
|
|
42
|
+
error.message &&
|
|
43
|
+
(error.message.includes('Unauthorized') ||
|
|
44
|
+
error.message.includes('Authentication failed'))) ||
|
|
45
|
+
(error &&
|
|
46
|
+
error.code &&
|
|
47
|
+
(error.code === 401 || error.code === 'AUTH_FAILED'))) {
|
|
41
48
|
console.error(chalk_1.default.yellow('\nYou need to be logged in to use this command.'));
|
|
42
49
|
console.error(chalk_1.default.yellow('Run `berget auth login` to authenticate.'));
|
|
43
50
|
}
|
|
@@ -23,8 +23,8 @@ marked_1.marked.setOptions({
|
|
|
23
23
|
// Adjust the width to fit the terminal
|
|
24
24
|
width: process.stdout.columns || 80,
|
|
25
25
|
// Customize code block rendering
|
|
26
|
-
codespan: chalk_1.default.cyan
|
|
27
|
-
})
|
|
26
|
+
codespan: chalk_1.default.cyan,
|
|
27
|
+
}),
|
|
28
28
|
});
|
|
29
29
|
/**
|
|
30
30
|
* Render markdown text to terminal-friendly formatted text
|
|
@@ -66,8 +66,8 @@ function containsMarkdown(text) {
|
|
|
66
66
|
/^\s*>\s+/m, // Blockquotes
|
|
67
67
|
/\|.*\|.*\|/, // Tables
|
|
68
68
|
/^---+$/m, // Horizontal rules
|
|
69
|
-
/^===+$/m // Alternative headers
|
|
69
|
+
/^===+$/m, // Alternative headers
|
|
70
70
|
];
|
|
71
|
-
return markdownPatterns.some(pattern => pattern.test(text));
|
|
71
|
+
return markdownPatterns.some((pattern) => pattern.test(text));
|
|
72
72
|
}
|
|
73
73
|
exports.containsMarkdown = containsMarkdown;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fixOpenCodeConfig = exports.validateOpenCodeConfig = void 0;
|
|
7
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
8
|
+
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const path_2 = require("path");
|
|
12
|
+
// Load the official OpenCode JSON Schema
|
|
13
|
+
const __dirname = (0, path_2.dirname)(__filename);
|
|
14
|
+
const schemaPath = (0, path_1.join)(__dirname, '..', 'schemas', 'opencode-schema.json');
|
|
15
|
+
let ajv;
|
|
16
|
+
let openCodeSchema;
|
|
17
|
+
let validateFunction;
|
|
18
|
+
try {
|
|
19
|
+
const schemaContent = (0, fs_1.readFileSync)(schemaPath, 'utf-8');
|
|
20
|
+
openCodeSchema = JSON.parse(schemaContent);
|
|
21
|
+
// Initialize AJV with formats and options
|
|
22
|
+
ajv = new ajv_1.default({
|
|
23
|
+
allErrors: true,
|
|
24
|
+
verbose: true,
|
|
25
|
+
strict: false,
|
|
26
|
+
allowUnionTypes: true,
|
|
27
|
+
removeAdditional: false,
|
|
28
|
+
});
|
|
29
|
+
// Add JSON Schema formats
|
|
30
|
+
(0, ajv_formats_1.default)(ajv);
|
|
31
|
+
// Compile the schema
|
|
32
|
+
validateFunction = ajv.compile(openCodeSchema);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Failed to load OpenCode schema:', error);
|
|
36
|
+
throw new Error('Could not initialize OpenCode validator');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate OpenCode configuration against the official JSON Schema
|
|
40
|
+
*/
|
|
41
|
+
function validateOpenCodeConfig(config) {
|
|
42
|
+
var _a;
|
|
43
|
+
try {
|
|
44
|
+
if (!validateFunction) {
|
|
45
|
+
return { valid: false, errors: ['Schema validator not initialized'] };
|
|
46
|
+
}
|
|
47
|
+
const isValid = validateFunction(config);
|
|
48
|
+
if (isValid) {
|
|
49
|
+
return { valid: true };
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const errors = ((_a = validateFunction.errors) === null || _a === void 0 ? void 0 : _a.map((err) => {
|
|
53
|
+
const path = err.instancePath || err.schemaPath || 'root';
|
|
54
|
+
const message = err.message || 'Unknown error';
|
|
55
|
+
return `${path}: ${message}`;
|
|
56
|
+
})) || ['Unknown validation error'];
|
|
57
|
+
return { valid: false, errors };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('Validation error:', error);
|
|
62
|
+
return { valid: false, errors: ['Validation process failed'] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.validateOpenCodeConfig = validateOpenCodeConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Fix common OpenCode configuration issues
|
|
68
|
+
*/
|
|
69
|
+
function fixOpenCodeConfig(config) {
|
|
70
|
+
const fixed = Object.assign({}, config);
|
|
71
|
+
// Fix tools.compact - should be boolean, not object
|
|
72
|
+
if (fixed.tools && typeof fixed.tools.compact === 'object') {
|
|
73
|
+
console.warn('⚠️ Converting tools.compact from object to boolean');
|
|
74
|
+
// If it has properties, assume it should be enabled
|
|
75
|
+
fixed.tools.compact = true;
|
|
76
|
+
}
|
|
77
|
+
// Remove invalid properties
|
|
78
|
+
const invalidProps = ['maxTokens', 'contextWindow'];
|
|
79
|
+
invalidProps.forEach((prop) => {
|
|
80
|
+
if (fixed[prop] !== undefined) {
|
|
81
|
+
console.warn(`⚠️ Removing invalid property: ${prop}`);
|
|
82
|
+
delete fixed[prop];
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Fix provider models with invalid properties
|
|
86
|
+
if (fixed.provider) {
|
|
87
|
+
Object.values(fixed.provider).forEach((provider) => {
|
|
88
|
+
if (provider === null || provider === void 0 ? void 0 : provider.models) {
|
|
89
|
+
Object.values(provider.models).forEach((model) => {
|
|
90
|
+
if (model && typeof model === 'object') {
|
|
91
|
+
// Move maxTokens/contextWindow to proper structure if needed
|
|
92
|
+
if (model.maxTokens || model.contextWindow) {
|
|
93
|
+
if (!model.limit)
|
|
94
|
+
model.limit = {};
|
|
95
|
+
// Use the larger of maxTokens/contextWindow for context
|
|
96
|
+
const contextValues = [
|
|
97
|
+
model.maxTokens,
|
|
98
|
+
model.contextWindow,
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
if (contextValues.length > 0) {
|
|
101
|
+
const newContext = Math.max(...contextValues);
|
|
102
|
+
if (!model.limit.context || newContext > model.limit.context) {
|
|
103
|
+
model.limit.context = newContext;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Set a reasonable default for output if not present
|
|
107
|
+
// (typically 1/4 to 1/8 of context window)
|
|
108
|
+
if (!model.limit.output && model.limit.context) {
|
|
109
|
+
model.limit.output = Math.floor(model.limit.context / 4);
|
|
110
|
+
}
|
|
111
|
+
delete model.maxTokens;
|
|
112
|
+
delete model.contextWindow;
|
|
113
|
+
console.warn('⚠️ Moved maxTokens/contextWindow to limit.context/output');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return fixed;
|
|
121
|
+
}
|
|
122
|
+
exports.fixOpenCodeConfig = fixOpenCodeConfig;
|
|
@@ -135,7 +135,7 @@ class TokenManager {
|
|
|
135
135
|
this.tokenData = {
|
|
136
136
|
access_token: accessToken,
|
|
137
137
|
refresh_token: refreshToken,
|
|
138
|
-
expires_at: Date.now() +
|
|
138
|
+
expires_at: Date.now() + expiresIn * 1000,
|
|
139
139
|
};
|
|
140
140
|
this.saveToken();
|
|
141
141
|
}
|
|
@@ -148,7 +148,7 @@ class TokenManager {
|
|
|
148
148
|
if (!this.tokenData)
|
|
149
149
|
return;
|
|
150
150
|
this.tokenData.access_token = accessToken;
|
|
151
|
-
this.tokenData.expires_at = Date.now() +
|
|
151
|
+
this.tokenData.expires_at = Date.now() + expiresIn * 1000;
|
|
152
152
|
this.saveToken();
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const vitest_1 = require("vitest");
|
|
13
|
+
const commander_1 = require("commander");
|
|
14
|
+
const chat_1 = require("../../src/commands/chat");
|
|
15
|
+
const chat_service_1 = require("../../src/services/chat-service");
|
|
16
|
+
const default_api_key_1 = require("../../src/utils/default-api-key");
|
|
17
|
+
// Mock dependencies
|
|
18
|
+
vitest_1.vi.mock('../../src/services/chat-service');
|
|
19
|
+
vitest_1.vi.mock('../../src/utils/default-api-key');
|
|
20
|
+
vitest_1.vi.mock('readline', () => ({
|
|
21
|
+
createInterface: vitest_1.vi.fn(() => ({
|
|
22
|
+
question: vitest_1.vi.fn(),
|
|
23
|
+
close: vitest_1.vi.fn(),
|
|
24
|
+
})),
|
|
25
|
+
}));
|
|
26
|
+
(0, vitest_1.describe)('Chat Commands', () => {
|
|
27
|
+
let program;
|
|
28
|
+
let mockChatService;
|
|
29
|
+
let mockDefaultApiKeyManager;
|
|
30
|
+
(0, vitest_1.beforeEach)(() => {
|
|
31
|
+
program = new commander_1.Command();
|
|
32
|
+
// Mock ChatService
|
|
33
|
+
mockChatService = {
|
|
34
|
+
createCompletion: vitest_1.vi.fn(),
|
|
35
|
+
listModels: vitest_1.vi.fn(),
|
|
36
|
+
};
|
|
37
|
+
vitest_1.vi.mocked(chat_service_1.ChatService.getInstance).mockReturnValue(mockChatService);
|
|
38
|
+
// Mock DefaultApiKeyManager
|
|
39
|
+
mockDefaultApiKeyManager = {
|
|
40
|
+
getDefaultApiKeyData: vitest_1.vi.fn(),
|
|
41
|
+
promptForDefaultApiKey: vitest_1.vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
vitest_1.vi.mocked(default_api_key_1.DefaultApiKeyManager.getInstance).mockReturnValue(mockDefaultApiKeyManager);
|
|
44
|
+
(0, chat_1.registerChatCommands)(program);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.afterEach)(() => {
|
|
47
|
+
vitest_1.vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)('chat run command', () => {
|
|
50
|
+
(0, vitest_1.it)('should use openai/gpt-oss as default model', () => {
|
|
51
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
52
|
+
const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'run');
|
|
53
|
+
(0, vitest_1.expect)(runCommand).toBeDefined();
|
|
54
|
+
// Check the help text which contains the default model
|
|
55
|
+
const helpText = runCommand === null || runCommand === void 0 ? void 0 : runCommand.helpInformation();
|
|
56
|
+
(0, vitest_1.expect)(helpText).toContain('openai/gpt-oss');
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)('should have streaming enabled by default', () => {
|
|
59
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
60
|
+
const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'run');
|
|
61
|
+
(0, vitest_1.expect)(runCommand).toBeDefined();
|
|
62
|
+
// Check that the option is --no-stream (meaning streaming is default)
|
|
63
|
+
const streamOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--no-stream');
|
|
64
|
+
(0, vitest_1.expect)(streamOption).toBeDefined();
|
|
65
|
+
(0, vitest_1.expect)(streamOption === null || streamOption === void 0 ? void 0 : streamOption.description).toContain('Disable streaming');
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)('should create completion with correct default options', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
// Mock API key
|
|
69
|
+
process.env.BERGET_API_KEY = 'test-key';
|
|
70
|
+
// Mock successful completion
|
|
71
|
+
mockChatService.createCompletion.mockResolvedValue({
|
|
72
|
+
choices: [
|
|
73
|
+
{
|
|
74
|
+
message: { content: 'Test response' },
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
// This would normally test the actual command execution
|
|
79
|
+
// but since it involves readline interaction, we just verify
|
|
80
|
+
// that the service would be called with correct defaults
|
|
81
|
+
(0, vitest_1.expect)(mockChatService.createCompletion).not.toHaveBeenCalled();
|
|
82
|
+
// Clean up
|
|
83
|
+
delete process.env.BERGET_API_KEY;
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
(0, vitest_1.describe)('chat list command', () => {
|
|
87
|
+
(0, vitest_1.it)('should list available models', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const mockModels = {
|
|
89
|
+
data: [
|
|
90
|
+
{
|
|
91
|
+
id: 'gpt-oss',
|
|
92
|
+
owned_by: 'openai',
|
|
93
|
+
active: true,
|
|
94
|
+
capabilities: {
|
|
95
|
+
vision: false,
|
|
96
|
+
function_calling: true,
|
|
97
|
+
json_mode: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
mockChatService.listModels.mockResolvedValue(mockModels);
|
|
103
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
|
|
104
|
+
const listCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'list');
|
|
105
|
+
(0, vitest_1.expect)(listCommand).toBeDefined();
|
|
106
|
+
(0, vitest_1.expect)(listCommand === null || listCommand === void 0 ? void 0 : listCommand.description()).toBe('List available chat models');
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
});
|