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.
Files changed (66) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +184 -0
  3. package/TODO.md +2 -0
  4. package/blog-post.md +176 -0
  5. package/dist/index.js +11 -8
  6. package/dist/package.json +7 -2
  7. package/dist/src/commands/api-keys.js +4 -2
  8. package/dist/src/commands/chat.js +21 -11
  9. package/dist/src/commands/code.js +1424 -0
  10. package/dist/src/commands/index.js +2 -0
  11. package/dist/src/constants/command-structure.js +12 -0
  12. package/dist/src/schemas/opencode-schema.json +1121 -0
  13. package/dist/src/services/cluster-service.js +1 -1
  14. package/dist/src/utils/default-api-key.js +2 -2
  15. package/dist/src/utils/env-manager.js +86 -0
  16. package/dist/src/utils/error-handler.js +10 -3
  17. package/dist/src/utils/markdown-renderer.js +4 -4
  18. package/dist/src/utils/opencode-validator.js +122 -0
  19. package/dist/src/utils/token-manager.js +2 -2
  20. package/dist/tests/commands/chat.test.js +20 -18
  21. package/dist/tests/commands/code.test.js +414 -0
  22. package/dist/tests/utils/env-manager.test.js +148 -0
  23. package/dist/tests/utils/opencode-validator.test.js +103 -0
  24. package/index.ts +67 -32
  25. package/opencode.json +182 -0
  26. package/package.json +7 -2
  27. package/src/client.ts +20 -20
  28. package/src/commands/api-keys.ts +93 -60
  29. package/src/commands/auth.ts +4 -2
  30. package/src/commands/billing.ts +6 -3
  31. package/src/commands/chat.ts +149 -107
  32. package/src/commands/clusters.ts +2 -2
  33. package/src/commands/code.ts +1696 -0
  34. package/src/commands/index.ts +2 -0
  35. package/src/commands/models.ts +3 -3
  36. package/src/commands/users.ts +2 -2
  37. package/src/constants/command-structure.ts +112 -58
  38. package/src/schemas/opencode-schema.json +991 -0
  39. package/src/services/api-key-service.ts +1 -1
  40. package/src/services/auth-service.ts +27 -25
  41. package/src/services/chat-service.ts +26 -23
  42. package/src/services/cluster-service.ts +5 -5
  43. package/src/services/collaborator-service.ts +3 -3
  44. package/src/services/flux-service.ts +2 -2
  45. package/src/services/helm-service.ts +2 -2
  46. package/src/services/kubectl-service.ts +3 -6
  47. package/src/types/api.d.ts +1032 -1010
  48. package/src/types/json.d.ts +3 -3
  49. package/src/utils/default-api-key.ts +54 -42
  50. package/src/utils/env-manager.ts +98 -0
  51. package/src/utils/error-handler.ts +24 -15
  52. package/src/utils/logger.ts +12 -12
  53. package/src/utils/markdown-renderer.ts +18 -18
  54. package/src/utils/opencode-validator.ts +134 -0
  55. package/src/utils/token-manager.ts +35 -23
  56. package/tests/commands/chat.test.ts +43 -31
  57. package/tests/commands/code.test.ts +505 -0
  58. package/tests/utils/env-manager.test.ts +199 -0
  59. package/tests/utils/opencode-validator.test.ts +118 -0
  60. package/tsconfig.json +8 -8
  61. package/-27b-it +0 -0
  62. package/examples/README.md +0 -95
  63. package/examples/ai-review.sh +0 -30
  64. package/examples/install-global-security-hook.sh +0 -170
  65. package/examples/security-check.sh +0 -102
  66. 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(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
  */
@@ -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(mockDefaultApiKeyManager)
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(cmd => cmd.name() === 'run')
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(cmd => cmd.name() === 'run')
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(opt => opt.long === '--no-stream')
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
- message: { content: 'Test response' }
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(cmd => cmd.name() === 'list')
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
  })