berget 1.4.0 → 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/AGENTS.md +184 -0
- package/TODO.md +2 -0
- package/blog-post.md +176 -0
- package/dist/index.js +11 -8
- package/dist/package.json +7 -2
- package/dist/src/commands/api-keys.js +4 -2
- package/dist/src/commands/chat.js +21 -11
- 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/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 +20 -18
- 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/index.ts +67 -32
- package/opencode.json +182 -0
- package/package.json +7 -2
- 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 +149 -107
- 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 +26 -23
- 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 +43 -31
- 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/-27b-it +0 -0
- package/examples/README.md +0 -95
- package/examples/ai-review.sh +0 -30
- package/examples/install-global-security-hook.sh +0 -170
- package/examples/security-check.sh +0 -102
- package/examples/smart-commit.sh +0 -26
|
@@ -17,7 +17,7 @@ export class TokenManager {
|
|
|
17
17
|
private static instance: TokenManager
|
|
18
18
|
private tokenFilePath: string
|
|
19
19
|
private tokenData: TokenData | null = null
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
private constructor() {
|
|
22
22
|
// Set up token file path in user's home directory
|
|
23
23
|
const bergetDir = path.join(os.homedir(), '.berget')
|
|
@@ -27,14 +27,14 @@ export class TokenManager {
|
|
|
27
27
|
this.tokenFilePath = path.join(bergetDir, 'auth.json')
|
|
28
28
|
this.loadToken()
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
public static getInstance(): TokenManager {
|
|
32
32
|
if (!TokenManager.instance) {
|
|
33
33
|
TokenManager.instance = new TokenManager()
|
|
34
34
|
}
|
|
35
35
|
return TokenManager.instance
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
/**
|
|
39
39
|
* Load token data from file
|
|
40
40
|
*/
|
|
@@ -49,14 +49,17 @@ export class TokenManager {
|
|
|
49
49
|
this.tokenData = null
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
/**
|
|
54
54
|
* Save token data to file
|
|
55
55
|
*/
|
|
56
56
|
private saveToken(): void {
|
|
57
57
|
try {
|
|
58
58
|
if (this.tokenData) {
|
|
59
|
-
fs.writeFileSync(
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
this.tokenFilePath,
|
|
61
|
+
JSON.stringify(this.tokenData, null, 2),
|
|
62
|
+
)
|
|
60
63
|
// Set file permissions to be readable only by the owner
|
|
61
64
|
fs.chmodSync(this.tokenFilePath, 0o600)
|
|
62
65
|
} else {
|
|
@@ -69,7 +72,7 @@ export class TokenManager {
|
|
|
69
72
|
logger.error('Failed to save authentication token')
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
|
-
|
|
75
|
+
|
|
73
76
|
/**
|
|
74
77
|
* Get the current access token
|
|
75
78
|
* @returns The access token or null if not available
|
|
@@ -78,7 +81,7 @@ export class TokenManager {
|
|
|
78
81
|
if (!this.tokenData) return null
|
|
79
82
|
return this.tokenData.access_token
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
/**
|
|
83
86
|
* Get the refresh token
|
|
84
87
|
* @returns The refresh token or null if not available
|
|
@@ -87,47 +90,56 @@ export class TokenManager {
|
|
|
87
90
|
if (!this.tokenData) return null
|
|
88
91
|
return this.tokenData.refresh_token
|
|
89
92
|
}
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
/**
|
|
92
95
|
* Check if the access token is expired
|
|
93
96
|
* @returns true if expired or about to expire (within 5 minutes), false otherwise
|
|
94
97
|
*/
|
|
95
98
|
public isTokenExpired(): boolean {
|
|
96
99
|
if (!this.tokenData || !this.tokenData.expires_at) return true
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
try {
|
|
99
102
|
// Consider token expired if it's within 10 minutes of expiration
|
|
100
103
|
// Using a larger buffer to be more proactive about refreshing
|
|
101
104
|
const expirationBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds
|
|
102
|
-
const isExpired =
|
|
103
|
-
|
|
105
|
+
const isExpired =
|
|
106
|
+
Date.now() + expirationBuffer >= this.tokenData.expires_at
|
|
107
|
+
|
|
104
108
|
if (isExpired) {
|
|
105
|
-
logger.debug(
|
|
109
|
+
logger.debug(
|
|
110
|
+
`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(this.tokenData.expires_at).toISOString()}`,
|
|
111
|
+
)
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
return isExpired
|
|
113
|
+
|
|
114
|
+
return isExpired
|
|
109
115
|
} catch (error) {
|
|
110
116
|
// If there's any error checking expiration, assume token is expired
|
|
111
|
-
logger.error(
|
|
112
|
-
|
|
117
|
+
logger.error(
|
|
118
|
+
`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
)
|
|
120
|
+
return true
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
|
-
|
|
123
|
+
|
|
116
124
|
/**
|
|
117
125
|
* Set new token data
|
|
118
126
|
* @param accessToken The new access token
|
|
119
127
|
* @param refreshToken The new refresh token
|
|
120
128
|
* @param expiresIn Expiration time in seconds
|
|
121
129
|
*/
|
|
122
|
-
public setTokens(
|
|
130
|
+
public setTokens(
|
|
131
|
+
accessToken: string,
|
|
132
|
+
refreshToken: string,
|
|
133
|
+
expiresIn: number,
|
|
134
|
+
): void {
|
|
123
135
|
this.tokenData = {
|
|
124
136
|
access_token: accessToken,
|
|
125
137
|
refresh_token: refreshToken,
|
|
126
|
-
expires_at: Date.now() +
|
|
138
|
+
expires_at: Date.now() + expiresIn * 1000,
|
|
127
139
|
}
|
|
128
140
|
this.saveToken()
|
|
129
141
|
}
|
|
130
|
-
|
|
142
|
+
|
|
131
143
|
/**
|
|
132
144
|
* Update just the access token and its expiration
|
|
133
145
|
* @param accessToken The new access token
|
|
@@ -135,12 +147,12 @@ export class TokenManager {
|
|
|
135
147
|
*/
|
|
136
148
|
public updateAccessToken(accessToken: string, expiresIn: number): void {
|
|
137
149
|
if (!this.tokenData) return
|
|
138
|
-
|
|
150
|
+
|
|
139
151
|
this.tokenData.access_token = accessToken
|
|
140
|
-
this.tokenData.expires_at = Date.now() +
|
|
152
|
+
this.tokenData.expires_at = Date.now() + expiresIn * 1000
|
|
141
153
|
this.saveToken()
|
|
142
154
|
}
|
|
143
|
-
|
|
155
|
+
|
|
144
156
|
/**
|
|
145
157
|
* Clear all token data
|
|
146
158
|
*/
|
|
@@ -10,8 +10,8 @@ vi.mock('../../src/utils/default-api-key')
|
|
|
10
10
|
vi.mock('readline', () => ({
|
|
11
11
|
createInterface: vi.fn(() => ({
|
|
12
12
|
question: vi.fn(),
|
|
13
|
-
close: vi.fn()
|
|
14
|
-
}))
|
|
13
|
+
close: vi.fn(),
|
|
14
|
+
})),
|
|
15
15
|
}))
|
|
16
16
|
|
|
17
17
|
describe('Chat Commands', () => {
|
|
@@ -21,21 +21,23 @@ describe('Chat Commands', () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
23
|
program = new Command()
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// Mock ChatService
|
|
26
26
|
mockChatService = {
|
|
27
27
|
createCompletion: vi.fn(),
|
|
28
|
-
listModels: vi.fn()
|
|
28
|
+
listModels: vi.fn(),
|
|
29
29
|
}
|
|
30
30
|
vi.mocked(ChatService.getInstance).mockReturnValue(mockChatService)
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
// Mock DefaultApiKeyManager
|
|
33
33
|
mockDefaultApiKeyManager = {
|
|
34
34
|
getDefaultApiKeyData: vi.fn(),
|
|
35
|
-
promptForDefaultApiKey: vi.fn()
|
|
35
|
+
promptForDefaultApiKey: vi.fn(),
|
|
36
36
|
}
|
|
37
|
-
vi.mocked(DefaultApiKeyManager.getInstance).mockReturnValue(
|
|
38
|
-
|
|
37
|
+
vi.mocked(DefaultApiKeyManager.getInstance).mockReturnValue(
|
|
38
|
+
mockDefaultApiKeyManager,
|
|
39
|
+
)
|
|
40
|
+
|
|
39
41
|
registerChatCommands(program)
|
|
40
42
|
})
|
|
41
43
|
|
|
@@ -45,24 +47,30 @@ describe('Chat Commands', () => {
|
|
|
45
47
|
|
|
46
48
|
describe('chat run command', () => {
|
|
47
49
|
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(
|
|
50
|
-
|
|
50
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
|
|
51
|
+
const runCommand = chatCommand?.commands.find(
|
|
52
|
+
(cmd) => cmd.name() === 'run',
|
|
53
|
+
)
|
|
54
|
+
|
|
51
55
|
expect(runCommand).toBeDefined()
|
|
52
|
-
|
|
56
|
+
|
|
53
57
|
// Check the help text which contains the default model
|
|
54
58
|
const helpText = runCommand?.helpInformation()
|
|
55
59
|
expect(helpText).toContain('openai/gpt-oss')
|
|
56
60
|
})
|
|
57
61
|
|
|
58
62
|
it('should have streaming enabled by default', () => {
|
|
59
|
-
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat')
|
|
60
|
-
const runCommand = chatCommand?.commands.find(
|
|
61
|
-
|
|
63
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
|
|
64
|
+
const runCommand = chatCommand?.commands.find(
|
|
65
|
+
(cmd) => cmd.name() === 'run',
|
|
66
|
+
)
|
|
67
|
+
|
|
62
68
|
expect(runCommand).toBeDefined()
|
|
63
|
-
|
|
69
|
+
|
|
64
70
|
// Check that the option is --no-stream (meaning streaming is default)
|
|
65
|
-
const streamOption = runCommand?.options.find(
|
|
71
|
+
const streamOption = runCommand?.options.find(
|
|
72
|
+
(opt) => opt.long === '--no-stream',
|
|
73
|
+
)
|
|
66
74
|
expect(streamOption).toBeDefined()
|
|
67
75
|
expect(streamOption?.description).toContain('Disable streaming')
|
|
68
76
|
})
|
|
@@ -70,19 +78,21 @@ describe('Chat Commands', () => {
|
|
|
70
78
|
it('should create completion with correct default options', async () => {
|
|
71
79
|
// Mock API key
|
|
72
80
|
process.env.BERGET_API_KEY = 'test-key'
|
|
73
|
-
|
|
81
|
+
|
|
74
82
|
// Mock successful completion
|
|
75
83
|
mockChatService.createCompletion.mockResolvedValue({
|
|
76
|
-
choices: [
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
choices: [
|
|
85
|
+
{
|
|
86
|
+
message: { content: 'Test response' },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
79
89
|
})
|
|
80
90
|
|
|
81
91
|
// This would normally test the actual command execution
|
|
82
92
|
// but since it involves readline interaction, we just verify
|
|
83
93
|
// that the service would be called with correct defaults
|
|
84
94
|
expect(mockChatService.createCompletion).not.toHaveBeenCalled()
|
|
85
|
-
|
|
95
|
+
|
|
86
96
|
// Clean up
|
|
87
97
|
delete process.env.BERGET_API_KEY
|
|
88
98
|
})
|
|
@@ -99,17 +109,19 @@ describe('Chat Commands', () => {
|
|
|
99
109
|
capabilities: {
|
|
100
110
|
vision: false,
|
|
101
111
|
function_calling: true,
|
|
102
|
-
json_mode: true
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
]
|
|
112
|
+
json_mode: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
106
116
|
}
|
|
107
|
-
|
|
117
|
+
|
|
108
118
|
mockChatService.listModels.mockResolvedValue(mockModels)
|
|
109
|
-
|
|
110
|
-
const chatCommand = program.commands.find(cmd => cmd.name() === 'chat')
|
|
111
|
-
const listCommand = chatCommand?.commands.find(
|
|
112
|
-
|
|
119
|
+
|
|
120
|
+
const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
|
|
121
|
+
const listCommand = chatCommand?.commands.find(
|
|
122
|
+
(cmd) => cmd.name() === 'list',
|
|
123
|
+
)
|
|
124
|
+
|
|
113
125
|
expect(listCommand).toBeDefined()
|
|
114
126
|
expect(listCommand?.description()).toBe('List available chat models')
|
|
115
127
|
})
|