berget 1.4.0 → 2.0.1

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
@@ -74,7 +74,7 @@ class ClusterService {
74
74
  // This is a placeholder since the API doesn't have a specific endpoint
75
75
  // In a real implementation, this would call a specific endpoint
76
76
  const clusters = yield this.list();
77
- return clusters.find(cluster => cluster.id === clusterId) || null;
77
+ return clusters.find((cluster) => cluster.id === clusterId) || null;
78
78
  }
79
79
  catch (error) {
80
80
  console.error('Failed to describe cluster:', error);
@@ -179,7 +179,7 @@ class DefaultApiKeyManager {
179
179
  // Create readline interface for user input
180
180
  const rl = readline_1.default.createInterface({
181
181
  input: process.stdin,
182
- output: process.stdout
182
+ output: process.stdout,
183
183
  });
184
184
  // Prompt for selection
185
185
  const selection = yield new Promise((resolve) => {
@@ -202,7 +202,7 @@ class DefaultApiKeyManager {
202
202
  // Create a new API key with the selected name
203
203
  const newKey = yield apiKeyService.create({
204
204
  name: `CLI Default (copy of ${selectedKey.name})`,
205
- description: 'Created automatically by the Berget CLI for default use'
205
+ description: 'Created automatically by the Berget CLI for default use',
206
206
  });
207
207
  // Save the new key as default
208
208
  this.setDefaultApiKey(newKey.id.toString(), newKey.name, newKey.key.substring(0, 8), // Use first 8 chars as prefix
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.hasEnvKey = exports.updateEnvFile = void 0;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const promises_1 = require("fs/promises");
19
+ const chalk_1 = __importDefault(require("chalk"));
20
+ const dotenv_1 = __importDefault(require("dotenv"));
21
+ /**
22
+ * Safely updates .env file without overwriting existing keys
23
+ * Uses dotenv for proper parsing and formatting
24
+ */
25
+ function updateEnvFile(options) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ const { envPath = path_1.default.join(process.cwd(), '.env'), key, value, comment, force = false, } = options;
28
+ try {
29
+ let existingContent = '';
30
+ let parsed = {};
31
+ // Read existing .env file if it exists
32
+ if (fs_1.default.existsSync(envPath)) {
33
+ existingContent = fs_1.default.readFileSync(envPath, 'utf8');
34
+ parsed = dotenv_1.default.parse(existingContent);
35
+ }
36
+ // Check if key already exists and we're not forcing
37
+ if (parsed[key] && !force) {
38
+ console.log(chalk_1.default.yellow(`⚠ ${key} already exists in .env - leaving unchanged`));
39
+ return false;
40
+ }
41
+ // Update the parsed object
42
+ parsed[key] = value;
43
+ // Generate new .env content
44
+ let newContent = '';
45
+ // Add comment at the top if this is a new file
46
+ if (!existingContent && comment) {
47
+ newContent += `# ${comment}\n`;
48
+ }
49
+ // Convert parsed object back to .env format
50
+ for (const [envKey, envValue] of Object.entries(parsed)) {
51
+ newContent += `${envKey}=${envValue}\n`;
52
+ }
53
+ // Write the updated content
54
+ yield (0, promises_1.writeFile)(envPath, newContent.trim() + '\n');
55
+ if (existingContent) {
56
+ console.log(chalk_1.default.green(`✓ Updated .env with ${key}`));
57
+ }
58
+ else {
59
+ console.log(chalk_1.default.green(`✓ Created .env with ${key}`));
60
+ }
61
+ return true;
62
+ }
63
+ catch (error) {
64
+ console.error(chalk_1.default.red(`Failed to update .env file:`));
65
+ throw error;
66
+ }
67
+ });
68
+ }
69
+ exports.updateEnvFile = updateEnvFile;
70
+ /**
71
+ * Checks if a .env file exists and contains a specific key
72
+ */
73
+ function hasEnvKey(envPath = path_1.default.join(process.cwd(), '.env'), key) {
74
+ if (!fs_1.default.existsSync(envPath)) {
75
+ return false;
76
+ }
77
+ try {
78
+ const content = fs_1.default.readFileSync(envPath, 'utf8');
79
+ const parsed = dotenv_1.default.parse(content);
80
+ return key in parsed;
81
+ }
82
+ catch (_a) {
83
+ return false;
84
+ }
85
+ }
86
+ exports.hasEnvKey = hasEnvKey;
@@ -35,9 +35,16 @@ function handleError(message, error) {
35
35
  console.error(chalk_1.default.dim(`Details: ${error.message}`));
36
36
  }
37
37
  // Check for authentication errors
38
- if ((typeof error === 'string' && (error.includes('Unauthorized') || error.includes('Authentication failed'))) ||
39
- (error && error.message && (error.message.includes('Unauthorized') || error.message.includes('Authentication failed'))) ||
40
- (error && error.code && (error.code === 401 || error.code === 'AUTH_FAILED'))) {
38
+ if ((typeof error === 'string' &&
39
+ (error.includes('Unauthorized') ||
40
+ error.includes('Authentication failed'))) ||
41
+ (error &&
42
+ error.message &&
43
+ (error.message.includes('Unauthorized') ||
44
+ error.message.includes('Authentication failed'))) ||
45
+ (error &&
46
+ error.code &&
47
+ (error.code === 401 || error.code === 'AUTH_FAILED'))) {
41
48
  console.error(chalk_1.default.yellow('\nYou need to be logged in to use this command.'));
42
49
  console.error(chalk_1.default.yellow('Run `berget auth login` to authenticate.'));
43
50
  }
@@ -23,8 +23,8 @@ marked_1.marked.setOptions({
23
23
  // Adjust the width to fit the terminal
24
24
  width: process.stdout.columns || 80,
25
25
  // Customize code block rendering
26
- codespan: chalk_1.default.cyan
27
- })
26
+ codespan: chalk_1.default.cyan,
27
+ }),
28
28
  });
