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
@@ -1,4 +1,4 @@
1
- declare module "*.json" {
2
- const value: any;
3
- export = value;
1
+ declare module '*.json' {
2
+ const value: any
3
+ export = value
4
4
  }
@@ -20,7 +20,7 @@ export class DefaultApiKeyManager {
20
20
  private static instance: DefaultApiKeyManager
21
21
  private configFilePath: string
22
22
  private defaultApiKey: DefaultApiKeyData | null = null
23
-
23
+
24
24
  private constructor() {
25
25
  // Set up config file path in user's home directory
26
26
  const bergetDir = path.join(os.homedir(), '.berget')
@@ -30,14 +30,14 @@ export class DefaultApiKeyManager {
30
30
  this.configFilePath = path.join(bergetDir, 'default-api-key.json')
31
31
  this.loadConfig()
32
32
  }
33
-
33
+
34
34
  public static getInstance(): DefaultApiKeyManager {
35
35
  if (!DefaultApiKeyManager.instance) {
36
36
  DefaultApiKeyManager.instance = new DefaultApiKeyManager()
37
37
  }
38
38
  return DefaultApiKeyManager.instance
39
39
  }
40
-
40
+
41
41
  /**
42
42
  * Load default API key from file
43
43
  */
@@ -52,14 +52,17 @@ export class DefaultApiKeyManager {
52
52
  this.defaultApiKey = null
53
53
  }
54
54
  }
55
-
55
+
56
56
  /**
57
57
  * Save default API key to file
58
58
  */
59
59
  private saveConfig(): void {
60
60
  try {
61
61
  if (this.defaultApiKey) {
62
- fs.writeFileSync(this.configFilePath, JSON.stringify(this.defaultApiKey, null, 2))
62
+ fs.writeFileSync(
63
+ this.configFilePath,
64
+ JSON.stringify(this.defaultApiKey, null, 2),
65
+ )
63
66
  // Set file permissions to be readable only by the owner
64
67
  fs.chmodSync(this.configFilePath, 0o600)
65
68
  } else {
@@ -72,29 +75,34 @@ export class DefaultApiKeyManager {
72
75
  logger.debug('Failed to save default API key configuration')
73
76
  }
74
77
  }
75
-
78
+
76
79
  /**
77
80
  * Set the default API key
78
81
  */
79
- public setDefaultApiKey(id: string, name: string, prefix: string, key: string): void {
82
+ public setDefaultApiKey(
83
+ id: string,
84
+ name: string,
85
+ prefix: string,
86
+ key: string,
87
+ ): void {
80
88
  this.defaultApiKey = { id, name, prefix, key }
81
89
  this.saveConfig()
82
90
  }
83
-
91
+
84
92
  /**
85
93
  * Get the default API key string
86
94
  */
87
95
  public getDefaultApiKey(): string | null {
88
96
  return this.defaultApiKey?.key || null
89
97
  }
90
-
98
+
91
99
  /**
92
100
  * Get the default API key data object
93
101
  */
94
102
  public getDefaultApiKeyData(): DefaultApiKeyData | null {
95
103
  return this.defaultApiKey
96
104
  }
97
-
105
+
98
106
  /**
99
107
  * Clear the default API key
100
108
  */
@@ -110,7 +118,7 @@ export class DefaultApiKeyManager {
110
118
  public async promptForDefaultApiKey(): Promise<string | null> {
111
119
  try {
112
120
  logger.debug('promptForDefaultApiKey called')
113
-
121
+
114
122
  // If we already have a default API key, return it
115
123
  if (this.defaultApiKey) {
116
124
  logger.debug('Using existing default API key')
@@ -118,18 +126,18 @@ export class DefaultApiKeyManager {
118
126
  }
119
127
 
120
128
  logger.debug('No default API key found, getting ApiKeyService')
121
-
129
+
122
130
  const apiKeyService = ApiKeyService.getInstance()
123
-
131
+
124
132
  // Get all API keys
125
- let apiKeys;
133
+ let apiKeys
126
134
  try {
127
135
  logger.debug('Calling apiKeyService.list()')
128
-
136
+
129
137
  apiKeys = await apiKeyService.list()
130
-
138
+
131
139
  logger.debug(`Got ${apiKeys ? apiKeys.length : 0} API keys`)
132
-
140
+
133
141
  if (!apiKeys || apiKeys.length === 0) {
134
142
  logger.warn('No API keys found. Create one with:')
135
143
  logger.info(' berget api-keys create --name "My Key"')
@@ -137,37 +145,41 @@ export class DefaultApiKeyManager {
137
145
  }
138
146
  } catch (error) {
139
147
  // Check if this is an authentication error
140
- const errorMessage = error instanceof Error ? error.message : String(error);
141
- const isAuthError = errorMessage.includes('Unauthorized') ||
142
- errorMessage.includes('Authentication failed') ||
143
- errorMessage.includes('AUTH_FAILED');
144
-
148
+ const errorMessage =
149
+ error instanceof Error ? error.message : String(error)
150
+ const isAuthError =
151
+ errorMessage.includes('Unauthorized') ||
152
+ errorMessage.includes('Authentication failed') ||
153
+ errorMessage.includes('AUTH_FAILED')
154
+
145
155
  if (isAuthError) {
146
- logger.warn('Authentication required. Please run `berget auth login` first.');
156
+ logger.warn(
157
+ 'Authentication required. Please run `berget auth login` first.',
158
+ )
147
159
  } else {
148
- logger.error('Error fetching API keys:');
160
+ logger.error('Error fetching API keys:')
149
161
  if (error instanceof Error) {
150
- logger.error(error.message);
151
- logger.debug(`API key list error: ${error.message}`);
152
- logger.debug(`Stack: ${error.stack}`);
162
+ logger.error(error.message)
163
+ logger.debug(`API key list error: ${error.message}`)
164
+ logger.debug(`Stack: ${error.stack}`)
153
165
  }
154
166
  }
155
- return null;
167
+ return null
156
168
  }
157
-
169
+
158
170
  logger.info('Select an API key to use as default:')
159
-
171
+
160
172
  // Display available API keys
161
173
  apiKeys.forEach((key, index) => {
162
174
  logger.log(` ${index + 1}. ${key.name} (${key.prefix}...)`)
163
175
  })
164
-
176
+
165
177
  // Create readline interface for user input
166
178
  const rl = readline.createInterface({
167
179
  input: process.stdin,
168
- output: process.stdout
180
+ output: process.stdout,
169
181
  })
170
-
182
+
171
183
  // Prompt for selection
172
184
  const selection = await new Promise<number>((resolve) => {
173
185
  rl.question('Enter number (or press Enter to cancel): ', (answer) => {
@@ -180,28 +192,28 @@ export class DefaultApiKeyManager {
180
192
  }
181
193
  })
182
194
  })
183
-
195
+
184
196
  if (selection === -1) {
185
197
  logger.warn('No API key selected')
186
198
  return null
187
199
  }
188
-
200
+
189
201
  const selectedKey = apiKeys[selection]
190
-
202
+
191
203
  // Create a new API key with the selected name
192
204
  const newKey = await apiKeyService.create({
193
205
  name: `CLI Default (copy of ${selectedKey.name})`,
194
- description: 'Created automatically by the Berget CLI for default use'
206
+ description: 'Created automatically by the Berget CLI for default use',
195
207
  })
196
-
208
+
197
209
  // Save the new key as default
198
210
  this.setDefaultApiKey(
199
- newKey.id.toString(),
200
- newKey.name,
211
+ newKey.id.toString(),
212
+ newKey.name,
201
213
  newKey.key.substring(0, 8), // Use first 8 chars as prefix
202
- newKey.key
214
+ newKey.key,
203
215
  )
204
-
216
+
205
217
  logger.success(`✓ Default API key set to: ${newKey.name}`)
206
218
  return newKey.key
207
219
  } catch (error) {
@@ -0,0 +1,98 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { writeFile } from 'fs/promises'
4
+ import chalk from 'chalk'
5
+ import dotenv from 'dotenv'
6
+
7
+ export interface EnvUpdateOptions {
8
+ envPath?: string
9
+ key: string
10
+ value: string
11
+ comment?: string
12
+ force?: boolean
13
+ }
14
+
15
+ /**
16
+ * Safely updates .env file without overwriting existing keys
17
+ * Uses dotenv for proper parsing and formatting
18
+ */
19
+ export async function updateEnvFile(
20
+ options: EnvUpdateOptions,
21
+ ): Promise<boolean> {
22
+ const {
23
+ envPath = path.join(process.cwd(), '.env'),
24
+ key,
25
+ value,
26
+ comment,
27
+ force = false,
28
+ } = options
29
+
30
+ try {
31
+ let existingContent = ''
32
+ let parsed: Record<string, string> = {}
33
+
34
+ // Read existing .env file if it exists
35
+ if (fs.existsSync(envPath)) {
36
+ existingContent = fs.readFileSync(envPath, 'utf8')
37
+ parsed = dotenv.parse(existingContent)
38
+ }
39
+
40
+ // Check if key already exists and we're not forcing
41
+ if (parsed[key] && !force) {
42
+ console.log(
43
+ chalk.yellow(`⚠ ${key} already exists in .env - leaving unchanged`),
44
+ )
45
+ return false
46
+ }
47
+
48
+ // Update the parsed object
49
+ parsed[key] = value
50
+
51
+ // Generate new .env content
52
+ let newContent = ''
53
+
54
+ // Add comment at the top if this is a new file
55
+ if (!existingContent && comment) {
56
+ newContent += `# ${comment}\n`
57
+ }
58
+
59
+ // Convert parsed object back to .env format
60
+ for (const [envKey, envValue] of Object.entries(parsed)) {
61
+ newContent += `${envKey}=${envValue}\n`
62
+ }
63
+
64
+ // Write the updated content
65
+ await writeFile(envPath, newContent.trim() + '\n')
66
+
67
+ if (existingContent) {
68
+ console.log(chalk.green(`✓ Updated .env with ${key}`))
69
+ } else {
70
+ console.log(chalk.green(`✓ Created .env with ${key}`))
71
+ }
72
+
73
+ return true
74
+ } catch (error) {
75
+ console.error(chalk.red(`Failed to update .env file:`))
76
+ throw error
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Checks if a .env file exists and contains a specific key
82
+ */
83
+ export function hasEnvKey(
84
+ envPath: string = path.join(process.cwd(), '.env'),
85
+ key: string,
86
+ ): boolean {
87
+ if (!fs.existsSync(envPath)) {
88
+ return false
89
+ }
90
+
91
+ try {
92
+ const content = fs.readFileSync(envPath, 'utf8')
93
+ const parsed = dotenv.parse(content)
94
+ return key in parsed
95
+ } catch {
96
+ return false
97
+ }
98
+ }
@@ -1,40 +1,49 @@
1
- import chalk from 'chalk';
1
+ import chalk from 'chalk'
2
2
 
3
3
  /**
4
4
  * Formats and prints error messages in a consistent way
5
5
  */
6
6
  export function handleError(message: string, error: any): void {
7
- console.error(chalk.red(`Error: ${message}`));
8
-
7
+ console.error(chalk.red(`Error: ${message}`))
8
+
9
9
  // If the error is a string (like JSON.stringify(error))
10
10
  if (typeof error === 'string') {
11
11
  try {
12
12
  // Try to parse it as JSON
13
- const parsedError = JSON.parse(error);
13
+ const parsedError = JSON.parse(error)
14
14
  if (parsedError.error) {
15
- console.error(chalk.dim(`Details: ${parsedError.error}`));
15
+ console.error(chalk.dim(`Details: ${parsedError.error}`))
16
16
  if (parsedError.code) {
17
- console.error(chalk.dim(`Code: ${parsedError.code}`));
17
+ console.error(chalk.dim(`Code: ${parsedError.code}`))
18
18
  }
19
19
  } else {
20
- console.error(chalk.dim(`Details: ${error}`));
20
+ console.error(chalk.dim(`Details: ${error}`))
21
21
  }
22
22
  } catch {
23
23
  // If it's not valid JSON, just print the string
24
- console.error(chalk.dim(`Details: ${error}`));
24
+ console.error(chalk.dim(`Details: ${error}`))
25
25
  }
26
26
  } else if (error && error.message) {
27
27
  // If it's an Error object
28
- console.error(chalk.dim(`Details: ${error.message}`));
28
+ console.error(chalk.dim(`Details: ${error.message}`))
29
29
  }
30
-
30
+
31
31
  // Check for authentication errors
32
32
  if (
33
- (typeof error === 'string' && (error.includes('Unauthorized') || error.includes('Authentication failed'))) ||
34
- (error && error.message && (error.message.includes('Unauthorized') || error.message.includes('Authentication failed'))) ||
35
- (error && error.code && (error.code === 401 || error.code === 'AUTH_FAILED'))
33
+ (typeof error === 'string' &&
34
+ (error.includes('Unauthorized') ||
35
+ error.includes('Authentication failed'))) ||
36
+ (error &&
37
+ error.message &&
38
+ (error.message.includes('Unauthorized') ||
39
+ error.message.includes('Authentication failed'))) ||
40
+ (error &&
41
+ error.code &&
42
+ (error.code === 401 || error.code === 'AUTH_FAILED'))
36
43
  ) {
37
- console.error(chalk.yellow('\nYou need to be logged in to use this command.'));
38
- console.error(chalk.yellow('Run `berget auth login` to authenticate.'));
44
+ console.error(
45
+ chalk.yellow('\nYou need to be logged in to use this command.'),
46
+ )
47
+ console.error(chalk.yellow('Run `berget auth login` to authenticate.'))
39
48
  }
40
49
  }
@@ -8,7 +8,7 @@ export enum LogLevel {
8
8
  ERROR = 1,
9
9
  WARN = 2,
10
10
  INFO = 3,
11
- DEBUG = 4
11
+ DEBUG = 4,
12
12
  }
13
13
 
14
14
  /**
@@ -17,7 +17,7 @@ export enum LogLevel {
17
17
  export class Logger {
18
18
  private static instance: Logger
19
19
  private logLevel: LogLevel = LogLevel.INFO // Default log level
20
-
20
+
21
21
  private constructor() {
22
22
  // Set log level from environment variable or command line argument
23
23
  if (process.env.LOG_LEVEL) {
@@ -28,14 +28,14 @@ export class Logger {
28
28
  this.logLevel = LogLevel.ERROR
29
29
  }
30
30
  }
31
-
31
+
32
32
  public static getInstance(): Logger {
33
33
  if (!Logger.instance) {
34
34
  Logger.instance = new Logger()
35
35
  }
36
36
  return Logger.instance
37
37
  }
38
-
38
+
39
39
  /**
40
40
  * Set the log level from a string
41
41
  */
@@ -61,21 +61,21 @@ export class Logger {
61
61
  console.warn(`Invalid log level: ${level}. Using default (INFO).`)
62
62
  }
63
63
  }
64
-
64
+
65
65
  /**
66
66
  * Set the log level
67
67
  */
68
68
  public setLogLevel(level: LogLevel): void {
69
69
  this.logLevel = level
70
70
  }
71
-
71
+
72
72
  /**
73
73
  * Get the current log level
74
74
  */
75
75
  public getLogLevel(): LogLevel {
76
76
  return this.logLevel
77
77
  }
78
-
78
+
79
79
  /**
80
80
  * Log a debug message (only shown at DEBUG level)
81
81
  */
@@ -88,7 +88,7 @@ export class Logger {
88
88
  }
89
89
  }
90
90
  }
91
-
91
+
92
92
  /**
93
93
  * Log an info message (shown at INFO level and above)
94
94
  */
@@ -101,7 +101,7 @@ export class Logger {
101
101
  }
102
102
  }
103
103
  }
104
-
104
+
105
105
  /**
106
106
  * Log a warning message (shown at WARN level and above)
107
107
  */
@@ -114,7 +114,7 @@ export class Logger {
114
114
  }
115
115
  }
116
116
  }
117
-
117
+
118
118
  /**
119
119
  * Log an error message (shown at ERROR level and above)
120
120
  */
@@ -127,7 +127,7 @@ export class Logger {
127
127
  }
128
128
  }
129
129
  }
130
-
130
+
131
131
  /**
132
132
  * Log a success message (shown at INFO level and above)
133
133
  */
@@ -140,7 +140,7 @@ export class Logger {
140
140
  }
141
141
  }
142
142
  }
143
-
143
+
144
144
  /**
145
145
  * Log a plain message without color (shown at INFO level and above)
146
146
  */
@@ -18,8 +18,8 @@ marked.setOptions({
18
18
  // Adjust the width to fit the terminal
19
19
  width: process.stdout.columns || 80,
20
20
  // Customize code block rendering
21
- codespan: chalk.cyan
22
- })
21
+ codespan: chalk.cyan,
22
+ }),
23
23
  })
