@nclamvn/vibecode-cli 1.1.0 → 1.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,226 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Iteration Tracker
3
+ // Manage and track build-test-fix iterations
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import path from 'path';
7
+ import { ensureDir, writeJson, readJson, pathExists, appendToFile } from '../utils/files.js';
8
+
9
+ /**
10
+ * @typedef {Object} IterationRecord
11
+ * @property {number} iteration - Iteration number
12
+ * @property {string} timestamp - ISO timestamp
13
+ * @property {boolean} passed - Whether tests passed
14
+ * @property {number} errorCount - Number of errors
15
+ * @property {string[]} errorTypes - Types of errors found
16
+ * @property {string[]} affectedFiles - Files with errors
17
+ * @property {number} duration - Duration in ms
18
+ * @property {string} action - What action was taken (build/fix)
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} IterationState
23
+ * @property {string} sessionId - Session identifier
24
+ * @property {string} startTime - When iteration started
25
+ * @property {number} currentIteration - Current iteration number
26
+ * @property {number} maxIterations - Max allowed iterations
27
+ * @property {IterationRecord[]} history - History of iterations
28
+ * @property {boolean} completed - Whether iteration loop completed
29
+ * @property {string} result - Final result (success/max_reached/error)
30
+ */
31
+
32
+ /**
33
+ * Create new iteration state
34
+ * @param {string} sessionId - Session identifier
35
+ * @param {number} maxIterations - Maximum iterations allowed
36
+ * @returns {IterationState}
37
+ */
38
+ export function createIterationState(sessionId, maxIterations = 3) {
39
+ return {
40
+ sessionId,
41
+ startTime: new Date().toISOString(),
42
+ currentIteration: 0,
43
+ maxIterations,
44
+ history: [],
45
+ completed: false,
46
+ result: null
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Record an iteration result
52
+ * @param {IterationState} state - Current iteration state
53
+ * @param {Object} result - Iteration result
54
+ * @returns {IterationState} - Updated state
55
+ */
56
+ export function recordIteration(state, result) {
57
+ const record = {
58
+ iteration: state.currentIteration + 1,
59
+ timestamp: new Date().toISOString(),
60
+ passed: result.passed,
61
+ errorCount: result.errorCount || 0,
62
+ errorTypes: result.errorTypes || [],
63
+ affectedFiles: result.affectedFiles || [],
64
+ duration: result.duration || 0,
65
+ action: result.action || 'build'
66
+ };
67
+
68
+ return {
69
+ ...state,
70
+ currentIteration: state.currentIteration + 1,
71
+ history: [...state.history, record]
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Check if can continue iterating
77
+ * @param {IterationState} state - Current state
78
+ * @returns {{canContinue: boolean, reason: string}}
79
+ */
80
+ export function canContinue(state) {
81
+ if (state.completed) {
82
+ return { canContinue: false, reason: 'Iteration already completed' };
83
+ }
84
+
85
+ if (state.currentIteration >= state.maxIterations) {
86
+ return { canContinue: false, reason: `Max iterations (${state.maxIterations}) reached` };
87
+ }
88
+
89
+ // Check if last iteration passed
90
+ const lastRecord = state.history[state.history.length - 1];
91
+ if (lastRecord && lastRecord.passed) {
92
+ return { canContinue: false, reason: 'Tests passed - no more iterations needed' };
93
+ }
94
+
95
+ // Check for stuck loop (same errors repeated 3 times)
96
+ if (state.history.length >= 3) {
97
+ const last3 = state.history.slice(-3);
98
+ const errorCounts = last3.map(r => r.errorCount);
99
+ if (errorCounts.every(c => c === errorCounts[0]) && errorCounts[0] > 0) {
100
+ return { canContinue: false, reason: 'Stuck in loop - same error count for 3 iterations' };
101
+ }
102
+ }
103
+
104
+ return { canContinue: true, reason: '' };
105
+ }
106
+
107
+ /**
108
+ * Finalize iteration state
109
+ * @param {IterationState} state - Current state
110
+ * @param {string} result - Result type (success/max_reached/error/stuck)
111
+ * @returns {IterationState}
112
+ */
113
+ export function finalizeIterationState(state, result) {
114
+ return {
115
+ ...state,
116
+ completed: true,
117
+ result,
118
+ endTime: new Date().toISOString(),
119
+ totalDuration: state.history.reduce((sum, r) => sum + r.duration, 0)
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Save iteration state to session directory
125
+ * @param {string} sessionDir - Session directory path
126
+ * @param {IterationState} state - Iteration state
127
+ */
128
+ export async function saveIterationState(sessionDir, state) {
129
+ const iterationDir = path.join(sessionDir, 'iterations');
130
+ await ensureDir(iterationDir);
131
+
132
+ const stateFile = path.join(iterationDir, 'state.json');
133
+ await writeJson(stateFile, state, { spaces: 2 });
134
+
135
+ // Also write individual iteration files for evidence
136
+ for (const record of state.history) {
137
+ const recordFile = path.join(iterationDir, `iteration-${record.iteration}.json`);
138
+ if (!await pathExists(recordFile)) {
139
+ await writeJson(recordFile, record, { spaces: 2 });
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load iteration state from session directory
146
+ * @param {string} sessionDir - Session directory path
147
+ * @returns {Promise<IterationState|null>}
148
+ */
149
+ export async function loadIterationState(sessionDir) {
150
+ const stateFile = path.join(sessionDir, 'iterations', 'state.json');
151
+ if (await pathExists(stateFile)) {
152
+ return await readJson(stateFile);
153
+ }
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * Format iteration summary for display
159
+ * @param {IterationState} state - Iteration state
160
+ * @returns {string}
161
+ */
162
+ export function formatIterationSummary(state) {
163
+ const lines = [];
164
+
165
+ lines.push(`Iteration Summary (${state.sessionId})`);
166
+ lines.push('═'.repeat(50));
167
+ lines.push(`Total Iterations: ${state.currentIteration}/${state.maxIterations}`);
168
+ lines.push(`Result: ${state.result || 'In Progress'}`);
169
+ lines.push('');
170
+
171
+ if (state.history.length > 0) {
172
+ lines.push('History:');
173
+ for (const record of state.history) {
174
+ const status = record.passed ? '✅' : '❌';
175
+ const errors = record.errorCount > 0 ? ` (${record.errorCount} errors)` : '';
176
+ lines.push(` ${record.iteration}. ${status} ${record.action}${errors} - ${formatDuration(record.duration)}`);
177
+ }
178
+ }
179
+
180
+ if (state.totalDuration) {
181
+ lines.push('');
182
+ lines.push(`Total Duration: ${formatDuration(state.totalDuration)}`);
183
+ }
184
+
185
+ return lines.join('\n');
186
+ }
187
+
188
+ /**
189
+ * Format duration in human readable format
190
+ */
191
+ function formatDuration(ms) {
192
+ if (ms < 1000) return `${ms}ms`;
193
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
194
+ return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
195
+ }
196
+
197
+ /**
198
+ * Write iteration log entry
199
+ */
200
+ export async function logIteration(logPath, iteration, message) {
201
+ const timestamp = new Date().toISOString();
202
+ const entry = `[${timestamp}] [Iteration ${iteration}] ${message}\n`;
203
+ await appendToFile(logPath, entry);
204
+ }
205
+
206
+ /**
207
+ * Get progress percentage
208
+ */
209
+ export function getProgressPercent(state) {
210
+ if (state.completed && state.result === 'success') {
211
+ return 100;
212
+ }
213
+ return Math.round((state.currentIteration / state.maxIterations) * 100);
214
+ }
215
+
216
+ /**
217
+ * Check if errors are improving (decreasing)
218
+ */
219
+ export function isImproving(state) {
220
+ if (state.history.length < 2) return true;
221
+
222
+ const last = state.history[state.history.length - 1];
223
+ const prev = state.history[state.history.length - 2];
224
+
225
+ return last.errorCount < prev.errorCount;
226
+ }
@@ -0,0 +1,248 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Test Runner
3
+ // Automated test execution for iterative builds
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import path from 'path';
9
+ import { pathExists, readJson } from '../utils/files.js';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * Run all available tests for a project
15
+ * @param {string} projectPath - Path to the project
16
+ * @returns {Promise<TestResults>}
17
+ */
18
+ export async function runTests(projectPath) {
19
+ const results = {
20
+ passed: true,
21
+ tests: [],
22
+ errors: [],
23
+ summary: {
24
+ total: 0,
25
+ passed: 0,
26
+ failed: 0
27
+ },
28
+ duration: 0
29
+ };
30
+
31
+ const startTime = Date.now();
32
+
33
+ // 1. Check if package.json exists
34
+ const packageJsonPath = path.join(projectPath, 'package.json');
35
+ const hasPackageJson = await pathExists(packageJsonPath);
36
+
37
+ if (hasPackageJson) {
38
+ const pkg = await readJson(packageJsonPath);
39
+
40
+ // 2. Run npm test (if script exists)
41
+ if (pkg.scripts?.test && !pkg.scripts.test.includes('no test specified')) {
42
+ const npmTest = await runCommand('npm test', projectPath, 'npm test');
43
+ results.tests.push(npmTest);
44
+ }
45
+
46
+ // 3. Run npm run lint (if script exists)
47
+ if (pkg.scripts?.lint) {
48
+ const npmLint = await runCommand('npm run lint', projectPath, 'npm lint');
49
+ results.tests.push(npmLint);
50
+ }
51
+
52
+ // 4. Run npm run build (if script exists) - check for build errors
53
+ if (pkg.scripts?.build) {
54
+ const npmBuild = await runCommand('npm run build', projectPath, 'npm build');
55
+ results.tests.push(npmBuild);
56
+ }
57
+
58
+ // 5. Run TypeScript check if tsconfig exists
59
+ const tsconfigPath = path.join(projectPath, 'tsconfig.json');
60
+ if (await pathExists(tsconfigPath)) {
61
+ const tscCheck = await runCommand('npx tsc --noEmit', projectPath, 'typescript');
62
+ results.tests.push(tscCheck);
63
+ }
64
+ }
65
+
66
+ // 6. Check for syntax errors in JS files
67
+ const syntaxCheck = await checkJsSyntax(projectPath);
68
+ if (syntaxCheck.ran) {
69
+ results.tests.push(syntaxCheck);
70
+ }
71
+
72
+ // 7. Aggregate results
73
+ results.summary.total = results.tests.length;
74
+ results.summary.passed = results.tests.filter(t => t.passed).length;
75
+ results.summary.failed = results.tests.filter(t => !t.passed).length;
76
+ results.passed = results.tests.length === 0 || results.tests.every(t => t.passed);
77
+ results.errors = results.tests.filter(t => !t.passed).flatMap(t => t.errors || []);
78
+ results.duration = Date.now() - startTime;
79
+
80
+ return results;
81
+ }
82
+
83
+ /**
84
+ * Run a single command and capture results
85
+ */
86
+ async function runCommand(command, cwd, name) {
87
+ const result = {
88
+ name,
89
+ command,
90
+ passed: false,
91
+ ran: true,
92
+ output: '',
93
+ errors: [],
94
+ duration: 0
95
+ };
96
+
97
+ const startTime = Date.now();
98
+
99
+ try {
100
+ const { stdout, stderr } = await execAsync(command, {
101
+ cwd,
102
+ timeout: 120000, // 2 minute timeout
103
+ maxBuffer: 10 * 1024 * 1024
104
+ });
105
+
106
+ result.passed = true;
107
+ result.output = stdout + stderr;
108
+ result.duration = Date.now() - startTime;
109
+
110
+ } catch (error) {
111
+ result.passed = false;
112
+ result.output = error.stdout || '';
113
+ result.error = error.stderr || error.message;
114
+ result.exitCode = error.code;
115
+ result.duration = Date.now() - startTime;
116
+
117
+ // Parse errors from output
118
+ result.errors = parseErrors(error.stderr || error.stdout || error.message, name);
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Check JavaScript syntax errors
126
+ */
127
+ async function checkJsSyntax(projectPath) {
128
+ const result = {
129
+ name: 'syntax-check',
130
+ passed: true,
131
+ ran: false,
132
+ errors: []
133
+ };
134
+
135
+ try {
136
+ // Find JS/TS files (limited to src/ to avoid node_modules)
137
+ const { stdout } = await execAsync(
138
+ 'find src -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" 2>/dev/null | head -20',
139
+ { cwd: projectPath }
140
+ );
141
+
142
+ const files = stdout.trim().split('\n').filter(f => f);
143
+ if (files.length === 0) return result;
144
+
145
+ result.ran = true;
146
+
147
+ // Check each file for syntax errors using node --check
148
+ for (const file of files) {
149
+ if (file.endsWith('.ts') || file.endsWith('.tsx')) continue; // Skip TS files
150
+
151
+ try {
152
+ await execAsync(`node --check "${file}"`, { cwd: projectPath });
153
+ } catch (error) {
154
+ result.passed = false;
155
+ result.errors.push({
156
+ file,
157
+ message: error.message,
158
+ type: 'syntax'
159
+ });
160
+ }
161
+ }
162
+ } catch (error) {
163
+ // find command failed, skip syntax check
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * Parse error messages into structured format
171
+ */
172
+ function parseErrors(errorOutput, source) {
173
+ const errors = [];
174
+ const lines = errorOutput.split('\n');
175
+
176
+ for (const line of lines) {
177
+ // Match common error patterns
178
+ // Pattern: file.js:10:5: error message
179
+ const fileLineMatch = line.match(/([^\s:]+):(\d+):(\d+)?:?\s*(.+)/);
180
+ if (fileLineMatch) {
181
+ errors.push({
182
+ source,
183
+ file: fileLineMatch[1],
184
+ line: parseInt(fileLineMatch[2]),
185
+ column: fileLineMatch[3] ? parseInt(fileLineMatch[3]) : null,
186
+ message: fileLineMatch[4].trim(),
187
+ raw: line
188
+ });
189
+ continue;
190
+ }
191
+
192
+ // Pattern: Error: message
193
+ const errorMatch = line.match(/^(Error|TypeError|SyntaxError|ReferenceError):\s*(.+)/);
194
+ if (errorMatch) {
195
+ errors.push({
196
+ source,
197
+ type: errorMatch[1],
198
+ message: errorMatch[2].trim(),
199
+ raw: line
200
+ });
201
+ continue;
202
+ }
203
+
204
+ // Pattern: ✖ or ✗ or FAIL
205
+ if (line.includes('✖') || line.includes('✗') || line.includes('FAIL')) {
206
+ errors.push({
207
+ source,
208
+ message: line.trim(),
209
+ raw: line
210
+ });
211
+ }
212
+ }
213
+
214
+ // If no structured errors found, add the whole output
215
+ if (errors.length === 0 && errorOutput.trim()) {
216
+ errors.push({
217
+ source,
218
+ message: errorOutput.substring(0, 500),
219
+ raw: errorOutput
220
+ });
221
+ }
222
+
223
+ return errors;
224
+ }
225
+
226
+ /**
227
+ * Format test results for display
228
+ */
229
+ export function formatTestResults(results) {
230
+ const lines = [];
231
+
232
+ lines.push(`Tests: ${results.summary.passed}/${results.summary.total} passed`);
233
+ lines.push(`Duration: ${(results.duration / 1000).toFixed(1)}s`);
234
+
235
+ if (!results.passed) {
236
+ lines.push('');
237
+ lines.push('Failed tests:');
238
+ for (const test of results.tests.filter(t => !t.passed)) {
239
+ lines.push(` ❌ ${test.name}`);
240
+ for (const error of test.errors || []) {
241
+ const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
242
+ lines.push(` ${loc} ${error.message?.substring(0, 80) || ''}`);
243
+ }
244
+ }
245
+ }
246
+
247
+ return lines.join('\n');
248
+ }
package/src/index.js CHANGED
@@ -16,4 +16,11 @@ export { buildCommand } from './commands/build.js';
16
16
  export { reviewCommand } from './commands/review.js';
17
17
  export { snapshotCommand } from './commands/snapshot.js';
18
18
 
19
+ // Phase C Commands
20
+ export { configCommand } from './commands/config.js';
21
+
22
+ // Constants
19
23
  export { VERSION, SPEC_HASH, STATES } from './config/constants.js';
24
+
25
+ // Providers
26
+ export { PROVIDERS, getProvider, getDefaultProvider } from './providers/index.js';
@@ -0,0 +1,159 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Claude Code Provider
3
+ // "Claude/LLM là PIPELINE, là KIẾN TRÚC SƯ"
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import { spawn } from 'child_process';
7
+ import path from 'path';
8
+ import { pathExists, appendToFile } from '../utils/files.js';
9
+
10
+ /**
11
+ * Claude Code optimal configuration
12
+ * Contract LOCKED = License to build (không cần hỏi thêm)
13
+ */
14
+ export const CLAUDE_CODE_CONFIG = {
15
+ command: 'claude',
16
+ flags: [
17
+ '--dangerously-skip-permissions', // Trust the AI - Contract đã locked
18
+ ],
19
+ timeout: 30 * 60 * 1000, // 30 minutes max
20
+ };
21
+
22
+ /**
23
+ * Check if Claude Code CLI is available
24
+ */
25
+ export async function isClaudeCodeAvailable() {
26
+ return new Promise((resolve) => {
27
+ const proc = spawn('which', ['claude'], { shell: true });
28
+ proc.on('close', (code) => {
29
+ resolve(code === 0);
30
+ });
31
+ proc.on('error', () => {
32
+ resolve(false);
33
+ });
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Spawn Claude Code with optimal settings
39
+ *
40
+ * @param {string} prompt - The coder pack / prompt to send
41
+ * @param {object} options - Configuration options
42
+ * @param {string} options.cwd - Working directory
43
+ * @param {string} options.logPath - Path to write build logs
44
+ * @param {function} options.onOutput - Callback for output
45
+ * @returns {Promise<{success: boolean, code: number}>}
46
+ */
47
+ export async function spawnClaudeCode(prompt, options = {}) {
48
+ const { cwd, logPath, onOutput } = options;
49
+ const fs = await import('fs-extra');
50
+ const os = await import('os');
51
+
52
+ // Check if Claude Code is available
53
+ const available = await isClaudeCodeAvailable();
54
+ if (!available) {
55
+ throw new Error('Claude Code CLI not found. Install with: npm install -g @anthropic/claude-code');
56
+ }
57
+
58
+ // Write prompt to temp file to avoid shell escaping issues
59
+ const tempDir = os.default.tmpdir();
60
+ const promptFile = path.join(tempDir, `vibecode-prompt-${Date.now()}.md`);
61
+ await fs.default.writeFile(promptFile, prompt, 'utf-8');
62
+
63
+ return new Promise((resolve, reject) => {
64
+ // Use cat to pipe the prompt file content to claude
65
+ const command = `cat "${promptFile}" | claude ${CLAUDE_CODE_CONFIG.flags.join(' ')}`;
66
+
67
+ // Log the command being run
68
+ if (logPath) {
69
+ appendToFile(logPath, `\n[${new Date().toISOString()}] Running: claude with prompt from ${promptFile}\n`);
70
+ }
71
+
72
+ const proc = spawn(command, [], {
73
+ cwd: cwd || process.cwd(),
74
+ stdio: 'inherit', // Stream directly to terminal
75
+ shell: true,
76
+ });
77
+
78
+ let timeoutId = setTimeout(() => {
79
+ proc.kill();
80
+ // Cleanup temp file
81
+ fs.default.remove(promptFile).catch(() => {});
82
+ reject(new Error('Claude Code process timed out'));
83
+ }, CLAUDE_CODE_CONFIG.timeout);
84
+
85
+ proc.on('close', async (code) => {
86
+ clearTimeout(timeoutId);
87
+
88
+ // Cleanup temp file
89
+ await fs.default.remove(promptFile).catch(() => {});
90
+
91
+ const result = {
92
+ success: code === 0,
93
+ code: code || 0,
94
+ timestamp: new Date().toISOString()
95
+ };
96
+
97
+ if (logPath) {
98
+ const status = result.success ? 'SUCCESS' : 'FAILED';
99
+ appendToFile(logPath, `\n[${result.timestamp}] Claude Code ${status} (exit code: ${code})\n`);
100
+ }
101
+
102
+ resolve(result);
103
+ });
104
+
105
+ proc.on('error', async (error) => {
106
+ clearTimeout(timeoutId);
107
+ // Cleanup temp file
108
+ await fs.default.remove(promptFile).catch(() => {});
109
+
110
+ if (logPath) {
111
+ appendToFile(logPath, `\n[${new Date().toISOString()}] ERROR: ${error.message}\n`);
112
+ }
113
+ reject(error);
114
+ });
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Build prompt with optional CLAUDE.md injection
120
+ *
121
+ * @param {string} coderPackContent - Content of coder_pack.md
122
+ * @param {string} projectRoot - Project root directory
123
+ * @returns {Promise<string>} - Final prompt
124
+ */
125
+ export async function buildPromptWithContext(coderPackContent, projectRoot) {
126
+ let fullPrompt = coderPackContent;
127
+
128
+ // Check for CLAUDE.md in project root
129
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
130
+ if (await pathExists(claudeMdPath)) {
131
+ const fs = await import('fs-extra');
132
+ const claudeMd = await fs.default.readFile(claudeMdPath, 'utf-8');
133
+
134
+ // Inject CLAUDE.md rules before coder pack
135
+ fullPrompt = `# PROJECT RULES (from CLAUDE.md)
136
+
137
+ ${claudeMd}
138
+
139
+ ---
140
+
141
+ # BUILD INSTRUCTIONS
142
+
143
+ ${coderPackContent}`;
144
+ }
145
+
146
+ return fullPrompt;
147
+ }
148
+
149
+ /**
150
+ * Get provider info for status display
151
+ */
152
+ export function getProviderInfo() {
153
+ return {
154
+ name: 'Claude Code',
155
+ command: CLAUDE_CODE_CONFIG.command,
156
+ mode: '--dangerously-skip-permissions',
157
+ description: 'AI coding with guardrails disabled (contract-approved)'
158
+ };
159
+ }
@@ -0,0 +1,45 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - AI Provider Manager
3
+ // ═══════════════════════════════════════════════════════════════════════════════
4
+
5
+ export {
6
+ spawnClaudeCode,
7
+ isClaudeCodeAvailable,
8
+ buildPromptWithContext,
9
+ getProviderInfo,
10
+ CLAUDE_CODE_CONFIG
11
+ } from './claude-code.js';
12
+
13
+ // Future providers:
14
+ // export { callAnthropicAPI } from './anthropic-api.js';
15
+ // export { spawnCursor } from './cursor.js';
16
+
17
+ /**
18
+ * Available providers
19
+ */
20
+ export const PROVIDERS = {
21
+ 'claude-code': {
22
+ name: 'Claude Code',
23
+ description: 'Official Claude CLI for coding',
24
+ available: true
25
+ },
26
+ 'anthropic-api': {
27
+ name: 'Anthropic API',
28
+ description: 'Direct API calls (coming soon)',
29
+ available: false
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Get provider by name
35
+ */
36
+ export function getProvider(name) {
37
+ return PROVIDERS[name] || null;
38
+ }
39
+
40
+ /**
41
+ * Get default provider
42
+ */
43
+ export function getDefaultProvider() {
44
+ return 'claude-code';
45
+ }