29
29
  /**
30
30
  * Render markdown text to terminal-friendly formatted text
@@ -66,8 +66,8 @@ function containsMarkdown(text) {
66
66
  /^\s*>\s+/m, // Blockquotes
67
67
  /\|.*\|.*\|/, // Tables
68
68
  /^---+$/m, // Horizontal rules
69
- /^===+$/m // Alternative headers
69
+ /^===+$/m, // Alternative headers
70
70
  ];
71
- return markdownPatterns.some(pattern => pattern.test(text));
71
+ return markdownPatterns.some((pattern) => pattern.test(text));
72
72
  }
73
73
  exports.containsMarkdown = containsMarkdown;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fixOpenCodeConfig = exports.validateOpenCodeConfig = void 0;
7
+ const ajv_1 = __importDefault(require("ajv"));
8
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ const path_2 = require("path");
12
+ // Load the official OpenCode JSON Schema
13
+ const __dirname = (0, path_2.dirname)(__filename);
14
+ const schemaPath = (0, path_1.join)(__dirname, '..', 'schemas', 'opencode-schema.json');
15
+ let ajv;
16
+ let openCodeSchema;
17
+ let validateFunction;
18
+ try {
19
+ const schemaContent = (0, fs_1.readFileSync)(schemaPath, 'utf-8');
20
+ openCodeSchema = JSON.parse(schemaContent);
21
+ // Initialize AJV with formats and options
22
+ ajv = new ajv_1.default({
23
+ allErrors: true,
24
+ verbose: true,
25
+ strict: false,
26
+ allowUnionTypes: true,
27
+ removeAdditional: false,
28
+ });
29
+ // Add JSON Schema formats
30
+ (0, ajv_formats_1.default)(ajv);
31
+ // Compile the schema
32
+ validateFunction = ajv.compile(openCodeSchema);
33
+ }
34
+ catch (error) {
35
+ console.error('Failed to load OpenCode schema:', error);
36
+ throw new Error('Could not initialize OpenCode validator');
37
+ }
38
+ /**
39
+ * Validate OpenCode configuration against the official JSON Schema
40
+ */
41
+ function validateOpenCodeConfig(config) {
42
+ var _a;
43
+ try {
44
+ if (!validateFunction) {
45
+ return { valid: false, errors: ['Schema validator not initialized'] };
46
+ }
47
+ const isValid = validateFunction(config);
48
+ if (isValid) {
49
+ return { valid: true };
50
+ }
51
+ else {
52
+ const errors = ((_a = validateFunction.errors) === null || _a === void 0 ? void 0 : _a.map((err) => {
53
+ const path = err.instancePath || err.schemaPath || 'root';
54
+ const message = err.message || 'Unknown error';
55
+ return `${path}: ${message}`;
56
+ })) || ['Unknown validation error'];
57
+ return { valid: false, errors };
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.error('Validation error:', error);
62
+ return { valid: false, errors: ['Validation process failed'] };
63
+ }
64
+ }
65
+ exports.validateOpenCodeConfig = validateOpenCodeConfig;
66
+ /**
67
+ * Fix common OpenCode configuration issues
68
+ */
69
+ function fixOpenCodeConfig(config) {
70
+ const fixed = Object.assign({}, config);
71
+ // Fix tools.compact - should be boolean, not object
72
+ if (fixed.tools && typeof fixed.tools.compact === 'object') {
73
+ console.warn('⚠️ Converting tools.compact from object to boolean');
74
+ // If it has properties, assume it should be enabled
75
+ fixed.tools.compact = true;
76
+ }
77
+ // Remove invalid properties
78
+ const invalidProps = ['maxTokens', 'contextWindow'];
79
+ invalidProps.forEach((prop) => {
80
+ if (fixed[prop] !== undefined) {
81
+ console.warn(`⚠️ Removing invalid property: ${prop}`);
82
+ delete fixed[prop];
83
+ }
84
+ });
85
+ // Fix provider models with invalid properties
86
+ if (fixed.provider) {
87
+ Object.values(fixed.provider).forEach((provider) => {
88
+ if (provider === null || provider === void 0 ? void 0 : provider.models) {
89
+ Object.values(provider.models).forEach((model) => {
90
+ if (model && typeof model === 'object') {
91
+ // Move maxTokens/contextWindow to proper structure if needed
92
+ if (model.maxTokens || model.contextWindow) {
93
+ if (!model.limit)
94
+ model.limit = {};
95
+ // Use the larger of maxTokens/contextWindow for context
96
+ const contextValues = [
97
+ model.maxTokens,
98
+ model.contextWindow,
99
+ ].filter(Boolean);
100
+ if (contextValues.length > 0) {
101
+ const newContext = Math.max(...contextValues);
102
+ if (!model.limit.context || newContext > model.limit.context) {
103
+ model.limit.context = newContext;
104
+ }
105
+ }
106
+ // Set a reasonable default for output if not present
107
+ // (typically 1/4 to 1/8 of context window)
108
+ if (!model.limit.output && model.limit.context) {
109
+ model.limit.output = Math.floor(model.limit.context / 4);
110
+ }
111
+ delete model.maxTokens;
112
+ delete model.contextWindow;
113
+ console.warn('⚠️ Moved maxTokens/contextWindow to limit.context/output');
114
+ }
115
+ }
116
+ });
117
+ }
118
+ });
119
+ }
120
+ return fixed;
121
+ }
122
+ exports.fixOpenCodeConfig = fixOpenCodeConfig;
@@ -135,7 +135,7 @@ class TokenManager {
135
135
  this.tokenData = {
136
136
  access_token: accessToken,
137
137
  refresh_token: refreshToken,
138
- expires_at: Date.now() + (expiresIn * 1000)
138
+ expires_at: Date.now() + expiresIn * 1000,
139
139
  };
140
140
  this.saveToken();
141
141
  }
@@ -148,7 +148,7 @@ class TokenManager {
148
148
  if (!this.tokenData)
149
149
  return;
150
150
  this.tokenData.access_token = accessToken;
151
- this.tokenData.expires_at = Date.now() + (expiresIn * 1000);
151
+ this.tokenData.expires_at = Date.now() + expiresIn * 1000;
152
152
  this.saveToken();
153
153
  }
154
154
  /**
@@ -20,8 +20,8 @@ vitest_1.vi.mock('../../src/utils/default-api-key');
20
20
  vitest_1.vi.mock('readline', () => ({
21
21
  createInterface: vitest_1.vi.fn(() => ({
22
22
  question: vitest_1.vi.fn(),
23
- close: vitest_1.vi.fn()
24
- }))
23
+ close: vitest_1.vi.fn(),
24
+ })),
25
25
  }));
26
26
  (0, vitest_1.describe)('Chat Commands', () => {
27
27
  let program;
@@ -32,13 +32,13 @@ vitest_1.vi.mock('readline', () => ({
32
32
  // Mock ChatService
33
33
  mockChatService = {
34
34
  createCompletion: vitest_1.vi.fn(),
35
- listModels: vitest_1.vi.fn()
35
+ listModels: vitest_1.vi.fn(),
36
36
  };
37
37
  vitest_1.vi.mocked(chat_service_1.ChatService.getInstance).mockReturnValue(mockChatService);
38
38
  // Mock DefaultApiKeyManager
39
39
  mockDefaultApiKeyManager = {
40
40
  getDefaultApiKeyData: vitest_1.vi.fn(),
41
- promptForDefaultApiKey: vitest_1.vi.fn()
41
+ promptForDefaultApiKey: vitest_1.vi.fn(),
42
42
  };
43
43
  vitest_1.vi.mocked(default_api_key_1.DefaultApiKeyManager.getInstance).mockReturnValue(mockDefaultApiKeyManager);
44
44
  (0, chat_1.registerChatCommands)(program);
@@ -48,19 +48,19 @@ vitest_1.vi.mock('readline', () => ({
48
48
  });
49
49
  (0, vitest_1.describe)('chat run command', () => {
50
50
  (0, vitest_1.it)('should use openai/gpt-oss as default model', () => {
51
- const chatCommand = program.commands.find(cmd => cmd.name() === 'chat');
52
- const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find(cmd => cmd.name() === 'run');
51
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
52
+ const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'run');
53
53
  (0, vitest_1.expect)(runCommand).toBeDefined();
54
54
  // Check the help text which contains the default model
55
55
  const helpText = runCommand === null || runCommand === void 0 ? void 0 : runCommand.helpInformation();
56
56
  (0, vitest_1.expect)(helpText).toContain('openai/gpt-oss');
57
57
  });
58
58
  (0, vitest_1.it)('should have streaming enabled by default', () => {
59
- const chatCommand = program.commands.find(cmd => cmd.name() === 'chat');
60
- const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find(cmd => cmd.name() === 'run');
59
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
60
+ const runCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'run');
61
61
  (0, vitest_1.expect)(runCommand).toBeDefined();
62
62
  // Check that the option is --no-stream (meaning streaming is default)
63
- const streamOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find(opt => opt.long === '--no-stream');
63
+ const streamOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--no-stream');
64
64
  (0, vitest_1.expect)(streamOption).toBeDefined();
65
65
  (0, vitest_1.expect)(streamOption === null || streamOption === void 0 ? void 0 : streamOption.description).toContain('Disable streaming');
66
66
  });
@@ -69,9 +69,11 @@ vitest_1.vi.mock('readline', () => ({
69
69
  process.env.BERGET_API_KEY = 'test-key';
70
70
  // Mock successful completion
71
71
  mockChatService.createCompletion.mockResolvedValue({
72
- choices: [{
73
- message: { content: 'Test response' }
74
- }]
72
+ choices: [
73
+ {
74
+ message: { content: 'Test response' },
75
+ },
76
+ ],
75
77
  });
76
78
  // This would normally test the actual command execution
77
79
  // but since it involves readline interaction, we just verify
@@ -92,14 +94,14 @@ vitest_1.vi.mock('readline', () => ({
92
94
  capabilities: {
93
95
  vision: false,
94
96
  function_calling: true,
95
- json_mode: true
96
- }
97
- }
98
- ]
97
+ json_mode: true,
98
+ },
99
+ },
100
+ ],
99
101
  };
100
102
  mockChatService.listModels.mockResolvedValue(mockModels);
101
- const chatCommand = program.commands.find(cmd => cmd.name() === 'chat');
102
- const listCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find(cmd => cmd.name() === 'list');
103
+ const chatCommand = program.commands.find((cmd) => cmd.name() === 'chat');
104
+ const listCommand = chatCommand === null || chatCommand === void 0 ? void 0 : chatCommand.commands.find((cmd) => cmd.name() === 'list');
103
105
  (0, vitest_1.expect)(listCommand).toBeDefined();
104
106
  (0, vitest_1.expect)(listCommand === null || listCommand === void 0 ? void 0 : listCommand.description()).toBe('List available chat models');
105
107
  }));