@vibedx/vibekit 0.1.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedx/vibekit",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A powerful CLI tool for managing development tickets and project workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,6 +18,7 @@
18
18
  "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
19
19
  "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage --detectOpenHandles",
20
20
  "test:cleanup": "rm -rf __temp__",
21
+ "test:e2e": "node scripts/e2e.js",
21
22
  "prepublishOnly": "echo 'Ready to publish'",
22
23
  "version": "git add -A && git commit -m 'chore: version bump'",
23
24
  "postversion": "git push && git push --tags"
@@ -1,96 +1,26 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { createInterface } from 'readline';
3
+ import { spawn } from 'child_process';
4
4
  import yaml from 'js-yaml';
5
5
 
6
- const SUPPORTED_PROVIDERS = {
7
- 'claude-code': 'Claude Code (Anthropic)'
8
- };
6
+ const CLAUDE_CODE_INSTALL_URL = 'https://docs.anthropic.com/en/docs/claude-code';
7
+ const CLAUDE_DETECT_TIMEOUT = 5000;
9
8
 
10
9
  /**
11
- * Create readline interface for user input
12
- */
13
- function createReadlineInterface() {
14
- return createInterface({
15
- input: process.stdin,
16
- output: process.stdout
17
- });
18
- }
19
-
20
- /**
21
- * Prompt user for input with question
22
- */
23
- function askQuestion(rl, question) {
24
- return new Promise((resolve) => {
25
- rl.question(question, (answer) => {
26
- resolve(answer.trim());
27
- });
28
- });
29
- }
30
-
31
- /**
32
- * Prompt user for password/API key (hidden input)
33
- */
34
- function askSecretQuestion(rl, question) {
35
- return new Promise((resolve) => {
36
- process.stdout.write(question);
37
-
38
- // Use readline's built-in password functionality instead of raw mode
39
- const stdin = process.stdin;
40
- const originalMode = stdin.isTTY ? stdin.setRawMode : null;
41
-
42
- if (stdin.isTTY && originalMode) {
43
- stdin.setRawMode(true);
44
- }
45
-
46
- let input = '';
47
- const onData = (buffer) => {
48
- const char = buffer.toString('utf8');
49
- const code = char.charCodeAt(0);
50
-
51
- if (code === 13 || code === 10) { // Enter key (CR or LF)
52
- if (stdin.isTTY && originalMode) {
53
- stdin.setRawMode(false);
54
- }
55
- stdin.removeListener('data', onData);
56
- console.log(); // New line
57
- resolve(input);
58
- } else if (code === 127 || code === 8) { // Backspace/Delete
59
- if (input.length > 0) {
60
- input = input.slice(0, -1);
61
- process.stdout.write('\b \b');
62
- }
63
- } else if (code === 3) { // Ctrl+C
64
- if (stdin.isTTY && originalMode) {
65
- stdin.setRawMode(false);
66
- }
67
- stdin.removeListener('data', onData);
68
- console.log('\n❌ Cancelled');
69
- process.exit(0);
70
- } else if (code >= 32 && code <= 126) { // Printable ASCII characters
71
- input += char;
72
- process.stdout.write('*');
73
- }
74
- };
75
-
76
- stdin.on('data', onData);
77
- });
78
- }
79
-
80
- /**
81
- * Load existing config.yml
10
+ * Load existing .vibe/config.yml
11
+ * @returns {Object} Parsed config object
82
12
  */
