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
package/src/types/json.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare module
|
|
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(
|
|
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(
|
|
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 =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
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' &&
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
38
|
-
|
|
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
|
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -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,
|
|
54
|
-
/\*\*.*?\*\*/,
|
|
55
|
-
/\*.*?\*/,
|
|
56
|
-
/`.*?`/,
|
|
57
|
-
/```[\s\S]*?```/,
|
|
58
|
-
/\[.*?\]\(.*?\)/,
|
|
59
|
-
/^\s*[-*+]\s+/m,
|
|
60
|
-
/^\s*\d+\.\s+/m,
|
|
61
|
-
/^\s*>\s+/m,
|
|
62
|
-
/\|.*\|.*\|/,
|
|
63
|
-
/^---+$/m,
|
|
64
|
-
/^===+$/m
|
|
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
|
+
}
|