@freetison/git-super 0.2.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.
@@ -0,0 +1,327 @@
1
+ /**
2
+ * CLI Authentication Commands
3
+ * Handles auth login, logout, status, and context management
4
+ */
5
+
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
+ import { getConfigPath, listOrganizations, loadConfig } from '../config/config-loader.mjs';
8
+ import { ProviderRegistry } from '../providers/provider-registry.mjs';
9
+ import { openBrowser } from '../auth/oauth-flows.mjs';
10
+
11
+ const colors = {
12
+ reset: '\x1b[0m',
13
+ bright: '\x1b[1m',
14
+ green: '\x1b[32m',
15
+ yellow: '\x1b[33m',
16
+ blue: '\x1b[34m',
17
+ cyan: '\x1b[36m',
18
+ red: '\x1b[31m',
19
+ };
20
+
21
+ function log(message, color = 'reset') {
22
+ console.log(`${colors[color]}${message}${colors.reset}`);
23
+ }
24
+
25
+ /**
26
+ * Handle 'auth login' command
27
+ */
28
+ export async function handleAuthLogin(args) {
29
+ const providerName = args.provider || args.p;
30
+ const orgId = args.org || args.o;
31
+
32
+ if (!providerName) {
33
+ log('Usage: git super auth login --provider <name> [--org <org-id>]', 'yellow');
34
+ log('\nAvailable OAuth providers:', 'bright');
35
+ log(' • github-copilot GitHub Copilot Enterprise', 'cyan');
36
+ log(' • azure-openai Azure OpenAI with Azure AD', 'cyan');
37
+ log(' • generic-oidc Generic OIDC provider', 'cyan');
38
+ return;
39
+ }
40
+
41
+ try {
42
+ // Load config for the specified org (if provided)
43
+ let config = loadConfig();
44
+
45
+ if (orgId && config.organizations) {
46
+ const orgConfig = config.organizations[orgId];
47
+ if (!orgConfig) {
48
+ throw new Error(`Organization '${orgId}' not found in config`);
49
+ }
50
+ config = { ...config, ...orgConfig };
51
+ log(`\n🏢 Using organization: ${orgConfig.name || orgId}`, 'cyan');
52
+ }
53
+
54
+ // Get provider instance
55
+ const registry = new ProviderRegistry(config);
56
+
57
+ // Create provider if not in registry (force initialization)
58
+ let provider;
59
+ try {
60
+ provider = registry.get(providerName);
61
+ } catch {
62
+ // Provider not auto-registered, try to create it manually
63
+ const { GitHubCopilotProvider } = await import('../providers/github-copilot-provider.mjs');
64
+ const { AzureOpenAIProvider } = await import('../providers/azure-openai-provider.mjs');
65
+ const { GenericOIDCProvider } = await import('../providers/generic-oidc-provider.mjs');
66
+
67
+ switch (providerName) {
68
+ case 'github-copilot':
69
+ provider = new GitHubCopilotProvider(config);
70
+ break;
71
+ case 'azure-openai':
72
+ provider = new AzureOpenAIProvider(config);
73
+ break;
74
+ case 'generic-oidc':
75
+ provider = new GenericOIDCProvider(config);
76
+ break;
77
+ default:
78
+ throw new Error(`Provider '${providerName}' is not an OAuth provider or doesn't exist`);
79
+ }
80
+ }
81
+
82
+ if (!provider.initiateAuth) {
83
+ throw new Error(`Provider '${providerName}' does not support OAuth authentication`);
84
+ }
85
+
86
+ log(`\n🔐 Initiating OAuth authentication for ${providerName}...`, 'bright');
87
+
88
+ // Step 1: Initiate device code flow
89
+ const deviceAuth = await provider.initiateAuth();
90
+
91
+ // Step 2: Display code to user
92
+ log(`\n📝 User Code: ${colors.bright}${deviceAuth.userCode}${colors.reset}`, 'green');
93
+ log(`\n🌐 Verification URL: ${colors.cyan}${deviceAuth.verificationUri}${colors.reset}`);
94
+
95
+ // Try to open browser
96
+ const opened = await openBrowser(deviceAuth.verificationUriComplete || deviceAuth.verificationUri);
97
+
98
+ if (opened) {
99
+ log('\n✅ Browser opened. Please complete authentication in your browser.', 'green');
100
+ } else {
101
+ log('\n⚠️ Could not open browser automatically. Please open the URL above manually.', 'yellow');
102
+ }
103
+
104
+ log(`\n⏳ Waiting for authorization (expires in ${Math.floor(deviceAuth.expiresIn / 60)} minutes)...`, 'cyan');
105
+
106
+ // Step 3: Poll for token
107
+ await provider.completeAuth(deviceAuth.deviceCode, deviceAuth.interval);
108
+
109
+ log('\n✅ Authentication successful!', 'green');
110
+
111
+ // Show token info
112
+ const tokenInfo = await provider.tokenManager.getTokenInfo();
113
+ if (tokenInfo?.expiresAt) {
114
+ const expiresAt = new Date(tokenInfo.expiresAt);
115
+ log(` Token valid until: ${expiresAt.toLocaleString()}`, 'cyan');
116
+ }
117
+
118
+ log(`\n💡 You can now use: git super`, 'bright');
119
+
120
+ } catch (error) {
121
+ log(`\n❌ Authentication failed: ${error.message}`, 'red');
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Handle 'auth logout' command
128
+ */
129
+ export async function handleAuthLogout(args) {
130
+ const providerName = args.provider || args.p;
131
+ const all = args.all || args.a;
132
+
133
+ const config = loadConfig();
134
+ const registry = new ProviderRegistry(config);
135
+
136
+ try {
137
+ if (all) {
138
+ // Logout from all OAuth providers
139
+ log('\n🔓 Logging out from all providers...', 'yellow');
140
+
141
+ for (const [name, provider] of registry.providers) {
142
+ if (provider.tokenManager) {
143
+ try {
144
+ await provider.tokenManager.revokeToken();
145
+ log(` ✅ ${name}`, 'green');
146
+ } catch (error) {
147
+ log(` ⚠️ ${name}: ${error.message}`, 'yellow');
148
+ }
149
+ }
150
+ }
151
+
152
+ log('\n✅ Logged out from all providers', 'green');
153
+ } else if (providerName) {
154
+ // Logout from specific provider
155
+ const provider = registry.get(providerName);
156
+
157
+ if (!provider.tokenManager) {
158
+ throw new Error(`Provider '${providerName}' does not use OAuth authentication`);
159
+ }
160
+
161
+ log(`\n🔓 Logging out from ${providerName}...`, 'yellow');
162
+ await provider.tokenManager.revokeToken();
163
+ log('✅ Successfully logged out', 'green');
164
+ } else {
165
+ // Logout from current provider
166
+ const currentProvider = registry.get(config.aiProvider);
167
+
168
+ if (!currentProvider.tokenManager) {
169
+ throw new Error(`Current provider '${config.aiProvider}' does not use OAuth authentication`);
170
+ }
171
+
172
+ log(`\n🔓 Logging out from ${config.aiProvider}...`, 'yellow');
173
+ await currentProvider.tokenManager.revokeToken();
174
+ log('✅ Successfully logged out', 'green');
175
+ }
176
+ } catch (error) {
177
+ log(`\n❌ Logout failed: ${error.message}`, 'red');
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Handle 'auth status' command
184
+ */
185
+ export async function handleAuthStatus() {
186
+ const config = loadConfig();
187
+ const registry = new ProviderRegistry(config);
188
+
189
+ log('\n📊 Authentication Status\n', 'bright');
190
+
191
+ // Show active context
192
+ if (config.activeOrg) {
193
+ const orgConfig = config.organizations?.[config.activeOrg];
194
+ log(`Active Context: ${colors.cyan}${orgConfig?.name || config.activeOrg}${colors.reset}`);
195
+ }
196
+
197
+ log(`Current Provider: ${colors.cyan}${config.aiProvider}${colors.reset}`);
198
+ log(`Model: ${colors.cyan}${config.aiModel}${colors.reset}\n`);
199
+
200
+ // Check each OAuth provider
201
+ for (const [name, provider] of registry.providers) {
202
+ if (provider.tokenManager) {
203
+ const tokenInfo = await provider.tokenManager.getTokenInfo();
204
+
205
+ if (tokenInfo?.hasToken) {
206
+ const status = tokenInfo.isValid ? '✅' : '❌';
207
+ const statusText = tokenInfo.isValid ? 'Valid' : 'Expired';
208
+ const expiresAt = tokenInfo.expiresAt ? new Date(tokenInfo.expiresAt).toLocaleString() : 'N/A';
209
+
210
+ log(`${status} ${colors.bright}${name}${colors.reset}: ${statusText}`, tokenInfo.isValid ? 'green' : 'red');
211
+ log(` Expires: ${expiresAt}`, 'cyan');
212
+ } else {
213
+ log(`⚪ ${colors.bright}${name}${colors.reset}: Not authenticated`, 'yellow');
214
+ }
215
+ } else {
216
+ // API key provider
217
+ if (name === config.aiProvider) {
218
+ const hasKey = await provider.authStrategy?.isValid();
219
+ const status = hasKey ? '✅' : '❌';
220
+ const statusText = hasKey ? 'API key configured' : 'No API key';
221
+ log(`${status} ${colors.bright}${name}${colors.reset}: ${statusText}`, hasKey ? 'green' : 'red');
222
+ }
223
+ }
224
+ }
225
+
226
+ console.log();
227
+ }
228
+
229
+ /**
230
+ * Handle 'context' commands
231
+ */
232
+ export async function handleContext(args) {
233
+ const subcommand = args._[1]; // 'list', 'switch', 'create'
234
+ const config = loadConfig();
235
+
236
+ switch (subcommand) {
237
+ case 'list':
238
+ case 'ls':
239
+ handleContextList();
240
+ break;
241
+
242
+ case 'switch':
243
+ case 'sw':
244
+ handleContextSwitch(args);
245
+ break;
246
+
247
+ case 'create':
248
+ log('Context creation wizard not yet implemented', 'yellow');
249
+ log('For now, edit ~/.gitsuperrc manually to add new contexts', 'cyan');
250
+ break;
251
+
252
+ default:
253
+ // No subcommand: show current context
254
+ handleContextCurrent();
255
+ }
256
+ }
257
+
258
+ function handleContextList() {
259
+ const orgs = listOrganizations();
260
+
261
+ if (orgs.length === 0) {
262
+ log('\n⚠️ No organizations configured', 'yellow');
263
+ log('Add organizations to ~/.gitsuperrc to use multi-context mode\n', 'cyan');
264
+ return;
265
+ }
266
+
267
+ log('\n📋 Available Contexts:\n', 'bright');
268
+
269
+ for (const org of orgs) {
270
+ const marker = org.isActive ? '* ' : ' ';
271
+ const color = org.isActive ? 'green' : 'cyan';
272
+ log(`${marker}${colors[color]}${org.id}${colors.reset} (${org.name})`, color);
273
+ log(` Provider: ${org.aiProvider}/${org.aiModel}`, 'cyan');
274
+ }
275
+
276
+ console.log();
277
+ }
278
+
279
+ function handleContextCurrent() {
280
+ const config = loadConfig();
281
+
282
+ log('\n📍 Current Context\n', 'bright');
283
+
284
+ if (config.activeOrg) {
285
+ const orgConfig = config.organizations?.[config.activeOrg];
286
+ log(`Organization: ${colors.cyan}${orgConfig?.name || config.activeOrg}${colors.reset}`);
287
+ } else {
288
+ log('Mode: Legacy (no organizations)', 'yellow');
289
+ }
290
+
291
+ log(`Provider: ${colors.cyan}${config.aiProvider}${colors.reset}`);
292
+ log(`Model: ${colors.cyan}${config.aiModel}${colors.reset}`);
293
+
294
+ console.log();
295
+ }
296
+
297
+ function handleContextSwitch(args) {
298
+ const targetOrg = args._[2] || args.org || args.o;
299
+
300
+ if (!targetOrg) {
301
+ log('Usage: git super context switch <org-id>', 'yellow');
302
+ return;
303
+ }
304
+
305
+ try {
306
+ const configPath = getConfigPath();
307
+ const content = readFileSync(configPath, 'utf-8');
308
+ const config = JSON.parse(content);
309
+
310
+ if (!config.organizations || !config.organizations[targetOrg]) {
311
+ throw new Error(`Organization '${targetOrg}' not found`);
312
+ }
313
+
314
+ // Update active org
315
+ config.activeOrg = targetOrg;
316
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
317
+
318
+ const orgConfig = config.organizations[targetOrg];
319
+ log(`\n✅ Switched to: ${colors.green}${orgConfig.name || targetOrg}${colors.reset}`, 'green');
320
+ log(` Provider: ${orgConfig.aiProvider}/${orgConfig.aiModel}`, 'cyan');
321
+ console.log();
322
+
323
+ } catch (error) {
324
+ log(`\n❌ Failed to switch context: ${error.message}`, 'red');
325
+ process.exit(1);
326
+ }
327
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Configuration loader with layered approach (no if-else chains)
3
+ * Layers: defaults → file config → environment variables
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+
10
+ /**
11
+ * Map environment variables to config keys
12
+ */
13
+ const ENV_MAPPINGS = {
14
+ aiProvider: 'AI_PROVIDER',
15
+ aiModel: 'AI_MODEL',
16
+ ollamaUrl: 'OLLAMA_URL',
17
+ anthropicKey: 'ANTHROPIC_API_KEY',
18
+ openaiKey: 'OPENAI_API_KEY',
19
+
20
+ // OAuth / Enterprise configs
21
+ githubOrg: 'GITHUB_ORG',
22
+ githubClientId: 'GITHUB_CLIENT_ID',
23
+ azureTenantId: 'AZURE_TENANT_ID',
24
+ azureClientId: 'AZURE_CLIENT_ID',
25
+ azureResourceEndpoint: 'AZURE_OPENAI_ENDPOINT',
26
+ oidcIssuer: 'OIDC_ISSUER',
27
+ oidcClientId: 'OIDC_CLIENT_ID',
28
+ oidcApiEndpoint: 'OIDC_API_ENDPOINT',
29
+
30
+ // Active organization context
31
+ activeOrg: 'GIT_SUPER_ACTIVE_ORG',
32
+ };
33
+
34
+ /**
35
+ * Default configuration values
36
+ */
37
+ function getDefaults() {
38
+ return {
39
+ aiProvider: 'ollama',
40
+ aiModel: 'mistral:latest',
41
+ ollamaUrl: 'http://localhost:11434',
42
+ messageTemplate: null,
43
+ ticketNumber: '',
44
+ commitRules: {
45
+ types: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'ci', 'build'],
46
+ maxLength: 72,
47
+ allowEmptyScope: true
48
+ }
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Load configuration from file
54
+ */
55
+ function loadFileConfig(configPath) {
56
+ if (!existsSync(configPath)) {
57
+ return {};
58
+ }
59
+
60
+ try {
61
+ const content = readFileSync(configPath, 'utf-8');
62
+ return JSON.parse(content);
63
+ } catch (error) {
64
+ console.warn(`⚠️ Error reading config file: ${error.message}`);
65
+ return {};
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Apply environment variable overrides using object mapping
71
+ */
72
+ function applyEnvOverrides(config) {
73
+ const overrides = {};
74
+
75
+ Object.entries(ENV_MAPPINGS).forEach(([key, envVar]) => {
76
+ const value = process.env[envVar];
77
+ if (value !== undefined) {
78
+ overrides[key] = value;
79
+ }
80
+ });
81
+
82
+ return { ...config, ...overrides };
83
+ }
84
+
85
+ /**
86
+ * Load configuration with layered approach
87
+ * Priority: ENV vars > file config > defaults
88
+ *
89
+ * Supports two modes:
90
+ * 1. Legacy mode: flat config (backward compatible)
91
+ * 2. Multi-org mode: organizations with active context
92
+ */
93
+ export function loadConfig() {
94
+ const configPath = join(homedir(), '.gitsuperrc');
95
+
96
+ // Layer 1: Defaults
97
+ let config = getDefaults();
98
+
99
+ // Layer 2: File config
100
+ const fileConfig = loadFileConfig(configPath);
101
+
102
+ // Check if using multi-org configuration
103
+ if (fileConfig.organizations) {
104
+ // Multi-org mode: load active organization context
105
+ const activeOrg = fileConfig.activeOrg || Object.keys(fileConfig.organizations)[0];
106
+ const orgConfig = fileConfig.organizations[activeOrg];
107
+
108
+ if (orgConfig) {
109
+ // Merge: defaults <- org config <- global fallbacks
110
+ config = {
111
+ ...config,
112
+ ...orgConfig,
113
+ activeOrg,
114
+ organizations: fileConfig.organizations, // Keep for context switching
115
+ };
116
+ } else {
117
+ console.warn(`⚠️ Active organization '${activeOrg}' not found in config, using defaults`);
118
+ }
119
+ } else {
120
+ // Legacy mode: flat config (backward compatible)
121
+ config = { ...config, ...fileConfig };
122
+ }
123
+
124
+ // Layer 3: Environment variables (highest priority)
125
+ config = applyEnvOverrides(config);
126
+
127
+ return config;
128
+ }
129
+
130
+ /**
131
+ * Get configuration path
132
+ * @returns {string}
133
+ */
134
+ export function getConfigPath() {
135
+ return join(homedir(), '.gitsuperrc');
136
+ }
137
+
138
+ /**
139
+ * List all available organizations from config
140
+ * @returns {Array<Object>} Array of { id, name, aiProvider, aiModel }
141
+ */
142
+ export function listOrganizations() {
143
+ const configPath = getConfigPath();
144
+ const fileConfig = loadFileConfig(configPath);
145
+
146
+ if (!fileConfig.organizations) {
147
+ return [];
148
+ }
149
+
150
+ return Object.entries(fileConfig.organizations).map(([id, org]) => ({
151
+ id,
152
+ name: org.name || id,
153
+ aiProvider: org.aiProvider || 'ollama',
154
+ aiModel: org.aiModel || 'unknown',
155
+ isActive: id === fileConfig.activeOrg,
156
+ }));
157
+ }
158
+
159
+ /**
160
+ * Get active organization ID
161
+ * @returns {string|null}
162
+ */
163
+ export function getActiveOrg() {
164
+ const configPath = getConfigPath();
165
+ const fileConfig = loadFileConfig(configPath);
166
+ return fileConfig.activeOrg || null;
167
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Add Files Strategy - Handles when only new files are added
3
+ */
4
+
5
+ import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
6
+
7
+ export class AddFilesStrategy extends BaseFallbackStrategy {
8
+ canHandle({ added, modified, deleted }) {
9
+ return added > 0 && modified === 0 && deleted === 0;
10
+ }
11
+
12
+ getMessage() {
13
+ return 'feat: add new files';
14
+ }
15
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Base Fallback Strategy - Strategy Pattern interface
3
+ * All fallback strategies must extend this class
4
+ */
5
+
6
+ export class BaseFallbackStrategy {
7
+ /**
8
+ * Check if this strategy can handle the given file stats
9
+ * @param {Object} stats - File change statistics
10
+ * @param {number} stats.added - Number of added files
11
+ * @param {number} stats.modified - Number of modified files
12
+ * @param {number} stats.deleted - Number of deleted files
13
+ * @returns {boolean}
14
+ */
15
+ canHandle(stats) {
16
+ throw new Error(`${this.constructor.name} must implement canHandle(stats)`);
17
+ }
18
+
19
+ /**
20
+ * Get the fallback commit message
21
+ * @returns {string}
22
+ */
23
+ getMessage() {
24
+ throw new Error(`${this.constructor.name} must implement getMessage()`);
25
+ }
26
+
27
+ /**
28
+ * Get strategy name (for debugging)
29
+ * @returns {string}
30
+ */
31
+ getName() {
32
+ return this.constructor.name;
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Delete Files Strategy - Handles when files are deleted
3
+ */
4
+
5
+ import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
6
+
7
+ export class DeleteFilesStrategy extends BaseFallbackStrategy {
8
+ canHandle({ deleted }) {
9
+ return deleted > 0;
10
+ }
11
+
12
+ getMessage() {
13
+ return 'chore: remove files';
14
+ }
15
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Fallback Resolver - Coordinates fallback strategies
3
+ * Uses Strategy Pattern to select appropriate message
4
+ */
5
+
6
+ import { AddFilesStrategy } from './add-files-strategy.mjs';
7
+ import { ModifyFilesStrategy } from './modify-files-strategy.mjs';
8
+ import { DeleteFilesStrategy } from './delete-files-strategy.mjs';
9
+
10
+ export class FallbackResolver {
11
+ constructor() {
12
+ // Order matters: more specific strategies first
13
+ this.strategies = [
14
+ new AddFilesStrategy(),
15
+ new DeleteFilesStrategy(),
16
+ new ModifyFilesStrategy(),
17
+ ];
18
+ this.defaultMessage = 'chore: update';
19
+ }
20
+
21
+ /**
22
+ * Resolve the appropriate fallback message based on file stats
23
+ * @param {Object} stats - File change statistics
24
+ * @param {number} stats.added - Number of added files
25
+ * @param {number} stats.modified - Number of modified files
26
+ * @param {number} stats.deleted - Number of deleted files
27
+ * @returns {string} - The fallback commit message
28
+ */
29
+ resolve(stats) {
30
+ for (const strategy of this.strategies) {
31
+ if (strategy.canHandle(stats)) {
32
+ return strategy.getMessage();
33
+ }
34
+ }
35
+
36
+ return this.defaultMessage;
37
+ }
38
+
39
+ /**
40
+ * Add a custom strategy
41
+ * @param {BaseFallbackStrategy} strategy - Strategy to add
42
+ */
43
+ addStrategy(strategy) {
44
+ this.strategies.push(strategy);
45
+ }
46
+
47
+ /**
48
+ * Set default message for when no strategy matches
49
+ * @param {string} message - Default message
50
+ */
51
+ setDefaultMessage(message) {
52
+ this.defaultMessage = message;
53
+ }
54
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Modify Files Strategy - Handles when files are modified
3
+ */
4
+
5
+ import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
6
+
7
+ export class ModifyFilesStrategy extends BaseFallbackStrategy {
8
+ canHandle({ modified }) {
9
+ return modified > 0;
10
+ }
11
+
12
+ getMessage() {
13
+ return 'refactor: update code';
14
+ }
15
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Anthropic AI Provider implementation
3
+ */
4
+
5
+ import { BaseAIProvider } from './base-provider.mjs';
6
+ import { ApiKeyAuthStrategy } from '../auth/auth-strategy.mjs';
7
+
8
+ export class AnthropicProvider extends BaseAIProvider {
9
+ constructor(config) {
10
+ // Use API Key authentication strategy
11
+ const authStrategy = new ApiKeyAuthStrategy(config, {
12
+ keyName: 'anthropicKey',
13
+ headerName: 'x-api-key',
14
+ headerFormat: '{key}',
15
+ });
16
+ super(config, authStrategy);
17
+ }
18
+
19
+ async generate(prompt) {
20
+ // Get auth headers from strategy
21
+ const authHeaders = await this.authStrategy.getAuthHeaders();
22
+
23
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ ...authHeaders,
28
+ 'anthropic-version': '2023-06-01',
29
+ },
30
+ body: JSON.stringify({
31
+ model: this.config.aiModel || 'claude-sonnet-4-20250514',
32
+ max_tokens: 200,
33
+ messages: [{ role: 'user', content: prompt }],
34
+ }),
35
+ });
36
+
37
+ if (!response.ok) {
38
+ throw new Error(`Anthropic error: ${response.statusText}`);
39
+ }
40
+
41
+ const data = await response.json();
42
+ return data.content[0].text.trim().replace(/^["']|["']$/g, '');
43
+ }
44
+ }