83
13
  function loadConfig() {
84
14
  const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
85
-
15
+
86
16
  if (!fs.existsSync(configPath)) {
87
17
  console.error('❌ No .vibe/config.yml found. Run "vibe init" first.');
88
18
  process.exit(1);
89
19
  }
90
-
20
+
91
21
  try {
92
- const configContent = fs.readFileSync(configPath, 'utf8');
93
- return yaml.load(configContent);
22
+ const content = fs.readFileSync(configPath, 'utf8');
23
+ return yaml.load(content);
94
24
  } catch (error) {
95
25
  console.error('❌ Error reading config.yml:', error.message);
96
26
  process.exit(1);
@@ -98,17 +28,15 @@ function loadConfig() {
98
28
  }
99
29
 
100
30
  /**
101
- * Save updated config.yml (without sensitive data)
31
+ * Save updated config to .vibe/config.yml (no sensitive data stored)
32
+ * @param {Object} config - Config object to persist
33
+ * @returns {boolean} True on success
102
34
  */
103
35
  function saveConfig(config) {
104
36
  const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
105
-
37
+
106
38
  try {
107
- const yamlContent = yaml.dump(config, {
108
- indent: 2,
109
- lineWidth: -1,
110
- noRefs: true
111
- });
39
+ const yamlContent = yaml.dump(config, { indent: 2, lineWidth: -1, noRefs: true });
112
40
  fs.writeFileSync(configPath, yamlContent, 'utf8');
113
41
  return true;
114
42
  } catch (error) {
@@ -118,278 +46,136 @@ function saveConfig(config) {
118
46
  }
119
47
 
120
48
  /**
121
- * Check if .env file exists and contains ANTHROPIC_API_KEY
49
+ * Check if the Claude Code CLI is installed and accessible
50
+ * @returns {Promise<{ installed: boolean, version: string | null }>}
122
51
  */
123
- function checkEnvFile() {
124
- const envPath = path.join(process.cwd(), '.env');
125
-
126
- if (!fs.existsSync(envPath)) {
127
- return { exists: false, hasKey: false };
128
- }
129
-
130
- try {
131
- const envContent = fs.readFileSync(envPath, 'utf8');
132
- const hasKey = envContent.includes('ANTHROPIC_API_KEY=');
133
- return { exists: true, hasKey };
134
- } catch (error) {
135
- return { exists: false, hasKey: false };
136
- }
137
- }
52
+ function detectClaudeCode() {
53
+ return new Promise((resolve) => {
54
+ let stdout = '';
138
55
 
139
- /**
140
- * Create or update .env file with API key
141
- */
142
- async function createEnvFile(rl) {
143
- const envPath = path.join(process.cwd(), '.env');
144
- const envInfo = checkEnvFile();
145
-
146
- console.log('\n📝 Setting up .env file for secure API key storage');
147
-
148
- if (envInfo.exists && envInfo.hasKey) {
149
- console.log('⚠️ .env file already contains ANTHROPIC_API_KEY');
150
- const overwrite = await askQuestion(rl, '? Update the existing API key? (y/n): ');
151
- if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
152
- return false;
153
- }
154
- }
155
-
156
- const apiKey = await askSecretQuestion(rl, '? Enter your Claude API key: ');
157
-
158
- if (!apiKey) {
159
- console.log('❌ API key is required.');
160
- return false;
161
- }
162
-
163
- // Validate API key first
164
- console.log('🔍 Validating API key...');
165
- const validation = validateClaudeApiKey(apiKey);
166
-
167
- if (!validation.valid) {
168
- console.log(`❌ ${validation.error}`);
169
- return false;
170
- }
171
-
172
- try {
173
- let envContent = '';
174
-
175
- if (envInfo.exists) {
176
- // Read existing .env and update/add ANTHROPIC_API_KEY
177
- envContent = fs.readFileSync(envPath, 'utf8');
178
-
179
- if (envInfo.hasKey) {
180
- // Replace existing key
181
- envContent = envContent.replace(/ANTHROPIC_API_KEY=.*$/m, `ANTHROPIC_API_KEY=${apiKey}`);
56
+ const child = spawn('claude', ['--version'], {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ });
59
+
60
+ child.stdout.on('data', (data) => {
61
+ stdout += data.toString();
62
+ });
63
+
64
+ const timer = setTimeout(() => {
65
+ child.kill('SIGTERM');
66
+ resolve({ installed: false, version: null });
67
+ }, CLAUDE_DETECT_TIMEOUT);
68
+
69
+ child.on('close', (code) => {
70
+ clearTimeout(timer);
71
+ if (code === 0) {
72
+ resolve({ installed: true, version: stdout.trim() || null });
182
73
  } else {
183
- // Add new key
184
- envContent += envContent.endsWith('\n') ? '' : '\n';
185
- envContent += `ANTHROPIC_API_KEY=${apiKey}\n`;
74
+ resolve({ installed: false, version: null });
186
75
  }
187
- } else {
188
- // Create new .env file
189
- envContent = `# Environment variables for VibeKit
190
- ANTHROPIC_API_KEY=${apiKey}
191
- `;
192
- }
193
-
194
- fs.writeFileSync(envPath, envContent, 'utf8');
195
- console.log('✅ API key saved to .env file');
196
- console.log('🔒 Make sure .env is in your .gitignore');
197
-
198
- // Update the current process environment
199
- process.env.ANTHROPIC_API_KEY = apiKey;
200
-
201
- return true;
202
- } catch (error) {
203
- console.error('❌ Error creating .env file:', error.message);
204
- return false;
205
- }
76
+ });
77
+
78
+ child.on('error', () => {
79
+ clearTimeout(timer);
80
+ resolve({ installed: false, version: null });
81
+ });
82
+ });
206
83
  }
207
84
 
208
85
  /**
209
- * Create AI instructions for Claude Code from template
86
+ * Create AI workflow instructions from the assets template
87
+ * @returns {Promise<boolean>} True on success
210
88
  */
211
89
  async function createAiInstructions() {
212
90
  try {
213
- // Create .context/instructions directory
214
91
  const instructionsDir = path.join(process.cwd(), '.context', 'instructions');
92
+
215
93
  if (!fs.existsSync(instructionsDir)) {
216
94
  fs.mkdirSync(instructionsDir, { recursive: true });
217
95
  }
218
-
219
- // Copy claude instructions from assets template
96
+
220
97
  const templatePath = path.join(process.cwd(), 'assets', 'instructions', 'claude.md');
221
- const claudeInstructionsPath = path.join(instructionsDir, 'claude.md');
222
-
98
+ const outputPath = path.join(instructionsDir, 'claude.md');
99
+
223
100
  if (fs.existsSync(templatePath)) {
224
- const templateContent = fs.readFileSync(templatePath, 'utf8');
225
- fs.writeFileSync(claudeInstructionsPath, templateContent, 'utf8');
226
- console.log('📄 Created .context/instructions/claude.md from template');
101
+ fs.writeFileSync(outputPath, fs.readFileSync(templatePath, 'utf8'), 'utf8');
227
102
  } else {
228
- console.warn('⚠️ Template not found, creating basic instructions');
229
- const basicContent = `# VibeKit Instructions for Claude
103
+ fs.writeFileSync(outputPath, buildFallbackInstructions(), 'utf8');
104
+ }
230
105
 
231
- This project uses VibeKit for organized development.
106
+ console.log('📄 Created .context/instructions/claude.md');
107
+ return true;
108
+ } catch (error) {
109
+ console.warn('⚠️ Could not create AI instructions:', error.message);
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Fallback instructions if the assets template is missing
116
+ * @returns {string} Markdown instruction content
117
+ */
118
+ function buildFallbackInstructions() {
119
+ return `# VibeKit Instructions for Claude
120
+
121
+ This project uses VibeKit for organised, ticket-driven development.
232
122
 
233
123
  ## Primary Rule: Always Work Through Tickets
234
- 1. Create ticket first: \`vibe new\`
124
+ 1. Create a ticket first: \`vibe new\`
235
125
  2. Start working: \`vibe start <ticket-id>\`
236
126
  3. Close when done: \`vibe close <ticket-id>\`
237
127
 
238
128
  For detailed instructions, see the VibeKit documentation.
239
129
  `;
240
- fs.writeFileSync(claudeInstructionsPath, basicContent, 'utf8');
241
- }
242
-
243
- // Create a README for the .context/instructions folder
244
- const readmePath = path.join(instructionsDir, 'README.md');
245
- const readmeContent = `# AI Instructions
246
-
247
- This folder contains instructions for different AI coding assistants.
248
-
249
- ## Files
250
- - \`claude.md\` - Instructions for Claude Code (Anthropic)
251
- - \`codex.md\` - Instructions for OpenAI Codex (coming soon)
252
-
253
- ## Important Notes
254
- - These files are automatically read by AI assistants
255
- - Only modify if you understand how AI instructions work
256
- - Changes affect how AI assistants interact with this project
257
- - Maintained by VibeKit for consistent development workflow
258
-
259
- ## Current Status
260
- - ✅ Claude Code - Active and configured
261
- - 🚧 OpenAI Codex - Coming soon
262
- `;
263
-
264
- fs.writeFileSync(readmePath, readmeContent, 'utf8');
265
- console.log('📄 Created .context/instructions/README.md with folder documentation');
266
-
267
- return true;
268
- } catch (error) {
269
- console.warn('⚠️ Could not create AI instructions:', error.message);
270
- return false;
271
- }
272
130
  }
273
131
 
274
132
  /**
275
- * Validate Claude API key format
133
+ * Print install instructions for Claude Code CLI
276
134
  */
277
- function validateClaudeApiKey(apiKey) {
278
- if (!apiKey || typeof apiKey !== 'string') {
279
- return { valid: false, error: 'API key is required' };
280
- }
281
-
282
- if (!apiKey.startsWith('sk-ant-api')) {
283
- return { valid: false, error: 'Invalid Claude API key format' };
284
- }
285
-
286
- return { valid: true };
135
+ function printInstallInstructions() {
136
+ console.log('\n📦 Install Claude Code to continue:\n');
137
+ console.log(' npm install -g @anthropic-ai/claude-code');
138
+ console.log(`\n📖 Documentation: ${CLAUDE_CODE_INSTALL_URL}`);
139
+ console.log('\nOnce installed, run "vibe link" again.\n');
287
140
  }
288
141
 
289
142
  /**
290
- * Main link command implementation
143
+ * Main link command — detects Claude Code CLI and configures the project
291
144
  */
292
145
  async function linkCommand() {
293
- console.log('🔗 VibeKit AI Provider Setup\n');
294
-
146
+ console.log('🔗 Linking VibeKit to Claude Code\n');
147
+
295
148
  const config = loadConfig();
296
- const rl = createReadlineInterface();
297
-
298
- try {
299
- // Check for environment variable first
300
- const envApiKey = process.env.ANTHROPIC_API_KEY;
301
- if (envApiKey) {
302
- console.log('🔍 Found ANTHROPIC_API_KEY in environment variables');
303
- const useEnvKey = await askQuestion(rl, '? Use this API key? (y/n): ');
304
-
305
- if (useEnvKey.toLowerCase() === 'y' || useEnvKey.toLowerCase() === 'yes') {
306
- console.log('🔍 Validating API key...');
307
- const validation = validateClaudeApiKey(envApiKey);
308
-
309
- if (validation.valid) {
310
- // Update config (never store API key in config)
311
- config.ai = {
312
- ...config.ai,
313
- provider: 'claude-code',
314
- enabled: true
315
- };
316
-
317
- if (saveConfig(config)) {
318
- console.log('✅ Environment API key validated!');
319
- console.log('🔗 Ready for ticket refinement!');
320
- console.log(`\n📝 Configuration updated:`);
321
- console.log(` Provider: ${SUPPORTED_PROVIDERS['claude-code']}`);
322
- console.log(' Source: Environment variable (ANTHROPIC_API_KEY)');
323
- console.log('\n💡 You can now use AI features like "vibe refine"');
324
-
325
- // Create AI instructions documentation
326
- await createAiInstructions();
327
- } else {
328
- console.log('❌ Failed to save configuration.');
329
- }
330
- rl.close();
331
- return;
332
- } else {
333
- console.log(`❌ Environment API key validation failed: ${validation.error}`);
334
- console.log('Continuing with setup...\n');
335
- }
336
- }
337
- }
338
-
339
- // No environment variable found or user chose not to use it
340
- console.log('\n🔑 API Key Setup Required');
341
- console.log('To use Claude Code, you need to set up your API key.');
342
- console.log('\nChoose your preferred method:');
343
- console.log('1. Export environment variable (recommended)');
344
- console.log('2. Create .env file');
345
- console.log();
346
-
347
- const methodChoice = await askQuestion(rl, '? Choose setup method (1-2): ');
348
-
349
- if (methodChoice === '1') {
350
- // Guide user to export environment variable
351
- console.log('\n📋 To set up environment variable:');
352
- console.log('\n🔹 For current session:');
353
- console.log(' export ANTHROPIC_API_KEY="your-api-key-here"');
354
- console.log('\n🔹 For permanent setup (add to ~/.bashrc or ~/.zshrc):');
355
- console.log(' echo \'export ANTHROPIC_API_KEY="your-api-key-here"\' >> ~/.bashrc');
356
- console.log('\n💡 After setting the environment variable, run "vibe link" again.');
357
- rl.close();
358
- return;
359
- } else if (methodChoice === '2') {
360
- // Create .env file
361
- const envCreated = await createEnvFile(rl);
362
-
363
- if (envCreated) {
364
- // Update config
365
- config.ai = {
366
- ...config.ai,
367
- provider: 'claude-code',
368
- enabled: true
369
- };
370
-
371
- if (saveConfig(config)) {
372
- console.log('🔗 Ready for ticket refinement!');
373
- console.log(`\n📝 Configuration updated:`);
374
- console.log(` Provider: ${SUPPORTED_PROVIDERS['claude-code']}`);
375
- console.log(' Source: .env file');
376
- console.log('\n💡 You can now use AI features like "vibe refine"');
377
-
378
- // Create AI instructions documentation
379
- await createAiInstructions();
380
- } else {
381
- console.log('❌ Failed to save configuration.');
382
- }
383
- }
384
- } else {
385
- console.log('❌ Invalid choice. Please run the command again.');
386
- }
387
-
388
- } catch (error) {
389
- console.error('❌ Error during setup:', error.message);
390
- } finally {
391
- rl.close();
149
+
150
+ console.log('🔍 Detecting Claude Code CLI...');
151
+ const { installed, version } = await detectClaudeCode();
152
+
153
+ if (!installed) {
154
+ console.error('❌ Claude Code CLI not found.');
155
+ printInstallInstructions();
156
+ process.exit(1);
157
+ }
158
+
159
+ const versionLabel = version ? ` (${version})` : '';
160
+ console.log(`✅ Claude Code detected${versionLabel}`);
161
+
162
+ config.ai = {
163
+ ...config.ai,
164
+ enabled: true,
165
+ provider: 'claude-code',
166
+ };
167
+
168
+ if (!saveConfig(config)) {
169
+ console.error('❌ Failed to save configuration.');
170
+ process.exit(1);
392
171
  }
172
+
173
+ console.log('✅ Configuration updated');
174
+
175
+ await createAiInstructions();
176
+
177
+ console.log('\n🎉 Claude Code linked successfully!');
178
+ console.log(' You can now use AI features like "vibe refine"\n');
393
179
  }
394
180
 
395
- export default linkCommand;
181
+ export default linkCommand;
@@ -1,14 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { spawn } from 'child_process';
3
4
  import yaml from 'js-yaml';
4
5
  import { resolveTicketId, parseTicket, updateTicket } from '../../utils/ticket.js';
5
6
  import { select, spinner, input, logger } from '../../utils/cli.js';
6
7
  import { arrowSelect } from '../../utils/arrow-select.js';
7
8
 
8
- // Configuration constants
9
- const CLAUDE_SDK_TIMEOUT = 30000;
10
- const ENHANCEMENT_MODEL = 'claude-3-5-sonnet-latest';
11
-
12
9
  /**
13
10
  * Load VibeKit configuration
14
11
  * @returns {Object} Configuration object
@@ -36,92 +33,20 @@ function loadConfig() {
36
33
  }
37
34
 
38
35
  /**
39
- * Check if Claude Code SDK is available
40
- * @returns {Promise<boolean>} True if SDK is available
36
+ * Check if AI is configured in .vibe/config.yml
37
+ * @returns {Object} AI configuration status
41
38
  */
42
- async function checkClaudeCodeSDK() {
43
- try {
44
- const { spawn } = await import('child_process');
45
-
46
- return new Promise((resolve) => {
47
- const child = spawn('claude', ['--version'], {
48
- stdio: 'pipe',
49
- timeout: 5000
50
- });
51
-
52
- child.on('close', (code) => {
53
- resolve(code === 0);
54
- });
55
-
56
- child.on('error', () => {
57
- resolve(false);
58
- });
59
-
60
- // Timeout fallback
61
- const timeout = setTimeout(() => {
62
- try {
63
- child.kill('SIGTERM');
64
- } catch (killError) {
65
- // Ignore kill errors
66
- }
67
- resolve(false);
68
- }, 5000);
69
-
70
- child.on('exit', () => {
71
- clearTimeout(timeout);
72
- });
73
- });
74
- } catch (error) {
75
- return false;
76
- }
77
- }
39
+ function checkAiConfiguration() {
40
+ const config = loadConfig();
78
41
 
79
- /**
80
- * Check if AI is configured and Claude Code SDK is available
81
- * @returns {Promise<Object>} AI configuration status
82
- * @throws {Error} If configuration check fails
83
- */
84
- async function checkAiConfiguration() {
85
- try {
86
- const config = loadConfig();
87
-
88
- // Check if AI is enabled in config
89
- if (!config.ai || !config.ai.enabled || config.ai.provider === 'none') {
90
- return {
91
- configured: false,
92
- needsSetup: false,
93
- reason: 'AI is not enabled in configuration'
94
- };
95
- }
96
-
97
- // Check for Claude Code SDK availability
98
- const sdkAvailable = await checkClaudeCodeSDK();
99
- if (sdkAvailable) {
100
- return { configured: true };
101
- }
102
-
103
- // SDK not available - needs installation
104
- return {
105
- configured: false,
106
- needsSetup: true,
107
- reason: 'Claude Code SDK not found'
42
+ if (!config.ai?.enabled || config.ai?.provider === 'none') {
43
+ return {
44
+ configured: false,
45
+ reason: 'AI is not enabled. Run "vibe link" first.'
108
46
  };
109
- } catch (error) {
110
- throw new Error(`Failed to check AI configuration: ${error.message}`);
111
47
  }
112
- }
113
48
 
114
- /**
115
- * Show Claude Code SDK installation information
116
- * @returns {void}
117
- */
118
- function showClaudeCodeInstallation() {
119
- logger.error('Claude Code SDK not found.');
120
- logger.info('VibeKit refine requires Claude Code SDK to enhance tickets.');
121
- console.log('\nTo install Claude Code SDK, run:');
122
- console.log(' npm install -g @anthropic-ai/claude-code');
123
- console.log('\nOr visit: https://docs.anthropic.com/en/docs/claude-code');
124
- logger.tip('After installation, run this command again.');
49
+ return { configured: true };
125
50
  }
126
51
 
127
52
 
@@ -225,125 +150,118 @@ Response must be valid JSON only.`;
225
150
  }
226
151
 
227
152
  /**
228
- * Execute Claude Code SDK command safely
153
+ * Extract a JSON object from Claude's raw text response.
154
+ * Handles plain JSON, markdown code blocks, and mixed content.
155
+ * @param {string} text - Raw text from Claude
156
+ * @returns {string} Validated JSON string
157
+ * @throws {Error} If no valid JSON object can be found
158
+ */
159
+ function extractJsonFromResponse(text) {
160
+ const cleaned = text.trim();
161
+
162
+ // 1. Direct parse — Claude responded with pure JSON
163
+ try {
164
+ JSON.parse(cleaned);
165
+ return cleaned;
166
+ } catch { /* fall through */ }
167
+
168
+ // 2. Markdown code block — ```json ... ``` or ``` ... ```
169
+ const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
170
+ if (codeBlockMatch) {
171
+ const inner = codeBlockMatch[1].trim();
172
+ try {
173
+ JSON.parse(inner);
174
+ return inner;
175
+ } catch { /* fall through */ }
176
+ }
177
+
178
+ // 3. Loose extraction — grab first {...} block
179
+ const objMatch = cleaned.match(/\{[\s\S]*\}/);
180
+ if (objMatch) {
181
+ try {
182
+ JSON.parse(objMatch[0]);
183
+ return objMatch[0];
184
+ } catch { /* fall through */ }
185
+ }
186
+
187
+ throw new Error('No valid JSON object found in Claude response');
188
+ }
189
+
190
+ /**
191
+ * Parse the JSON envelope returned by `claude --print --output-format json`.
192
+ * Throws if the response signals an error or contains no result text.
193
+ * @param {string} raw - Raw stdout from the claude subprocess
194
+ * @returns {string} Claude's text result
195
+ * @throws {Error} If the envelope signals an error or cannot be parsed
196
+ */
197
+ function parseClaudeEnvelope(raw) {
198
+ let envelope;
199
+ try {
200
+ envelope = JSON.parse(raw.trim());
201
+ } catch {
202
+ throw new Error('Claude returned non-JSON output');
203
+ }
204
+
205
+ if (envelope.is_error) {
206
+ throw new Error(envelope.result || 'Claude reported an error');
207
+ }
208
+
209
+ const text = envelope.result ?? '';
210
+ if (!text.trim()) {
211
+ throw new Error('Claude returned an empty result');
212
+ }
213
+
214
+ return text;
215
+ }
216
+
217
+ /**
218
+ * Send a prompt to Claude via `claude --print` and return the JSON response string.
219
+ * Uses the native Claude Code CLI — no API key management required.
229
220
  * @param {string} prompt - The prompt to send to Claude
230
- * @returns {Promise<string>} Claude's response
231
- * @throws {Error} If execution fails
221
+ * @returns {Promise<string>} Validated JSON string from Claude's result
222
+ * @throws {Error} If the subprocess fails or response contains no valid JSON
232
223
  */
233
224
  async function executeClaudeCommand(prompt) {
234
225
  if (typeof prompt !== 'string' || !prompt.trim()) {
235
226
  throw new Error('Prompt must be a non-empty string');
236
227
  }
237
-
238
- const { writeFileSync, unlinkSync } = await import('fs');
239
- const { exec } = await import('child_process');
240
- const { tmpdir } = await import('os');
241
- const { join } = await import('path');
242
-
228
+
243
229
  return new Promise((resolve, reject) => {
244
- const tempFile = join(tmpdir(), `vibe-prompt-${Date.now()}-${Math.random().toString(36).substring(7)}.txt`);
245
- let childProcess = null;
246
-
247
- // Cleanup function
248
- const cleanup = () => {
249
- try {
250
- if (fs.existsSync(tempFile)) {
251
- unlinkSync(tempFile);
252
- }
253
- } catch (cleanupError) {
254
- // Ignore cleanup errors
255
- }
256
-
257
- if (childProcess) {
258
- try {
259
- childProcess.kill('SIGTERM');
260
- } catch (killError) {
261
- // Ignore kill errors
262
- }
263
- }
264
- };
265
-
266
- try {
267
- // Write prompt to temporary file
268
- writeFileSync(tempFile, prompt, 'utf8');
269
- } catch (writeError) {
270
- reject(new Error(`Failed to write prompt file: ${writeError.message}`));
271
- return;
272
- }
273
-
274
- const command = `cat "${tempFile}" | claude --print --output-format json --model ${ENHANCEMENT_MODEL}`;
275
-
276
- childProcess = exec(command, {
277
- timeout: CLAUDE_SDK_TIMEOUT,
278
- maxBuffer: 2 * 1024 * 1024, // 2MB buffer
279
- killSignal: 'SIGTERM'
280
- }, (error, stdout, stderr) => {
281
- cleanup();
282
-
283
- if (error) {
284
- // Handle specific error types
285
- if (error.code === 'ENOENT') {
286
- reject(new Error('Claude Code SDK not found. Please install it first.'));
287
- } else if (error.code === 'EACCES') {
288
- reject(new Error('Permission denied accessing Claude Code SDK.'));
289
- } else if (error.signal === 'SIGTERM') {
290
- reject(new Error('Claude SDK operation timed out.'));
291
- } else {
292
- reject(new Error(`Claude Code SDK failed: ${error.message}`));
293
- }
294
- return;
295
- }
296
-
297
- // Check for stderr output
298
- if (stderr && stderr.trim()) {
299
- console.warn(`⚠️ Claude SDK warning: ${stderr.trim()}`);
300
- }
301
-
302
- // Validate stdout
303
- if (!stdout || stdout.trim() === '') {
304
- reject(new Error('Claude SDK returned empty response'));
230
+ // Create environment without API key — use native Claude Code authentication only
231
+ const env = { ...process.env };
232
+ delete env.ANTHROPIC_API_KEY;
233
+
234
+ const child = spawn('claude', ['--print', '--output-format', 'json'], {
235
+ stdio: ['pipe', 'pipe', 'pipe'],
236
+ env
237
+ });
238
+
239
+ let stdout = '';
240
+ let stderr = '';
241
+
242
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
243
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
244
+
245
+ child.on('error', (err) => {
246
+ reject(new Error(`Failed to spawn Claude: ${err.message}`));
247
+ });
248
+
249
+ child.on('close', (code) => {
250
+ if (!stdout.trim()) {
251
+ reject(new Error(stderr.trim() || `Claude exited with code ${code}`));
305
252
  return;
306
253
  }
307
-
254
+
308
255
  try {
309
- // Try to parse as JSON first
310
- const sdkResponse = JSON.parse(stdout.trim());
311
- const result = sdkResponse.result || sdkResponse.content || sdkResponse;
312
-
313
- if (typeof result === 'string') {
314
- resolve(result);
315
- } else {
316
- resolve(JSON.stringify(result));
317
- }
318
- } catch (parseError) {
319
- // If JSON parsing fails, try to extract JSON from response
320
- const jsonMatch = stdout.match(/{[\s\S]*}/);
321
- if (jsonMatch) {
322
- try {
323
- JSON.parse(jsonMatch[0]); // Validate JSON
324
- resolve(jsonMatch[0]);
325
- } catch (secondParseError) {
326
- reject(new Error(`Failed to parse Claude SDK response as JSON: ${secondParseError.message}`));
327
- }
328
- } else {
329
- reject(new Error('Claude SDK response is not valid JSON'));
330
- }
256
+ const text = parseClaudeEnvelope(stdout);
257
+ resolve(extractJsonFromResponse(text));
258
+ } catch (err) {
259
+ reject(err);
331
260
  }
332
261
  });
333
-
334
- // Handle process errors
335
- childProcess.on('error', (error) => {
336
- cleanup();
337
- reject(new Error(`Failed to run Claude Code SDK: ${error.message}`));
338
- });
339
-
340
- // Set up timeout handler
341
- setTimeout(() => {
342
- if (childProcess && !childProcess.killed) {
343
- cleanup();
344
- reject(new Error('Claude SDK operation timed out'));
345
- }
346
- }, CLAUDE_SDK_TIMEOUT + 1000);
262
+
263
+ child.stdin.write(prompt, 'utf8');
264
+ child.stdin.end();
347
265
  });
348
266
  }
349
267
 
@@ -646,14 +564,10 @@ async function refineCommand(args, options = {}) {
646
564
  logger.step(`Analyzing ticket ${ticketInput}...`);
647
565
 
648
566
  // Check AI configuration
649
- const aiStatus = await checkAiConfiguration();
567
+ const aiStatus = checkAiConfiguration();
650
568
  if (!aiStatus.configured) {
651
- if (aiStatus.needsSetup) {
652
- showClaudeCodeInstallation();
653
- } else {
654
- logger.error('AI is not enabled in config. Run "vibe link" first.');
655
- }
656
- throw new Error(aiStatus.reason || 'AI configuration check failed');
569
+ logger.error(aiStatus.reason);
570
+ return;
657
571
  }
658
572
 
659
573
  // Resolve ticket ID
@@ -686,18 +600,8 @@ async function refineCommand(args, options = {}) {
686
600
 
687
601
  } catch (error) {
688
602
  loadingSpinner.fail('Enhancement failed');
689
-
690
- if (error.message.includes('Claude Code SDK failed')) {
691
- logger.error('AI enhancement service is unavailable.');
692
- logger.info('This could be due to:');
693
- console.log(' • Claude Code SDK not installed or configured');
694
- console.log(' • Network connectivity issues');
695
- console.log(' • API rate limits or authentication problems');
696
- logger.tip('You can still view and edit the ticket manually.');
697
- } else {
698
- logger.error('Failed to enhance ticket.');
699
- logger.tip('Please check your Claude Code SDK installation and try again.');
700
- }
603
+ logger.error(error.message);
604
+ logger.tip('You can still view and edit the ticket manually.');
701
605
  return;
702
606
  }
703
607