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.
Files changed (67) hide show
  1. package/.env.example +5 -0
  2. package/.github/workflows/publish.yml +56 -0
  3. package/.github/workflows/test.yml +38 -0
  4. package/AGENTS.md +184 -0
  5. package/README.md +177 -38
  6. package/TODO.md +2 -0
  7. package/blog-post.md +176 -0
  8. package/dist/index.js +11 -8
  9. package/dist/package.json +14 -3
  10. package/dist/src/commands/api-keys.js +4 -2
  11. package/dist/src/commands/chat.js +182 -23
  12. package/dist/src/commands/code.js +1424 -0
  13. package/dist/src/commands/index.js +2 -0
  14. package/dist/src/constants/command-structure.js +12 -0
  15. package/dist/src/schemas/opencode-schema.json +1121 -0
  16. package/dist/src/services/chat-service.js +10 -10
  17. package/dist/src/services/cluster-service.js +1 -1
  18. package/dist/src/utils/default-api-key.js +2 -2
  19. package/dist/src/utils/env-manager.js +86 -0
  20. package/dist/src/utils/error-handler.js +10 -3
  21. package/dist/src/utils/markdown-renderer.js +4 -4
  22. package/dist/src/utils/opencode-validator.js +122 -0
  23. package/dist/src/utils/token-manager.js +2 -2
  24. package/dist/tests/commands/chat.test.js +109 -0
  25. package/dist/tests/commands/code.test.js +414 -0
  26. package/dist/tests/utils/env-manager.test.js +148 -0
  27. package/dist/tests/utils/opencode-validator.test.js +103 -0
  28. package/dist/vitest.config.js +9 -0
  29. package/index.ts +67 -32
  30. package/opencode.json +182 -0
  31. package/package.json +14 -3
  32. package/src/client.ts +20 -20
  33. package/src/commands/api-keys.ts +93 -60
  34. package/src/commands/auth.ts +4 -2
  35. package/src/commands/billing.ts +6 -3
  36. package/src/commands/chat.ts +291 -97
  37. package/src/commands/clusters.ts +2 -2
  38. package/src/commands/code.ts +1696 -0
  39. package/src/commands/index.ts +2 -0
  40. package/src/commands/models.ts +3 -3
  41. package/src/commands/users.ts +2 -2
  42. package/src/constants/command-structure.ts +112 -58
  43. package/src/schemas/opencode-schema.json +991 -0
  44. package/src/services/api-key-service.ts +1 -1
  45. package/src/services/auth-service.ts +27 -25
  46. package/src/services/chat-service.ts +37 -44
  47. package/src/services/cluster-service.ts +5 -5
  48. package/src/services/collaborator-service.ts +3 -3
  49. package/src/services/flux-service.ts +2 -2
  50. package/src/services/helm-service.ts +2 -2
  51. package/src/services/kubectl-service.ts +3 -6
  52. package/src/types/api.d.ts +1032 -1010
  53. package/src/types/json.d.ts +3 -3
  54. package/src/utils/default-api-key.ts +54 -42
  55. package/src/utils/env-manager.ts +98 -0
  56. package/src/utils/error-handler.ts +24 -15
  57. package/src/utils/logger.ts +12 -12
  58. package/src/utils/markdown-renderer.ts +18 -18
  59. package/src/utils/opencode-validator.ts +134 -0
  60. package/src/utils/token-manager.ts +35 -23
  61. package/tests/commands/chat.test.ts +129 -0
  62. package/tests/commands/code.test.ts +505 -0
  63. package/tests/utils/env-manager.test.ts +199 -0
  64. package/tests/utils/opencode-validator.test.ts +118 -0
  65. package/tsconfig.json +8 -8
  66. package/vitest.config.ts +8 -0
  67. package/-27b-it +0 -0
@@ -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(this.tokenFilePath, JSON.stringify(this.tokenData, null, 2))
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 = Date.now() + expirationBuffer >= this.tokenData.expires_at;
103
-
105
+ const isExpired =
106
+ Date.now() + expirationBuffer >= this.tokenData.expires_at
107
+
104
108
  if (isExpired) {
105
- logger.debug(`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(this.tokenData.expires_at).toISOString()}`);
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(`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}`);
112
- return true;
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(accessToken: string, refreshToken: string, expiresIn: number): void {
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() + (expiresIn * 1000)
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() + (expiresIn * 1000)
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
  */
@@ -0,0 +1,129 @@
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(
38
+ mockDefaultApiKeyManager,
39
+ )
40
+
41
+ registerChatCommands(program)
42
+ })
43
+
44
+ afterEach(() => {
45
+ vi.clearAllMocks()
46
+ })
47
+
48
+ describe('chat run command', () => {
49
+ it('should use openai/gpt-oss as default model', () => {
50
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
51
+ const runCommand = chatCommand?.commands.find(
52
+ (cmd) => cmd.name() === 'run',
53
+ )
54
+
55
+ expect(runCommand).toBeDefined()
56
+
57
+ // Check the help text which contains the default model
58
+ const helpText = runCommand?.helpInformation()
59
+ expect(helpText).toContain('openai/gpt-oss')
60
+ })
61
+
62
+ it('should have streaming enabled by default', () => {
63
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
64
+ const runCommand = chatCommand?.commands.find(
65
+ (cmd) => cmd.name() === 'run',
66
+ )
67
+
68
+ expect(runCommand).toBeDefined()
69
+
70
+ // Check that the option is --no-stream (meaning streaming is default)
71
+ const streamOption = runCommand?.options.find(
72
+ (opt) => opt.long === '--no-stream',
73
+ )
74
+ expect(streamOption).toBeDefined()
75
+ expect(streamOption?.description).toContain('Disable streaming')
76
+ })
77
+
78
+ it('should create completion with correct default options', async () => {
79
+ // Mock API key
80
+ process.env.BERGET_API_KEY = 'test-key'
81
+
82
+ // Mock successful completion
83
+ mockChatService.createCompletion.mockResolvedValue({
84
+ choices: [
85
+ {
86
+ message: { content: 'Test response' },
87
+ },
88
+ ],
89
+ })
90
+
91
+ // This would normally test the actual command execution
92
+ // but since it involves readline interaction, we just verify
93
+ // that the service would be called with correct defaults
94
+ expect(mockChatService.createCompletion).not.toHaveBeenCalled()
95
+
96
+ // Clean up
97
+ delete process.env.BERGET_API_KEY
98
+ })
99
+ })
100
+
101
+ describe('chat list command', () => {
102
+ it('should list available models', async () => {
103
+ const mockModels = {
104
+ data: [
105
+ {
106
+ id: 'gpt-oss',
107
+ owned_by: 'openai',
108
+ active: true,
109
+ capabilities: {
110
+ vision: false,
111
+ function_calling: true,
112
+ json_mode: true,
113
+ },
114
+ },
115
+ ],
116
+ }
117
+
118
+ mockChatService.listModels.mockResolvedValue(mockModels)
119
+
120
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat')
121
+ const listCommand = chatCommand?.commands.find(
122
+ (cmd) => cmd.name() === 'list',
123
+ )
124
+
125
+ expect(listCommand).toBeDefined()
126
+ expect(listCommand?.description()).toBe('List available chat models')
127
+ })
128
+ })
129
+ })