wiggum-cli 0.3.0 → 0.3.2
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/dist/ai/agents/codebase-analyst.d.ts.map +1 -1
- package/dist/ai/agents/codebase-analyst.js +23 -27
- package/dist/ai/agents/codebase-analyst.js.map +1 -1
- package/dist/ai/agents/index.d.ts.map +1 -1
- package/dist/ai/agents/index.js +38 -2
- package/dist/ai/agents/index.js.map +1 -1
- package/dist/ai/agents/orchestrator.d.ts +1 -1
- package/dist/ai/agents/orchestrator.d.ts.map +1 -1
- package/dist/ai/agents/orchestrator.js +18 -24
- package/dist/ai/agents/orchestrator.js.map +1 -1
- package/dist/ai/agents/stack-researcher.d.ts.map +1 -1
- package/dist/ai/agents/stack-researcher.js +21 -26
- package/dist/ai/agents/stack-researcher.js.map +1 -1
- package/dist/ai/enhancer.d.ts.map +1 -1
- package/dist/ai/enhancer.js +24 -32
- package/dist/ai/enhancer.js.map +1 -1
- package/dist/utils/json-repair.d.ts +14 -0
- package/dist/utils/json-repair.d.ts.map +1 -0
- package/dist/utils/json-repair.js +146 -0
- package/dist/utils/json-repair.js.map +1 -0
- package/dist/utils/tracing.d.ts +25 -0
- package/dist/utils/tracing.d.ts.map +1 -0
- package/dist/utils/tracing.js +64 -0
- package/dist/utils/tracing.js.map +1 -0
- package/package.json +3 -1
- package/src/ai/agents/codebase-analyst.ts +27 -30
- package/src/ai/agents/index.ts +37 -2
- package/src/ai/agents/orchestrator.ts +22 -26
- package/src/ai/agents/stack-researcher.ts +23 -27
- package/src/ai/enhancer.ts +26 -34
- package/src/utils/json-repair.ts +163 -0
- package/src/utils/tracing.ts +76 -0
package/src/ai/enhancer.ts
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* Uses AI to analyze the codebase for deeper insights
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { stepCountIs } from 'ai';
|
|
7
7
|
import type { ScanResult, DetectedStack, DetectionResult } from '../scanner/types.js';
|
|
8
8
|
import { getModel, type AIProvider, hasApiKey, getApiKeyEnvVar, isReasoningModel } from './providers.js';
|
|
9
9
|
import { SYSTEM_PROMPT, SYSTEM_PROMPT_AGENTIC, createAnalysisPrompt } from './prompts.js';
|
|
10
10
|
import { createExplorationTools } from './tools.js';
|
|
11
11
|
import { runMultiAgentAnalysis, type MultiAgentAnalysis } from './agents/index.js';
|
|
12
12
|
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { parseJsonSafe } from '../utils/json-repair.js';
|
|
14
|
+
import { getTracedAI, traced } from '../utils/tracing.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Project context from AI analysis - key structure information
|
|
@@ -127,46 +129,26 @@ function parseAIResponse(text: string, verbose: boolean = false): AIAnalysisResu
|
|
|
127
129
|
return null;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// The AI might wrap it in markdown code blocks
|
|
133
|
-
let jsonText = text;
|
|
132
|
+
// Use safe JSON parser with repair capabilities
|
|
133
|
+
const result = parseJsonSafe<AIAnalysisResult>(text);
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Try to find JSON object - use greedy match for the outermost braces
|
|
142
|
-
// This handles cases where there's text before/after the JSON
|
|
143
|
-
const objectMatch = jsonText.match(/\{[\s\S]*\}/);
|
|
144
|
-
if (objectMatch) {
|
|
145
|
-
jsonText = objectMatch[0];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Try to parse JSON
|
|
149
|
-
const result = JSON.parse(jsonText) as AIAnalysisResult;
|
|
150
|
-
|
|
151
|
-
// Validate that we got the expected structure
|
|
152
|
-
if (!result || typeof result !== 'object') {
|
|
153
|
-
if (verbose) {
|
|
154
|
-
logger.warn('AI response parsed but is not an object');
|
|
155
|
-
}
|
|
156
|
-
return null;
|
|
135
|
+
if (!result) {
|
|
136
|
+
if (verbose) {
|
|
137
|
+
logger.warn('Failed to parse AI response as JSON');
|
|
138
|
+
logger.warn(`Response preview: ${text.substring(0, 500)}...`);
|
|
157
139
|
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
158
142
|
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
// Validate that we got the expected structure
|
|
144
|
+
if (typeof result !== 'object') {
|
|
161
145
|
if (verbose) {
|
|
162
|
-
logger.warn(
|
|
163
|
-
// Log first 500 chars of response for debugging
|
|
164
|
-
logger.warn(`Response preview: ${text.substring(0, 500)}...`);
|
|
165
|
-
} else {
|
|
166
|
-
logger.warn('Failed to parse AI response as JSON');
|
|
146
|
+
logger.warn('AI response parsed but is not an object');
|
|
167
147
|
}
|
|
168
148
|
return null;
|
|
169
149
|
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
170
152
|
}
|
|
171
153
|
|
|
172
154
|
/**
|
|
@@ -310,6 +292,7 @@ export class AIEnhancer {
|
|
|
310
292
|
scanResult: ScanResult
|
|
311
293
|
): Promise<AIAnalysisResult | null> {
|
|
312
294
|
const prompt = createAnalysisPrompt(scanResult);
|
|
295
|
+
const { generateText } = getTracedAI();
|
|
313
296
|
|
|
314
297
|
const { text } = await generateText({
|
|
315
298
|
model,
|
|
@@ -318,6 +301,10 @@ export class AIEnhancer {
|
|
|
318
301
|
maxOutputTokens: 2000,
|
|
319
302
|
// Reasoning models don't support temperature
|
|
320
303
|
...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
|
|
304
|
+
experimental_telemetry: {
|
|
305
|
+
isEnabled: true,
|
|
306
|
+
metadata: { phase: 'simple-analysis', projectRoot: scanResult.projectRoot },
|
|
307
|
+
},
|
|
321
308
|
});
|
|
322
309
|
|
|
323
310
|
return parseAIResponse(text);
|
|
@@ -361,6 +348,7 @@ export class AIEnhancer {
|
|
|
361
348
|
scanResult: ScanResult
|
|
362
349
|
): Promise<AIAnalysisResult | null> {
|
|
363
350
|
const tools = createExplorationTools(scanResult.projectRoot);
|
|
351
|
+
const { generateText } = getTracedAI();
|
|
364
352
|
|
|
365
353
|
const prompt = `Analyze this codebase and produce configuration for AI-assisted development.
|
|
366
354
|
|
|
@@ -401,6 +389,10 @@ When done exploring, output your final analysis as valid JSON matching this stru
|
|
|
401
389
|
stopWhen: stepCountIs(10),
|
|
402
390
|
maxOutputTokens: 4000,
|
|
403
391
|
...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
|
|
392
|
+
experimental_telemetry: {
|
|
393
|
+
isEnabled: true,
|
|
394
|
+
metadata: { phase: 'legacy-agentic', projectRoot: scanResult.projectRoot },
|
|
395
|
+
},
|
|
404
396
|
});
|
|
405
397
|
|
|
406
398
|
// Try to get text from the result
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Repair Utility
|
|
3
|
+
* Fixes common JSON syntax errors from AI responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fix mismatched brackets in JSON
|
|
8
|
+
* AI sometimes writes } instead of ] or vice versa
|
|
9
|
+
*/
|
|
10
|
+
function fixMismatchedBrackets(text: string): string {
|
|
11
|
+
const chars = text.split('');
|
|
12
|
+
const stack: Array<{ char: string; index: number }> = [];
|
|
13
|
+
let inString = false;
|
|
14
|
+
let escapeNext = false;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < chars.length; i++) {
|
|
17
|
+
const char = chars[i];
|
|
18
|
+
|
|
19
|
+
if (escapeNext) {
|
|
20
|
+
escapeNext = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (char === '\\' && inString) {
|
|
25
|
+
escapeNext = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (char === '"') {
|
|
30
|
+
inString = !inString;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (inString) continue;
|
|
35
|
+
|
|
36
|
+
if (char === '{' || char === '[') {
|
|
37
|
+
stack.push({ char, index: i });
|
|
38
|
+
} else if (char === '}' || char === ']') {
|
|
39
|
+
if (stack.length > 0) {
|
|
40
|
+
const last = stack.pop()!;
|
|
41
|
+
const expected = last.char === '{' ? '}' : ']';
|
|
42
|
+
if (char !== expected) {
|
|
43
|
+
// Mismatch! Fix it
|
|
44
|
+
chars[i] = expected;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return chars.join('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Attempt to repair malformed JSON from AI responses
|
|
55
|
+
*/
|
|
56
|
+
export function repairJson(text: string): string {
|
|
57
|
+
let json = text;
|
|
58
|
+
|
|
59
|
+
// Remove any leading/trailing whitespace
|
|
60
|
+
json = json.trim();
|
|
61
|
+
|
|
62
|
+
// Fix mismatched brackets (AI writes } instead of ] or vice versa)
|
|
63
|
+
json = fixMismatchedBrackets(json);
|
|
64
|
+
|
|
65
|
+
// Remove trailing commas before ] or }
|
|
66
|
+
json = json.replace(/,(\s*[\]}])/g, '$1');
|
|
67
|
+
|
|
68
|
+
// Fix missing commas between array elements or object properties
|
|
69
|
+
// Pattern: value followed by newline and another value without comma
|
|
70
|
+
json = json.replace(/("|\d|true|false|null|\]|\})(\s*\n\s*)("|\[|\{)/g, '$1,$2$3');
|
|
71
|
+
|
|
72
|
+
// Fix single quotes to double quotes (but not inside strings)
|
|
73
|
+
// This is a simplified approach - may not work for all cases
|
|
74
|
+
json = json.replace(/'/g, '"');
|
|
75
|
+
|
|
76
|
+
// Remove JavaScript-style comments
|
|
77
|
+
json = json.replace(/\/\/.*$/gm, '');
|
|
78
|
+
json = json.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
79
|
+
|
|
80
|
+
// Fix unquoted keys (simple cases)
|
|
81
|
+
json = json.replace(/(\{|\,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
82
|
+
|
|
83
|
+
// Remove any text before the first { or [
|
|
84
|
+
const firstBrace = json.indexOf('{');
|
|
85
|
+
const firstBracket = json.indexOf('[');
|
|
86
|
+
let startIndex = -1;
|
|
87
|
+
|
|
88
|
+
if (firstBrace !== -1 && firstBracket !== -1) {
|
|
89
|
+
startIndex = Math.min(firstBrace, firstBracket);
|
|
90
|
+
} else if (firstBrace !== -1) {
|
|
91
|
+
startIndex = firstBrace;
|
|
92
|
+
} else if (firstBracket !== -1) {
|
|
93
|
+
startIndex = firstBracket;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (startIndex > 0) {
|
|
97
|
+
json = json.substring(startIndex);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Remove any text after the last } or ]
|
|
101
|
+
const lastBrace = json.lastIndexOf('}');
|
|
102
|
+
const lastBracket = json.lastIndexOf(']');
|
|
103
|
+
let endIndex = -1;
|
|
104
|
+
|
|
105
|
+
if (lastBrace !== -1 && lastBracket !== -1) {
|
|
106
|
+
endIndex = Math.max(lastBrace, lastBracket);
|
|
107
|
+
} else if (lastBrace !== -1) {
|
|
108
|
+
endIndex = lastBrace;
|
|
109
|
+
} else if (lastBracket !== -1) {
|
|
110
|
+
endIndex = lastBracket;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (endIndex !== -1 && endIndex < json.length - 1) {
|
|
114
|
+
json = json.substring(0, endIndex + 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return json;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse JSON with repair attempts
|
|
122
|
+
* Tries to fix common issues before parsing
|
|
123
|
+
*/
|
|
124
|
+
export function parseJsonSafe<T>(text: string): T | null {
|
|
125
|
+
// First try parsing as-is
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(text) as T;
|
|
128
|
+
} catch {
|
|
129
|
+
// Try with repairs
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try repairing the JSON
|
|
133
|
+
try {
|
|
134
|
+
const repaired = repairJson(text);
|
|
135
|
+
return JSON.parse(repaired) as T;
|
|
136
|
+
} catch {
|
|
137
|
+
// Repair failed
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Last resort: try to extract JSON from markdown code blocks
|
|
141
|
+
const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
142
|
+
if (jsonMatch) {
|
|
143
|
+
try {
|
|
144
|
+
const repaired = repairJson(jsonMatch[1]);
|
|
145
|
+
return JSON.parse(repaired) as T;
|
|
146
|
+
} catch {
|
|
147
|
+
// Still failed
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Try finding a JSON object
|
|
152
|
+
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
153
|
+
if (objectMatch) {
|
|
154
|
+
try {
|
|
155
|
+
const repaired = repairJson(objectMatch[0]);
|
|
156
|
+
return JSON.parse(repaired) as T;
|
|
157
|
+
} catch {
|
|
158
|
+
// Still failed
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Braintrust Tracing Utility
|
|
3
|
+
* Provides AI call tracing for debugging and analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { initLogger, wrapAISDK } from 'braintrust';
|
|
7
|
+
import * as ai from 'ai';
|
|
8
|
+
|
|
9
|
+
// Re-export traced utilities
|
|
10
|
+
export { traced, currentSpan, wrapTraced } from 'braintrust';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize Braintrust logger if API key is available
|
|
14
|
+
*/
|
|
15
|
+
let loggerInitialized = false;
|
|
16
|
+
|
|
17
|
+
export function initTracing(): void {
|
|
18
|
+
if (loggerInitialized) return;
|
|
19
|
+
|
|
20
|
+
const apiKey = process.env.BRAINTRUST_API_KEY;
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
// Silently skip tracing if no API key
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
initLogger({
|
|
28
|
+
apiKey,
|
|
29
|
+
projectName: process.env.BRAINTRUST_PROJECT_NAME || 'wiggum-cli',
|
|
30
|
+
});
|
|
31
|
+
loggerInitialized = true;
|
|
32
|
+
} catch {
|
|
33
|
+
// Silently fail if tracing can't be initialized
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if tracing is enabled
|
|
39
|
+
*/
|
|
40
|
+
export function isTracingEnabled(): boolean {
|
|
41
|
+
return !!process.env.BRAINTRUST_API_KEY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get wrapped AI SDK functions for automatic tracing
|
|
46
|
+
* Falls back to original functions if tracing not available
|
|
47
|
+
*/
|
|
48
|
+
export function getTracedAI() {
|
|
49
|
+
initTracing();
|
|
50
|
+
|
|
51
|
+
if (isTracingEnabled()) {
|
|
52
|
+
return wrapAISDK(ai);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return original AI SDK functions if tracing not enabled
|
|
56
|
+
return ai;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a function with tracing
|
|
61
|
+
* No-op if tracing is not enabled
|
|
62
|
+
*/
|
|
63
|
+
export function maybeTraced<T extends (...args: unknown[]) => unknown>(
|
|
64
|
+
fn: T,
|
|
65
|
+
options: { type?: string; name?: string } = {}
|
|
66
|
+
): T {
|
|
67
|
+
if (!isTracingEnabled()) {
|
|
68
|
+
return fn;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { wrapTraced } = require('braintrust');
|
|
72
|
+
return wrapTraced(fn, {
|
|
73
|
+
type: options.type || 'function',
|
|
74
|
+
name: options.name || fn.name || 'anonymous',
|
|
75
|
+
});
|
|
76
|
+
}
|