berget 1.3.0 → 1.4.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/.github/workflows/publish.yml +56 -0
- package/.github/workflows/test.yml +38 -0
- package/README.md +177 -38
- package/dist/package.json +12 -2
- package/dist/src/commands/chat.js +183 -22
- package/dist/src/commands/models.js +2 -2
- package/dist/src/services/chat-service.js +10 -10
- package/dist/src/utils/markdown-renderer.js +73 -0
- package/dist/tests/commands/chat.test.js +107 -0
- package/dist/vitest.config.js +9 -0
- package/examples/README.md +95 -0
- package/examples/ai-review.sh +30 -0
- package/examples/install-global-security-hook.sh +170 -0
- package/examples/security-check.sh +102 -0
- package/examples/smart-commit.sh +26 -0
- package/package.json +12 -2
- package/src/commands/chat.ts +190 -23
- package/src/commands/models.ts +4 -4
- package/src/services/chat-service.ts +13 -23
- package/src/utils/markdown-renderer.ts +68 -0
- package/tests/commands/chat.test.ts +117 -0
- package/vitest.config.ts +8 -0
|
@@ -8,6 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
12
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
13
|
+
var m = o[Symbol.asyncIterator], i;
|
|
14
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
15
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
16
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
17
|
+
};
|
|
11
18
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
19
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
20
|
};
|
|
@@ -21,6 +28,7 @@ const api_key_service_1 = require("../services/api-key-service");
|
|
|
21
28
|
const auth_service_1 = require("../services/auth-service");
|
|
22
29
|
const error_handler_1 = require("../utils/error-handler");
|
|
23
30
|
const default_api_key_1 = require("../utils/default-api-key");
|
|
31
|
+
const markdown_renderer_1 = require("../utils/markdown-renderer");
|
|
24
32
|
/**
|
|
25
33
|
* Helper function to get user confirmation
|
|
26
34
|
*/
|
|
@@ -48,14 +56,16 @@ function registerChatCommands(program) {
|
|
|
48
56
|
chat
|
|
49
57
|
.command(command_structure_1.SUBCOMMANDS.CHAT.RUN)
|
|
50
58
|
.description('Run a chat session with a specified model')
|
|
51
|
-
.argument('[model]', 'Model to use (default:
|
|
59
|
+
.argument('[model]', 'Model to use (default: openai/gpt-oss)')
|
|
60
|
+
.argument('[message]', 'Message to send directly (skips interactive mode)')
|
|
52
61
|
.option('-s, --system <message>', 'System message')
|
|
53
62
|
.option('-t, --temperature <temp>', 'Temperature (0-1)', parseFloat)
|
|
54
63
|
.option('-m, --max-tokens <tokens>', 'Maximum tokens to generate', parseInt)
|
|
55
64
|
.option('-k, --api-key <key>', 'API key to use for this chat session')
|
|
56
65
|
.option('--api-key-id <id>', 'ID of the API key to use from your saved keys')
|
|
57
|
-
.option('--stream', '
|
|
58
|
-
.action((options) => __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
.option('--no-stream', 'Disable streaming (streaming is enabled by default)')
|
|
67
|
+
.action((model, message, options) => __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
var _a, e_1, _b, _c;
|
|
59
69
|
try {
|
|
60
70
|
const chatService = chat_service_1.ChatService.getInstance();
|
|
61
71
|
// Check if we have an API key or need to get one
|
|
@@ -167,11 +177,6 @@ function registerChatCommands(program) {
|
|
|
167
177
|
return;
|
|
168
178
|
}
|
|
169
179
|
}
|
|
170
|
-
// Set up readline interface for user input
|
|
171
|
-
const rl = readline_1.default.createInterface({
|
|
172
|
-
input: process.stdin,
|
|
173
|
-
output: process.stdout,
|
|
174
|
-
});
|
|
175
180
|
// Prepare messages array
|
|
176
181
|
const messages = [];
|
|
177
182
|
// Add system message if provided
|
|
@@ -181,12 +186,134 @@ function registerChatCommands(program) {
|
|
|
181
186
|
content: options.system,
|
|
182
187
|
});
|
|
183
188
|
}
|
|
189
|
+
// Check if input is being piped in
|
|
190
|
+
let inputMessage = message;
|
|
191
|
+
let stdinContent = '';
|
|
192
|
+
if (!process.stdin.isTTY) {
|
|
193
|
+
// Read from stdin (piped input)
|
|
194
|
+
const chunks = [];
|
|
195
|
+
try {
|
|
196
|
+
for (var _d = true, _e = __asyncValues(process.stdin), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
197
|
+
_c = _f.value;
|
|
198
|
+
_d = false;
|
|
199
|
+
const chunk = _c;
|
|
200
|
+
chunks.push(chunk);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
204
|
+
finally {
|
|
205
|
+
try {
|
|
206
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
207
|
+
}
|
|
208
|
+
finally { if (e_1) throw e_1.error; }
|
|
209
|
+
}
|
|
210
|
+
stdinContent = Buffer.concat(chunks).toString('utf8').trim();
|
|
211
|
+
}
|
|
212
|
+
// Combine stdin content with message if both exist
|
|
213
|
+
if (stdinContent && message) {
|
|
214
|
+
inputMessage = `${stdinContent}\n\n${message}`;
|
|
215
|
+
}
|
|
216
|
+
else if (stdinContent && !message) {
|
|
217
|
+
inputMessage = stdinContent;
|
|
218
|
+
}
|
|
219
|
+
// If a message is provided (either as argument, from stdin, or both), send it directly and exit
|
|
220
|
+
if (inputMessage) {
|
|
221
|
+
// Add user message
|
|
222
|
+
messages.push({
|
|
223
|
+
role: 'user',
|
|
224
|
+
content: inputMessage,
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
// Call the API
|
|
228
|
+
const completionOptions = {
|
|
229
|
+
model: model || 'openai/gpt-oss',
|
|
230
|
+
messages: messages,
|
|
231
|
+
temperature: options.temperature !== undefined ? options.temperature : 0.7,
|
|
232
|
+
max_tokens: options.maxTokens || 4096,
|
|
233
|
+
stream: options.stream !== false
|
|
234
|
+
};
|
|
235
|
+
// Only add apiKey if it actually exists
|
|
236
|
+
if (apiKey) {
|
|
237
|
+
completionOptions.apiKey = apiKey;
|
|
238
|
+
}
|
|
239
|
+
// Add streaming support (now default)
|
|
240
|
+
if (completionOptions.stream) {
|
|
241
|
+
let assistantResponse = '';
|
|
242
|
+
// Stream the response in real-time
|
|
243
|
+
completionOptions.onChunk = (chunk) => {
|
|
244
|
+
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta && chunk.choices[0].delta.content) {
|
|
245
|
+
const content = chunk.choices[0].delta.content;
|
|
246
|
+
try {
|
|
247
|
+
process.stdout.write(content);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
// Handle EPIPE errors gracefully (when pipe is closed)
|
|
251
|
+
if (error.code === 'EPIPE') {
|
|
252
|
+
// Stop streaming if the pipe is closed
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
assistantResponse += content;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
yield chatService.createCompletion(completionOptions);
|
|
262
|
+
}
|
|
263
|
+
catch (streamError) {
|
|
264
|
+
console.error(chalk_1.default.red('\nStreaming error:'), streamError);
|
|
265
|
+
// Fallback to non-streaming if streaming fails
|
|
266
|
+
console.log(chalk_1.default.yellow('Falling back to non-streaming mode...'));
|
|
267
|
+
completionOptions.stream = false;
|
|
268
|
+
delete completionOptions.onChunk;
|
|
269
|
+
const response = yield chatService.createCompletion(completionOptions);
|
|
270
|
+
if (response && response.choices && response.choices[0] && response.choices[0].message) {
|
|
271
|
+
assistantResponse = response.choices[0].message.content;
|
|
272
|
+
console.log(assistantResponse);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
console.log(); // Add newline at the end
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const response = yield chatService.createCompletion(completionOptions);
|
|
279
|
+
// Check if response has the expected structure
|
|
280
|
+
if (!response ||
|
|
281
|
+
!response.choices ||
|
|
282
|
+
!response.choices[0] ||
|
|
283
|
+
!response.choices[0].message) {
|
|
284
|
+
console.error(chalk_1.default.red('Error: Unexpected response format from API'));
|
|
285
|
+
console.error(chalk_1.default.red('Response:', JSON.stringify(response, null, 2)));
|
|
286
|
+
throw new Error('Unexpected response format from API');
|
|
287
|
+
}
|
|
288
|
+
// Get assistant's response
|
|
289
|
+
const assistantMessage = response.choices[0].message.content;
|
|
290
|
+
// Display the response
|
|
291
|
+
if ((0, markdown_renderer_1.containsMarkdown)(assistantMessage)) {
|
|
292
|
+
console.log((0, markdown_renderer_1.renderMarkdown)(assistantMessage));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.log(assistantMessage);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error(chalk_1.default.red('Error: Failed to get response'));
|
|
301
|
+
if (error instanceof Error) {
|
|
302
|
+
console.error(chalk_1.default.red(error.message));
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Set up readline interface for user input (only for interactive mode)
|
|
308
|
+
const rl = readline_1.default.createInterface({
|
|
309
|
+
input: process.stdin,
|
|
310
|
+
output: process.stdout,
|
|
311
|
+
});
|
|
184
312
|
console.log(chalk_1.default.cyan('Chat with Berget AI (type "exit" to quit)'));
|
|
185
313
|
console.log(chalk_1.default.cyan('----------------------------------------'));
|
|
186
314
|
// Start the conversation loop
|
|
187
315
|
const askQuestion = () => {
|
|
188
316
|
rl.question(chalk_1.default.green('You: '), (input) => __awaiter(this, void 0, void 0, function* () {
|
|
189
|
-
var _a;
|
|
190
317
|
// Check if user wants to exit
|
|
191
318
|
if (input.toLowerCase() === 'exit') {
|
|
192
319
|
console.log(chalk_1.default.cyan('Goodbye!'));
|
|
@@ -201,28 +328,53 @@ function registerChatCommands(program) {
|
|
|
201
328
|
try {
|
|
202
329
|
// Call the API
|
|
203
330
|
const completionOptions = {
|
|
204
|
-
model:
|
|
331
|
+
model: model || 'openai/gpt-oss',
|
|
205
332
|
messages: messages,
|
|
206
333
|
temperature: options.temperature !== undefined ? options.temperature : 0.7,
|
|
207
334
|
max_tokens: options.maxTokens || 4096,
|
|
208
|
-
stream: options.stream
|
|
335
|
+
stream: options.stream !== false
|
|
209
336
|
};
|
|
210
337
|
// Only add apiKey if it actually exists
|
|
211
338
|
if (apiKey) {
|
|
212
339
|
completionOptions.apiKey = apiKey;
|
|
213
340
|
}
|
|
214
|
-
// Add streaming support
|
|
215
|
-
if (
|
|
341
|
+
// Add streaming support (now default)
|
|
342
|
+
if (completionOptions.stream) {
|
|
216
343
|
let assistantResponse = '';
|
|
217
|
-
|
|
344
|
+
console.log(chalk_1.default.blue('Assistant: '));
|
|
345
|
+
// Stream the response in real-time
|
|
218
346
|
completionOptions.onChunk = (chunk) => {
|
|
219
347
|
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta && chunk.choices[0].delta.content) {
|
|
220
348
|
const content = chunk.choices[0].delta.content;
|
|
221
|
-
|
|
349
|
+
try {
|
|
350
|
+
process.stdout.write(content);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
// Handle EPIPE errors gracefully (when pipe is closed)
|
|
354
|
+
if (error.code === 'EPIPE') {
|
|
355
|
+
// Stop streaming if the pipe is closed
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
222
360
|
assistantResponse += content;
|
|
223
361
|
}
|
|
224
362
|
};
|
|
225
|
-
|
|
363
|
+
try {
|
|
364
|
+
yield chatService.createCompletion(completionOptions);
|
|
365
|
+
}
|
|
366
|
+
catch (streamError) {
|
|
367
|
+
console.error(chalk_1.default.red('\nStreaming error:'), streamError);
|
|
368
|
+
// Fallback to non-streaming if streaming fails
|
|
369
|
+
console.log(chalk_1.default.yellow('Falling back to non-streaming mode...'));
|
|
370
|
+
completionOptions.stream = false;
|
|
371
|
+
delete completionOptions.onChunk;
|
|
372
|
+
const response = yield chatService.createCompletion(completionOptions);
|
|
373
|
+
if (response && response.choices && response.choices[0] && response.choices[0].message) {
|
|
374
|
+
assistantResponse = response.choices[0].message.content;
|
|
375
|
+
console.log(assistantResponse);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
226
378
|
console.log('\n');
|
|
227
379
|
// Add assistant response to messages
|
|
228
380
|
messages.push({
|
|
@@ -256,7 +408,14 @@ function registerChatCommands(program) {
|
|
|
256
408
|
content: assistantMessage,
|
|
257
409
|
});
|
|
258
410
|
// Display the response
|
|
259
|
-
console.log(chalk_1.default.blue('Assistant: ')
|
|
411
|
+
console.log(chalk_1.default.blue('Assistant: '));
|
|
412
|
+
// Check if the response contains markdown and render it if it does
|
|
413
|
+
if ((0, markdown_renderer_1.containsMarkdown)(assistantMessage)) {
|
|
414
|
+
console.log((0, markdown_renderer_1.renderMarkdown)(assistantMessage));
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log(assistantMessage);
|
|
418
|
+
}
|
|
260
419
|
console.log(); // Empty line for better readability
|
|
261
420
|
// Continue the conversation
|
|
262
421
|
askQuestion();
|
|
@@ -333,11 +492,12 @@ function registerChatCommands(program) {
|
|
|
333
492
|
}
|
|
334
493
|
console.log(chalk_1.default.bold('Available Chat Models:'));
|
|
335
494
|
console.log(chalk_1.default.dim('─'.repeat(70)));
|
|
336
|
-
console.log(chalk_1.default.dim('MODEL ID'.padEnd(
|
|
337
|
-
chalk_1.default.dim('OWNER'.padEnd(25)) +
|
|
495
|
+
console.log(chalk_1.default.dim('MODEL ID'.padEnd(40)) +
|
|
338
496
|
chalk_1.default.dim('CAPABILITIES'));
|
|
339
497
|
console.log(chalk_1.default.dim('─'.repeat(70)));
|
|
340
|
-
|
|
498
|
+
// Filter to only show active models
|
|
499
|
+
const activeModels = models.data.filter((model) => model.active === true);
|
|
500
|
+
activeModels.forEach((model) => {
|
|
341
501
|
const capabilities = [];
|
|
342
502
|
if (model.capabilities.vision)
|
|
343
503
|
capabilities.push('vision');
|
|
@@ -345,8 +505,9 @@ function registerChatCommands(program) {
|
|
|
345
505
|
capabilities.push('function_calling');
|
|
346
506
|
if (model.capabilities.json_mode)
|
|
347
507
|
capabilities.push('json_mode');
|
|
348
|
-
|
|
349
|
-
|
|
508
|
+
// Format model ID in Huggingface compatible format (owner/model)
|
|
509
|
+
const modelId = `${model.owned_by.toLowerCase()}/${model.id}`.padEnd(40);
|
|
510
|
+
console.log(modelId +
|
|
350
511
|
capabilities.join(', '));
|
|
351
512
|
});
|
|
352
513
|
}
|
|
@@ -44,7 +44,7 @@ function registerModelCommands(program) {
|
|
|
44
44
|
throw new Error(JSON.stringify(error));
|
|
45
45
|
response = data;
|
|
46
46
|
console.log('Available Models:');
|
|
47
|
-
console.log('ID
|
|
47
|
+
console.log('ID OWNED BY CAPABILITIES');
|
|
48
48
|
// Ensure response has the expected structure
|
|
49
49
|
const modelData = response;
|
|
50
50
|
if (modelData.data) {
|
|
@@ -56,7 +56,7 @@ function registerModelCommands(program) {
|
|
|
56
56
|
capabilities.push('function_calling');
|
|
57
57
|
if (model.capabilities.json_mode)
|
|
58
58
|
capabilities.push('json_mode');
|
|
59
|
-
console.log(`${model.
|
|
59
|
+
console.log(`${model.root.padEnd(50)} ${model.owned_by.padEnd(24)} ${capabilities.join(', ')}`);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -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');
|
|
@@ -0,0 +1,73 @@
|
|
|
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.containsMarkdown = exports.renderMarkdown = void 0;
|
|
7
|
+
const marked_1 = require("marked");
|
|
8
|
+
const marked_terminal_1 = __importDefault(require("marked-terminal"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
// Configure marked to use the terminal renderer
|
|
11
|
+
marked_1.marked.setOptions({
|
|
12
|
+
renderer: new marked_terminal_1.default({
|
|
13
|
+
// Customize the rendering options
|
|
14
|
+
code: chalk_1.default.cyan,
|
|
15
|
+
blockquote: chalk_1.default.gray.italic,
|
|
16
|
+
table: chalk_1.default.white,
|
|
17
|
+
listitem: chalk_1.default.yellow,
|
|
18
|
+
strong: chalk_1.default.bold,
|
|
19
|
+
em: chalk_1.default.italic,
|
|
20
|
+
heading: chalk_1.default.bold.blueBright,
|
|
21
|
+
hr: chalk_1.default.gray,
|
|
22
|
+
link: chalk_1.default.blue.underline,
|
|
23
|
+
// Adjust the width to fit the terminal
|
|
24
|
+
width: process.stdout.columns || 80,
|
|
25
|
+
// Customize code block rendering
|
|
26
|
+
codespan: chalk_1.default.cyan
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Render markdown text to terminal-friendly formatted text
|
|
31
|
+
* @param markdown The markdown text to render
|
|
32
|
+
* @returns Formatted text for terminal display
|
|
33
|
+
*/
|
|
34
|
+
function renderMarkdown(markdown) {
|
|
35
|
+
if (!markdown)
|
|
36
|
+
return '';
|
|
37
|
+
try {
|
|
38
|
+
// Convert markdown to terminal-friendly text
|
|
39
|
+
return (0, marked_1.marked)(markdown);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// If rendering fails, return the original text
|
|
43
|
+
console.error(`Error rendering markdown: ${error}`);
|
|
44
|
+
return markdown;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.renderMarkdown = renderMarkdown;
|
|
48
|
+
/**
|
|
49
|
+
* Check if a string contains markdown formatting
|
|
50
|
+
* @param text The text to check
|
|
51
|
+
* @returns True if the text contains markdown formatting
|
|
52
|
+
*/
|
|
53
|
+
function containsMarkdown(text) {
|
|
54
|
+
if (!text)
|
|
55
|
+
return false;
|
|
56
|
+
// Check for common markdown patterns
|
|
57
|
+
const markdownPatterns = [
|
|
58
|
+
/^#+\s+/m, // Headers
|
|
59
|
+
/\*\*.*?\*\*/, // Bold
|
|
60
|
+
/\*.*?\*/, // Italic
|
|
61
|
+
/`.*?`/, // Inline code
|
|
62
|
+
/```[\s\S]*?```/, // Code blocks
|
|
63
|
+
/\[.*?\]\(.*?\)/, // Links
|
|
64
|
+
/^\s*[-*+]\s+/m, // Lists
|
|
65
|
+
/^\s*\d+\.\s+/m, // Numbered lists
|
|
66
|
+
/^\s*>\s+/m, // Blockquotes
|
|
67
|
+
/\|.*\|.*\|/, // Tables
|
|
68
|
+
/^---+$/m, // Horizontal rules
|
|
69
|
+
/^===+$/m // Alternative headers
|
|
70
|
+
];
|
|
71
|
+
return markdownPatterns.some(pattern => pattern.test(text));
|
|
72
|
+
}
|
|
73
|
+
exports.containsMarkdown = containsMarkdown;
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
message: { content: 'Test response' }
|
|
74
|
+
}]
|
|
75
|
+
});
|
|
76
|
+
// This would normally test the actual command execution
|
|
77
|
+
// but since it involves readline interaction, we just verify
|
|
78
|
+
// that the service would be called with correct defaults
|
|
79
|
+
(0, vitest_1.expect)(mockChatService.createCompletion).not.toHaveBeenCalled();
|
|
80
|
+
// Clean up
|
|
81
|
+
delete process.env.BERGET_API_KEY;
|
|
82
|
+
}));
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.describe)('chat list command', () => {
|
|
85
|
+
(0, vitest_1.it)('should list available models', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const mockModels = {
|
|
87
|
+
data: [
|
|
88
|
+
{
|
|
89
|
+
id: 'gpt-oss',
|
|
90
|
+
owned_by: 'openai',
|
|
91
|
+
active: true,
|
|
92
|
+
capabilities: {
|
|
93
|
+
vision: false,
|
|
94
|
+
function_calling: true,
|
|
95
|
+
json_mode: true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
mockChatService.listModels.mockResolvedValue(mockModels);
|
|
101
|
+
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat');
|
|
102
|
+
const listCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find(cmd => cmd.name() === 'list');
|
|
103
|
+
(0, vitest_1.expect)(listCommand).toBeDefined();
|
|
104
|
+
(0, vitest_1.expect)(listCommand === null || listCommand === void 0 ? void 0 : listCommand.description()).toBe('List available chat models');
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Berget CLI Examples
|
|
2
|
+
|
|
3
|
+
This folder contains practical examples of how you can use Berget CLI for various automation tasks.
|
|
4
|
+
|
|
5
|
+
## Scripts
|
|
6
|
+
|
|
7
|
+
### smart-commit.sh
|
|
8
|
+
Automatic generation of conventional commit messages based on git diff.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Make the script executable
|
|
12
|
+
chmod +x examples/smart-commit.sh
|
|
13
|
+
|
|
14
|
+
# Use it
|
|
15
|
+
git add .
|
|
16
|
+
./examples/smart-commit.sh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### ai-review.sh
|
|
20
|
+
AI-driven code review that analyzes files for quality, bugs, and security aspects.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Make the script executable
|
|
24
|
+
chmod +x examples/ai-review.sh
|
|
25
|
+
|
|
26
|
+
# Review a file
|
|
27
|
+
./examples/ai-review.sh src/main.js
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### security-check.sh
|
|
31
|
+
Security review of git commits that blocks commits with critical security risks.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Make the script executable
|
|
35
|
+
chmod +x examples/security-check.sh
|
|
36
|
+
|
|
37
|
+
# Run security check
|
|
38
|
+
git add .
|
|
39
|
+
./examples/security-check.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
To use these scripts:
|
|
45
|
+
|
|
46
|
+
1. Copy them to your `~/bin` folder or another location in your PATH
|
|
47
|
+
2. Make them executable with `chmod +x`
|
|
48
|
+
3. Make sure you have Berget CLI installed and configured
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Copy to ~/bin
|
|
52
|
+
cp examples/*.sh ~/bin/
|
|
53
|
+
|
|
54
|
+
# Make them executable
|
|
55
|
+
chmod +x ~/bin/smart-commit.sh ~/bin/ai-review.sh ~/bin/security-check.sh
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Global Security Hook
|
|
59
|
+
|
|
60
|
+
For maximum security, you can install a global git hook that automatically runs security checks before every push:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Install the global security hook
|
|
64
|
+
chmod +x examples/install-global-security-hook.sh
|
|
65
|
+
./examples/install-global-security-hook.sh
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This will:
|
|
69
|
+
- Create a global pre-push hook that runs on all repositories
|
|
70
|
+
- Automatically analyze commits for security vulnerabilities using OWASP Top 20
|
|
71
|
+
- Block pushes with critical security issues
|
|
72
|
+
- Warn about medium-risk issues and allow you to choose
|
|
73
|
+
|
|
74
|
+
The hook will run automatically before every `git push`. To bypass it temporarily (not recommended):
|
|
75
|
+
```bash
|
|
76
|
+
git push --no-verify
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Git Aliases
|
|
80
|
+
|
|
81
|
+
You can also add these as git aliases:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git config --global alias.ai-commit '!~/bin/smart-commit.sh'
|
|
85
|
+
git config --global alias.ai-review '!~/bin/ai-review.sh'
|
|
86
|
+
git config --global alias.security-check '!~/bin/security-check.sh'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then you can use:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git ai-commit
|
|
93
|
+
git ai-review src/main.js
|
|
94
|
+
git security-check
|
|
95
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AI code review using Berget AI
|
|
3
|
+
# Usage: ./ai-review.sh <filename>
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
if [[ $# -eq 0 ]]; then
|
|
7
|
+
echo "Usage: ai-review <file>"
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
FILE="$1"
|
|
12
|
+
|
|
13
|
+
if [[ ! -f "$FILE" ]]; then
|
|
14
|
+
echo "Error: File '$FILE' does not exist"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "🔍 Reviewing $FILE with AI..."
|
|
19
|
+
echo "================================"
|
|
20
|
+
|
|
21
|
+
cat "$FILE" | npx berget chat run openai/gpt-oss "
|
|
22
|
+
Review this code and provide feedback on:
|
|
23
|
+
1. Code quality and readability
|
|
24
|
+
2. Potential bugs or issues
|
|
25
|
+
3. Performance improvements
|
|
26
|
+
4. Best practices
|
|
27
|
+
5. Security aspects
|
|
28
|
+
|
|
29
|
+
Provide concrete suggestions for improvements:
|
|
30
|
+
"
|