24
24
 
25
25
  /**
@@ -29,7 +29,7 @@ marked.setOptions({
29
29
  */
30
30
  export function renderMarkdown(markdown: string): string {
31
31
  if (!markdown) return ''
32
-
32
+
33
33
  try {
34
34
  // Convert markdown to terminal-friendly text
35
35
  return marked(markdown)
@@ -47,22 +47,22 @@ export function renderMarkdown(markdown: string): string {
47
47
  */
48
48
  export function containsMarkdown(text: string): boolean {
49
49
  if (!text) return false
50
-
50
+
51
51
  // Check for common markdown patterns
52
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
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
65
  ]
66
-
67
- return markdownPatterns.some(pattern => pattern.test(text))
66
+
67
+ return markdownPatterns.some((pattern) => pattern.test(text))
68
68
  }
@@ -0,0 +1,134 @@
1
+ import Ajv from 'ajv'
2
+ import addFormats from 'ajv-formats'
3
+ import { readFileSync } from 'fs'
4
+ import { join } from 'path'
5
+ import { dirname } from 'path'
6
+
7
+ // Load the official OpenCode JSON Schema
8
+ const __dirname = dirname(__filename)
9
+ const schemaPath = join(__dirname, '..', 'schemas', 'opencode-schema.json')
10
+
11
+ let ajv: Ajv
12
+ let openCodeSchema: any
13
+ let validateFunction: any
14
+
15
+ try {
16
+ const schemaContent = readFileSync(schemaPath, 'utf-8')
17
+ openCodeSchema = JSON.parse(schemaContent)
18
+
19
+ // Initialize AJV with formats and options
20
+ ajv = new Ajv({
21
+ allErrors: true,
22
+ verbose: true,
23
+ strict: false,
24
+ allowUnionTypes: true,
25
+ removeAdditional: false,
26
+ })
27
+
28
+ // Add JSON Schema formats
29
+ addFormats(ajv)
30
+
31
+ // Compile the schema
32
+ validateFunction = ajv.compile(openCodeSchema)
33
+ } catch (error) {
34
+ console.error('Failed to load OpenCode schema:', error)
35
+ throw new Error('Could not initialize OpenCode validator')
36
+ }
37
+
38
+ export type OpenCodeConfig = any
39
+
40
+ /**
41
+ * Validate OpenCode configuration against the official JSON Schema
42
+ */
43
+ export function validateOpenCodeConfig(config: any): {
44
+ valid: boolean
45
+ errors?: string[]
46
+ } {
47
+ try {
48
+ if (!validateFunction) {
49
+ return { valid: false, errors: ['Schema validator not initialized'] }
50
+ }
51
+
52
+ const isValid = validateFunction(config)
53
+
54
+ if (isValid) {
55
+ return { valid: true }
56
+ } else {
57
+ const errors = validateFunction.errors?.map((err: any) => {
58
+ const path = err.instancePath || err.schemaPath || 'root'
59
+ const message = err.message || 'Unknown error'
60
+ return `${path}: ${message}`
61
+ }) || ['Unknown validation error']
62
+
63
+ return { valid: false, errors }
64
+ }
65
+ } catch (error) {
66
+ console.error('Validation error:', error)
67
+ return { valid: false, errors: ['Validation process failed'] }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Fix common OpenCode configuration issues
73
+ */
74
+ export function fixOpenCodeConfig(config: any): OpenCodeConfig {
75
+ const fixed = { ...config }
76
+
77
+ // Fix tools.compact - should be boolean, not object
78
+ if (fixed.tools && typeof fixed.tools.compact === 'object') {
79
+ console.warn('⚠️ Converting tools.compact from object to boolean')
80
+ // If it has properties, assume it should be enabled
81
+ fixed.tools.compact = true
82
+ }
83
+
84
+ // Remove invalid properties
85
+ const invalidProps = ['maxTokens', 'contextWindow']
86
+ invalidProps.forEach((prop) => {
87
+ if (fixed[prop] !== undefined) {
88
+ console.warn(`⚠️ Removing invalid property: ${prop}`)
89
+ delete fixed[prop]
90
+ }
91
+ })
92
+
93
+ // Fix provider models with invalid properties
94
+ if (fixed.provider) {
95
+ Object.values(fixed.provider).forEach((provider: any) => {
96
+ if (provider?.models) {
97
+ Object.values(provider.models).forEach((model: any) => {
98
+ if (model && typeof model === 'object') {
99
+ // Move maxTokens/contextWindow to proper structure if needed
100
+ if (model.maxTokens || model.contextWindow) {
101
+ if (!model.limit) model.limit = {}
102
+
103
+ // Use the larger of maxTokens/contextWindow for context
104
+ const contextValues = [
105
+ model.maxTokens,
106
+ model.contextWindow,
107
+ ].filter(Boolean)
108
+ if (contextValues.length > 0) {
109
+ const newContext = Math.max(...contextValues)
110
+ if (!model.limit.context || newContext > model.limit.context) {
111
+ model.limit.context = newContext
112
+ }
113
+ }
114
+
115
+ // Set a reasonable default for output if not present
116
+ // (typically 1/4 to 1/8 of context window)
117
+ if (!model.limit.output && model.limit.context) {
118
+ model.limit.output = Math.floor(model.limit.context / 4)
119
+ }
120
+
121
+ delete model.maxTokens
122
+ delete model.contextWindow
123
+ console.warn(
124
+ '⚠️ Moved maxTokens/contextWindow to limit.context/output',
125
+ )
126
+ }
127
+ }
128
+ })
129
+ }
130
+ })
131
+ }
132
+
133
+ return fixed
134
+ }