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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAuthenticatedClient
|
|
1
|
+
import { createAuthenticatedClient } from '../client'
|
|
2
2
|
import { logger } from '../utils/logger'
|
|
3
3
|
|
|
4
4
|
export interface ChatMessage {
|
|
@@ -323,28 +323,15 @@ export class ChatService {
|
|
|
323
323
|
options: any,
|
|
324
324
|
headers: Record<string, string>
|
|
325
325
|
): Promise<any> {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const url = new URL(`${API_BASE_URL}/v1/chat/completions`)
|
|
330
|
-
|
|
331
|
-
// Debug the headers and options
|
|
332
|
-
logger.debug('Streaming headers:')
|
|
333
|
-
logger.debug(JSON.stringify(headers, null, 2))
|
|
334
|
-
|
|
335
|
-
logger.debug('Streaming options:')
|
|
336
|
-
logger.debug(
|
|
337
|
-
JSON.stringify(
|
|
338
|
-
{
|
|
339
|
-
...options,
|
|
340
|
-
onChunk: options.onChunk ? 'function present' : 'no function',
|
|
341
|
-
},
|
|
342
|
-
null,
|
|
343
|
-
2
|
|
344
|
-
)
|
|
345
|
-
)
|
|
326
|
+
// Use the same base URL as the client
|
|
327
|
+
const baseUrl = process.env.API_BASE_URL || 'https://api.berget.ai'
|
|
328
|
+
const url = new URL(`${baseUrl}/v1/chat/completions`)
|
|
346
329
|
|
|
347
330
|
try {
|
|
331
|
+
logger.debug(`Making streaming request to: ${url.toString()}`)
|
|
332
|
+
logger.debug(`Headers:`, JSON.stringify(headers, null, 2))
|
|
333
|
+
logger.debug(`Body:`, JSON.stringify(options, null, 2))
|
|
334
|
+
|
|
348
335
|
// Make fetch request directly to handle streaming
|
|
349
336
|
const response = await fetch(url.toString(), {
|
|
350
337
|
method: 'POST',
|
|
@@ -356,14 +343,17 @@ export class ChatService {
|
|
|
356
343
|
body: JSON.stringify(options),
|
|
357
344
|
})
|
|
358
345
|
|
|
346
|
+
logger.debug(`Response status: ${response.status}`)
|
|
347
|
+
logger.debug(`Response headers:`, JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2))
|
|
348
|
+
|
|
359
349
|
if (!response.ok) {
|
|
360
350
|
const errorText = await response.text()
|
|
361
351
|
logger.error(
|
|
362
352
|
`Stream request failed: ${response.status} ${response.statusText}`
|
|
363
353
|
)
|
|
364
|
-
logger.
|
|
354
|
+
logger.error(`Error response: ${errorText}`)
|
|
365
355
|
throw new Error(
|
|
366
|
-
`Stream request failed: ${response.status} ${response.statusText}`
|
|
356
|
+
`Stream request failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
367
357
|
)
|
|
368
358
|
}
|
|
369
359
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { marked } from 'marked'
|
|
2
|
+
import TerminalRenderer from 'marked-terminal'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
|
|
5
|
+
// Configure marked to use the terminal renderer
|
|
6
|
+
marked.setOptions({
|
|
7
|
+
renderer: new TerminalRenderer({
|
|
8
|
+
// Customize the rendering options
|
|
9
|
+
code: chalk.cyan,
|
|
10
|
+
blockquote: chalk.gray.italic,
|
|
11
|
+
table: chalk.white,
|
|
12
|
+
listitem: chalk.yellow,
|
|
13
|
+
strong: chalk.bold,
|
|
14
|
+
em: chalk.italic,
|
|
15
|
+
heading: chalk.bold.blueBright,
|
|
16
|
+
hr: chalk.gray,
|
|
17
|
+
link: chalk.blue.underline,
|
|
18
|
+
// Adjust the width to fit the terminal
|
|
19
|
+
width: process.stdout.columns || 80,
|
|
20
|
+
// Customize code block rendering
|
|
21
|
+
codespan: chalk.cyan
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render markdown text to terminal-friendly formatted text
|
|
27
|
+
* @param markdown The markdown text to render
|
|
28
|
+
* @returns Formatted text for terminal display
|
|
29
|
+
*/
|
|
30
|
+
export function renderMarkdown(markdown: string): string {
|
|
31
|
+
if (!markdown) return ''
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Convert markdown to terminal-friendly text
|
|
35
|
+
return marked(markdown)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// If rendering fails, return the original text
|
|
38
|
+
console.error(`Error rendering markdown: ${error}`)
|
|
39
|
+
return markdown
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a string contains markdown formatting
|
|
45
|
+
* @param text The text to check
|
|
46
|
+
* @returns True if the text contains markdown formatting
|
|
47
|
+
*/
|
|
48
|
+
export function containsMarkdown(text: string): boolean {
|
|
49
|
+
if (!text) return false
|
|
50
|
+
|
|
51
|
+
// Check for common markdown patterns
|
|
52
|
+
const markdownPatterns = [
|
|
53
|
+
/^#+\s+/m, // Headers
|
|
54
|
+
/\*\*.*?\*\*/, // Bold
|
|
55
|
+
/\*.*?\*/, // Italic
|
|
56
|
+
/`.*?`/, // Inline code
|
|
57
|
+
/```[\s\S]*?```/, // Code blocks
|
|
58
|
+
/\[.*?\]\(.*?\)/, // Links
|
|
59
|
+
/^\s*[-*+]\s+/m, // Lists
|
|
60
|
+
/^\s*\d+\.\s+/m, // Numbered lists
|
|
61
|
+
/^\s*>\s+/m, // Blockquotes
|
|
62
|
+
/\|.*\|.*\|/, // Tables
|
|
63
|
+
/^---+$/m, // Horizontal rules
|
|
64
|
+
/^===+$/m // Alternative headers
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
return markdownPatterns.some(pattern => pattern.test(text))
|
|
68
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { registerChatCommands } from '../../src/commands/chat'
|
|
4
|
+
import { ChatService } from '../../src/services/chat-service'
|
|
5
|
+
import { DefaultApiKeyManager } from '../../src/utils/default-api-key'
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
vi.mock('../../src/services/chat-service')
|
|
9
|
+
vi.mock('../../src/utils/default-api-key')
|
|
10
|
+
vi.mock('readline', () => ({
|
|
11
|
+
createInterface: vi.fn(() => ({
|
|
12
|
+
question: vi.fn(),
|
|
13
|
+
close: vi.fn()
|
|
14
|
+
}))
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('Chat Commands', () => {
|
|
18
|
+
let program: Command
|
|
19
|
+
let mockChatService: any
|
|
20
|
+
let mockDefaultApiKeyManager: any
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
program = new Command()
|
|
24
|
+
|
|
25
|
+
// Mock ChatService
|
|
26
|
+
mockChatService = {
|
|
27
|
+
createCompletion: vi.fn(),
|
|
28
|
+
listModels: vi.fn()
|
|
29
|
+
}
|
|
30
|
+
vi.mocked(ChatService.getInstance).mockReturnValue(mockChatService)
|
|
31
|
+
|
|
32
|
+
// Mock DefaultApiKeyManager
|
|
33
|
+
mockDefaultApiKeyManager = {
|
|
34
|
+
getDefaultApiKeyData: vi.fn(),
|
|
35
|
+
promptForDefaultApiKey: vi.fn()
|
|
36
|
+
}
|
|
37
|
+
vi.mocked(DefaultApiKeyManager.getInstance).mockReturnValue(mockDefaultApiKeyManager)
|
|
38
|
+
|
|
39
|
+
registerChatCommands(program)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('chat run command', () => {
|
|
47
|
+
it('should use openai/gpt-oss as default model', () => {
|
|
48
|
+
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat')
|
|
49
|
+
const runCommand = chatCommand?.commands.find(cmd => cmd.name() === 'run')
|
|
50
|
+
|
|
51
|
+
expect(runCommand).toBeDefined()
|
|
52
|
+
|
|
53
|
+
// Check the help text which contains the default model
|
|
54
|
+
const helpText = runCommand?.helpInformation()
|
|
55
|
+
expect(helpText).toContain('openai/gpt-oss')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should have streaming enabled by default', () => {
|
|
59
|
+
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat')
|
|
60
|
+
const runCommand = chatCommand?.commands.find(cmd => cmd.name() === 'run')
|
|
61
|
+
|
|
62
|
+
expect(runCommand).toBeDefined()
|
|
63
|
+
|
|
64
|
+
// Check that the option is --no-stream (meaning streaming is default)
|
|
65
|
+
const streamOption = runCommand?.options.find(opt => opt.long === '--no-stream')
|
|
66
|
+
expect(streamOption).toBeDefined()
|
|
67
|
+
expect(streamOption?.description).toContain('Disable streaming')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should create completion with correct default options', async () => {
|
|
71
|
+
// Mock API key
|
|
72
|
+
process.env.BERGET_API_KEY = 'test-key'
|
|
73
|
+
|
|
74
|
+
// Mock successful completion
|
|
75
|
+
mockChatService.createCompletion.mockResolvedValue({
|
|
76
|
+
choices: [{
|
|
77
|
+
message: { content: 'Test response' }
|
|
78
|
+
}]
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// This would normally test the actual command execution
|
|
82
|
+
// but since it involves readline interaction, we just verify
|
|
83
|
+
// that the service would be called with correct defaults
|
|
84
|
+
expect(mockChatService.createCompletion).not.toHaveBeenCalled()
|
|
85
|
+
|
|
86
|
+
// Clean up
|
|
87
|
+
delete process.env.BERGET_API_KEY
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('chat list command', () => {
|
|
92
|
+
it('should list available models', async () => {
|
|
93
|
+
const mockModels = {
|
|
94
|
+
data: [
|
|
95
|
+
{
|
|
96
|
+
id: 'gpt-oss',
|
|
97
|
+
owned_by: 'openai',
|
|
98
|
+
active: true,
|
|
99
|
+
capabilities: {
|
|
100
|
+
vision: false,
|
|
101
|
+
function_calling: true,
|
|
102
|
+
json_mode: true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mockChatService.listModels.mockResolvedValue(mockModels)
|
|
109
|
+
|
|
110
|
+
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat')
|
|
111
|
+
const listCommand = chatCommand?.commands.find(cmd => cmd.name() === 'list')
|
|
112
|
+
|
|
113
|
+
expect(listCommand).toBeDefined()
|
|
114
|
+
expect(listCommand?.description()).toBe('List available chat models')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|