wiggum-cli 0.3.0 → 0.3.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-repair.js","sourceRoot":"","sources":["../../src/utils/json-repair.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Braintrust Tracing Utility
3
+ * Provides AI call tracing for debugging and analysis
4
+ */
5
+ import * as ai from 'ai';
6
+ export { traced, currentSpan, wrapTraced } from 'braintrust';
7
+ export declare function initTracing(): void;
8
+ /**
9
+ * Check if tracing is enabled
10
+ */
11
+ export declare function isTracingEnabled(): boolean;
12
+ /**
13
+ * Get wrapped AI SDK functions for automatic tracing
14
+ * Falls back to original functions if tracing not available
15
+ */
16
+ export declare function getTracedAI(): typeof ai;
17
+ /**
18
+ * Wrap a function with tracing
19
+ * No-op if tracing is not enabled
20
+ */
21
+ export declare function maybeTraced<T extends (...args: unknown[]) => unknown>(fn: T, options?: {
22
+ type?: string;
23
+ name?: string;
24
+ }): T;
25
+ //# sourceMappingURL=tracing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/utils/tracing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAGzB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAO7D,wBAAgB,WAAW,IAAI,IAAI,CAkBlC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;;GAGG;AACH,wBAAgB,WAAW,cAS1B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EACnE,EAAE,EAAE,CAAC,EACL,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,CAAC,CAUH"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Braintrust Tracing Utility
3
+ * Provides AI call tracing for debugging and analysis
4
+ */
5
+ import { initLogger, wrapAISDK } from 'braintrust';
6
+ import * as ai from 'ai';
7
+ // Re-export traced utilities
8
+ export { traced, currentSpan, wrapTraced } from 'braintrust';
9
+ /**
10
+ * Initialize Braintrust logger if API key is available
11
+ */
12
+ let loggerInitialized = false;
13
+ export function initTracing() {
14
+ if (loggerInitialized)
15
+ return;
16
+ const apiKey = process.env.BRAINTRUST_API_KEY;
17
+ if (!apiKey) {
18
+ // Silently skip tracing if no API key
19
+ return;
20
+ }
21
+ try {
22
+ initLogger({
23
+ apiKey,
24
+ projectName: process.env.BRAINTRUST_PROJECT_NAME || 'wiggum-cli',
25
+ });
26
+ loggerInitialized = true;
27
+ }
28
+ catch {
29
+ // Silently fail if tracing can't be initialized
30
+ }
31
+ }
32
+ /**
33
+ * Check if tracing is enabled
34
+ */
35
+ export function isTracingEnabled() {
36
+ return !!process.env.BRAINTRUST_API_KEY;
37
+ }
38
+ /**
39
+ * Get wrapped AI SDK functions for automatic tracing
40
+ * Falls back to original functions if tracing not available
41
+ */
42
+ export function getTracedAI() {
43
+ initTracing();
44
+ if (isTracingEnabled()) {
45
+ return wrapAISDK(ai);
46
+ }
47
+ // Return original AI SDK functions if tracing not enabled
48
+ return ai;
49
+ }
50
+ /**
51
+ * Wrap a function with tracing
52
+ * No-op if tracing is not enabled
53
+ */
54
+ export function maybeTraced(fn, options = {}) {
55
+ if (!isTracingEnabled()) {
56
+ return fn;
57
+ }
58
+ const { wrapTraced } = require('braintrust');
59
+ return wrapTraced(fn, {
60
+ type: options.type || 'function',
61
+ name: options.name || fn.name || 'anonymous',
62
+ });
63
+ }
64
+ //# sourceMappingURL=tracing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.js","sourceRoot":"","sources":["../../src/utils/tracing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,6BAA6B;AAC7B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7D;;GAEG;AACH,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAE9B,MAAM,UAAU,WAAW;IACzB,IAAI,iBAAiB;QAAE,OAAO;IAE9B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,sCAAsC;QACtC,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,UAAU,CAAC;YACT,MAAM;YACN,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,YAAY;SACjE,CAAC,CAAC;QACH,iBAAiB,GAAG,IAAI,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,WAAW,EAAE,CAAC;IAEd,IAAI,gBAAgB,EAAE,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC;IAED,0DAA0D;IAC1D,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,EAAK,EACL,UAA4C,EAAE;IAE9C,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7C,OAAO,UAAU,CAAC,EAAE,EAAE;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,UAAU;QAChC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,WAAW;KAC7C,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiggum-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "AI-powered feature development loop CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,8 +28,10 @@
28
28
  "dependencies": {
29
29
  "@ai-sdk/anthropic": "^3.0.15",
30
30
  "@ai-sdk/openai": "^3.0.12",
31
+ "@braintrust/otel": "^0.2.0",
31
32
  "@clack/prompts": "^0.7.0",
32
33
  "ai": "^6.0.41",
34
+ "braintrust": "^2.0.2",
33
35
  "cfonts": "^3.2.0",
34
36
  "commander": "^12.1.0",
35
37
  "picocolors": "^1.0.0",
@@ -3,11 +3,13 @@
3
3
  * Explores the codebase to understand its structure and patterns
4
4
  */
5
5
 
6
- import { generateText, stepCountIs, type LanguageModel } from 'ai';
6
+ import { stepCountIs, type LanguageModel } from 'ai';
7
7
  import type { CodebaseAnalysis, CodebaseAnalystInput } from './types.js';
8
8
  import { createExplorationTools } from '../tools.js';
9
9
  import { isReasoningModel } from '../providers.js';
10
10
  import { logger } from '../../utils/logger.js';
11
+ import { parseJsonSafe } from '../../utils/json-repair.js';
12
+ import { getTracedAI } from '../../utils/tracing.js';
11
13
 
12
14
  /**
13
15
  * System prompt for the Codebase Analyst agent
@@ -82,6 +84,8 @@ Project: ${input.projectRoot}
82
84
  Start by exploring the directory structure and package.json, then produce your analysis as JSON.`;
83
85
 
84
86
  try {
87
+ const { generateText } = getTracedAI();
88
+
85
89
  const result = await generateText({
86
90
  model,
87
91
  system: CODEBASE_ANALYST_SYSTEM_PROMPT,
@@ -90,6 +94,10 @@ Start by exploring the directory structure and package.json, then produce your a
90
94
  stopWhen: stepCountIs(12),
91
95
  maxOutputTokens: 3000,
92
96
  ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
97
+ experimental_telemetry: {
98
+ isEnabled: true,
99
+ metadata: { agent: 'codebase-analyst', projectRoot: input.projectRoot },
100
+ },
93
101
  });
94
102
 
95
103
  // Extract JSON from response
@@ -133,40 +141,29 @@ function parseCodebaseAnalysis(
133
141
  return null;
134
142
  }
135
143
 
136
- try {
137
- // Remove markdown code blocks if present
138
- let jsonText = textToParse;
139
- const jsonMatch = textToParse.match(/```(?:json)?\s*([\s\S]*?)```/);
140
- if (jsonMatch) {
141
- jsonText = jsonMatch[1];
142
- }
143
-
144
- // Find JSON object
145
- const objectMatch = jsonText.match(/\{[\s\S]*\}/);
146
- if (objectMatch) {
147
- jsonText = objectMatch[0];
148
- }
149
-
150
- const parsed = JSON.parse(jsonText) as CodebaseAnalysis;
151
-
152
- // Validate required fields
153
- if (!parsed.projectContext || !parsed.commands) {
154
- if (verbose) {
155
- logger.warn('Codebase Analyst: Missing required fields in response');
156
- }
157
- return null;
158
- }
144
+ // Use safe JSON parser with repair capabilities
145
+ const parsed = parseJsonSafe<CodebaseAnalysis>(textToParse);
159
146
 
160
- // Ensure projectType is set
161
- if (!parsed.projectContext.projectType) {
162
- parsed.projectContext.projectType = 'Unknown';
147
+ if (!parsed) {
148
+ if (verbose) {
149
+ logger.warn('Codebase Analyst: Failed to parse JSON response');
150
+ logger.warn(`Response preview: ${textToParse.substring(0, 200)}...`);
163
151
  }
152
+ return null;
153
+ }
164
154
 
165
- return parsed;
166
- } catch (error) {
155
+ // Validate required fields
156
+ if (!parsed.projectContext || !parsed.commands) {
167
157
  if (verbose) {
168
- logger.warn(`Codebase Analyst: Failed to parse JSON - ${error instanceof Error ? error.message : String(error)}`);
158
+ logger.warn('Codebase Analyst: Missing required fields in response');
169
159
  }
170
160
  return null;
171
161
  }
162
+
163
+ // Ensure projectType is set
164
+ if (!parsed.projectContext.projectType) {
165
+ parsed.projectContext.projectType = 'Unknown';
166
+ }
167
+
168
+ return parsed;
172
169
  }
@@ -3,7 +3,7 @@
3
3
  * Coordinates the multi-agent analysis and merges results
4
4
  */
5
5
 
6
- import { generateText, type LanguageModel } from 'ai';
6
+ import type { LanguageModel } from 'ai';
7
7
  import type {
8
8
  CodebaseAnalysis,
9
9
  StackResearch,
@@ -14,6 +14,8 @@ import type {
14
14
  import type { DetectedStack } from '../../scanner/types.js';
15
15
  import { isReasoningModel } from '../providers.js';
16
16
  import { logger } from '../../utils/logger.js';
17
+ import { parseJsonSafe } from '../../utils/json-repair.js';
18
+ import { getTracedAI } from '../../utils/tracing.js';
17
19
 
18
20
  /**
19
21
  * System prompt for the Orchestrator
@@ -67,12 +69,18 @@ export async function runOrchestrator(
67
69
  const prompt = createOrchestratorPrompt(input);
68
70
 
69
71
  try {
72
+ const { generateText } = getTracedAI();
73
+
70
74
  const result = await generateText({
71
75
  model,
72
76
  system: ORCHESTRATOR_SYSTEM_PROMPT,
73
77
  prompt,
74
78
  maxOutputTokens: 1000,
75
79
  ...(isReasoningModel(modelId) ? {} : { temperature: 0.2 }),
80
+ experimental_telemetry: {
81
+ isEnabled: true,
82
+ metadata: { agent: 'orchestrator', projectType: input.codebaseAnalysis.projectContext.projectType },
83
+ },
76
84
  });
77
85
 
78
86
  const mcpServers = parseMcpRecommendations(result.text, input.stack, verbose);
@@ -130,36 +138,24 @@ function parseMcpRecommendations(
130
138
  return getDefaultMcpRecommendations('Unknown', stack);
131
139
  }
132
140
 
133
- try {
134
- // Remove markdown code blocks
135
- let jsonText = text;
136
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
137
- if (jsonMatch) {
138
- jsonText = jsonMatch[1];
139
- }
141
+ // Use safe JSON parser with repair capabilities
142
+ const parsed = parseJsonSafe<{ mcpServers?: McpRecommendations }>(text);
140
143
 
141
- // Find JSON object
142
- const objectMatch = jsonText.match(/\{[\s\S]*\}/);
143
- if (objectMatch) {
144
- jsonText = objectMatch[0];
145
- }
146
-
147
- const parsed = JSON.parse(jsonText) as { mcpServers?: McpRecommendations };
148
-
149
- if (parsed.mcpServers) {
150
- return {
151
- essential: parsed.mcpServers.essential || ['filesystem', 'git'],
152
- recommended: parsed.mcpServers.recommended || [],
153
- };
154
- }
155
-
156
- return getDefaultMcpRecommendations('Unknown', stack);
157
- } catch (error) {
144
+ if (!parsed) {
158
145
  if (verbose) {
159
- logger.warn(`Orchestrator: Failed to parse JSON - ${error instanceof Error ? error.message : String(error)}`);
146
+ logger.warn('Orchestrator: Failed to parse JSON response');
160
147
  }
161
148
  return getDefaultMcpRecommendations('Unknown', stack);
162
149
  }
150
+
151
+ if (parsed.mcpServers) {
152
+ return {
153
+ essential: parsed.mcpServers.essential || ['filesystem', 'git'],
154
+ recommended: parsed.mcpServers.recommended || [],
155
+ };
156
+ }
157
+
158
+ return getDefaultMcpRecommendations('Unknown', stack);
163
159
  }
164
160
 
165
161
  /**
@@ -4,13 +4,15 @@
4
4
  * Gracefully degrades when optional services are unavailable
5
5
  */
6
6
 
7
- import { generateText, stepCountIs, type LanguageModel, type Tool } from 'ai';
7
+ import { stepCountIs, type LanguageModel, type Tool } from 'ai';
8
8
  import type { StackResearch, StackResearcherInput, AgentCapabilities } from './types.js';
9
9
  import type { DetectedStack } from '../../scanner/types.js';
10
10
  import { createTavilySearchTool } from '../tools/tavily.js';
11
11
  import { createContext7Tool } from '../tools/context7.js';
12
12
  import { isReasoningModel } from '../providers.js';
13
13
  import { logger } from '../../utils/logger.js';
14
+ import { parseJsonSafe } from '../../utils/json-repair.js';
15
+ import { getTracedAI } from '../../utils/tracing.js';
14
16
 
15
17
  /**
16
18
  * System prompt for Stack Researcher with tools
@@ -200,6 +202,8 @@ export async function runStackResearcher(
200
202
  const prompt = createResearchPrompt(input.stack, input.projectType, hasTools);
201
203
 
202
204
  try {
205
+ const { generateText } = getTracedAI();
206
+
203
207
  const result = await generateText({
204
208
  model,
205
209
  system: systemPrompt,
@@ -207,6 +211,10 @@ export async function runStackResearcher(
207
211
  ...(hasTools ? { tools, stopWhen: stepCountIs(8) } : {}),
208
212
  maxOutputTokens: 2000,
209
213
  ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
214
+ experimental_telemetry: {
215
+ isEnabled: true,
216
+ metadata: { agent: 'stack-researcher', researchMode, projectType: input.projectType },
217
+ },
210
218
  });
211
219
 
212
220
  // Parse the response
@@ -250,37 +258,25 @@ function parseStackResearch(
250
258
  return getDefaultStackResearch(researchMode);
251
259
  }
252
260
 
253
- try {
254
- // Remove markdown code blocks if present
255
- let jsonText = textToParse;
256
- const jsonMatch = textToParse.match(/```(?:json)?\s*([\s\S]*?)```/);
257
- if (jsonMatch) {
258
- jsonText = jsonMatch[1];
259
- }
260
-
261
- // Find JSON object
262
- const objectMatch = jsonText.match(/\{[\s\S]*\}/);
263
- if (objectMatch) {
264
- jsonText = objectMatch[0];
265
- }
261
+ // Use safe JSON parser with repair capabilities
262
+ const parsed = parseJsonSafe<Partial<StackResearch>>(textToParse);
266
263
 
267
- const parsed = JSON.parse(jsonText) as Partial<StackResearch>;
268
-
269
- // Build result with defaults for missing fields
270
- return {
271
- bestPractices: parsed.bestPractices || [],
272
- antiPatterns: parsed.antiPatterns || [],
273
- testingTools: parsed.testingTools || [],
274
- debuggingTools: parsed.debuggingTools || [],
275
- documentationHints: parsed.documentationHints || [],
276
- researchMode: researchMode,
277
- };
278
- } catch (error) {
264
+ if (!parsed) {
279
265
  if (verbose) {
280
- logger.warn(`Stack Researcher: Failed to parse JSON - ${error instanceof Error ? error.message : String(error)}`);
266
+ logger.warn('Stack Researcher: Failed to parse JSON response');
281
267
  }
282
268
  return getDefaultStackResearch(researchMode);
283
269
  }
270
+
271
+ // Build result with defaults for missing fields
272
+ return {
273
+ bestPractices: parsed.bestPractices || [],
274
+ antiPatterns: parsed.antiPatterns || [],
275
+ testingTools: parsed.testingTools || [],
276
+ debuggingTools: parsed.debuggingTools || [],
277
+ documentationHints: parsed.documentationHints || [],
278
+ researchMode: researchMode,
279
+ };
284
280
  }
285
281
 
286
282
  /**
@@ -3,13 +3,15 @@
3
3
  * Uses AI to analyze the codebase for deeper insights
4
4
  */
5
5
 
6
- import { generateText, stepCountIs } from 'ai';
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
- try {
131
- // Try to extract JSON from the response
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
- // Remove markdown code blocks if present
136
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
137
- if (jsonMatch) {
138
- jsonText = jsonMatch[1];
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
- return result;
160
- } catch (error) {
143
+ // Validate that we got the expected structure
144
+ if (typeof result !== 'object') {
161
145
  if (verbose) {
162
- logger.warn(`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : String(error)}`);
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,113 @@
1
+ /**
2
+ * JSON Repair Utility
3
+ * Fixes common JSON syntax errors from AI responses
4
+ */
5
+
6
+ /**
7
+ * Attempt to repair malformed JSON from AI responses
8
+ */
9
+ export function repairJson(text: string): string {
10
+ let json = text;
11
+
12
+ // Remove any leading/trailing whitespace
13
+ json = json.trim();
14
+
15
+ // Remove trailing commas before ] or }
16
+ json = json.replace(/,(\s*[\]}])/g, '$1');
17
+
18
+ // Fix missing commas between array elements or object properties
19
+ // Pattern: value followed by newline and another value without comma
20
+ json = json.replace(/("|\d|true|false|null|\]|\})(\s*\n\s*)("|\[|\{)/g, '$1,$2$3');
21
+
22
+ // Fix single quotes to double quotes (but not inside strings)
23
+ // This is a simplified approach - may not work for all cases
24
+ json = json.replace(/'/g, '"');
25
+
26
+ // Remove JavaScript-style comments
27
+ json = json.replace(/\/\/.*$/gm, '');
28
+ json = json.replace(/\/\*[\s\S]*?\*\//g, '');
29
+
30
+ // Fix unquoted keys (simple cases)
31
+ json = json.replace(/(\{|\,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
32
+
33
+ // Remove any text before the first { or [
34
+ const firstBrace = json.indexOf('{');
35
+ const firstBracket = json.indexOf('[');
36
+ let startIndex = -1;
37
+
38
+ if (firstBrace !== -1 && firstBracket !== -1) {
39
+ startIndex = Math.min(firstBrace, firstBracket);
40
+ } else if (firstBrace !== -1) {
41
+ startIndex = firstBrace;
42
+ } else if (firstBracket !== -1) {
43
+ startIndex = firstBracket;
44
+ }
45
+
46
+ if (startIndex > 0) {
47
+ json = json.substring(startIndex);
48
+ }
49
+
50
+ // Remove any text after the last } or ]
51
+ const lastBrace = json.lastIndexOf('}');
52
+ const lastBracket = json.lastIndexOf(']');
53
+ let endIndex = -1;
54
+
55
+ if (lastBrace !== -1 && lastBracket !== -1) {
56
+ endIndex = Math.max(lastBrace, lastBracket);
57
+ } else if (lastBrace !== -1) {
58
+ endIndex = lastBrace;
59
+ } else if (lastBracket !== -1) {
60
+ endIndex = lastBracket;
61
+ }
62
+
63
+ if (endIndex !== -1 && endIndex < json.length - 1) {
64
+ json = json.substring(0, endIndex + 1);
65
+ }
66
+
67
+ return json;
68
+ }
69
+
70
+ /**
71
+ * Parse JSON with repair attempts
72
+ * Tries to fix common issues before parsing
73
+ */
74
+ export function parseJsonSafe<T>(text: string): T | null {
75
+ // First try parsing as-is
76
+ try {
77
+ return JSON.parse(text) as T;
78
+ } catch {
79
+ // Try with repairs
80
+ }
81
+
82
+ // Try repairing the JSON
83
+ try {
84
+ const repaired = repairJson(text);
85
+ return JSON.parse(repaired) as T;
86
+ } catch {
87
+ // Repair failed
88
+ }
89
+
90
+ // Last resort: try to extract JSON from markdown code blocks
91
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
92
+ if (jsonMatch) {
93
+ try {
94
+ const repaired = repairJson(jsonMatch[1]);
95
+ return JSON.parse(repaired) as T;
96
+ } catch {
97
+ // Still failed
98
+ }
99
+ }
100
+
101
+ // Try finding a JSON object
102
+ const objectMatch = text.match(/\{[\s\S]*\}/);
103
+ if (objectMatch) {
104
+ try {
105
+ const repaired = repairJson(objectMatch[0]);
106
+ return JSON.parse(repaired) as T;
107
+ } catch {
108
+ // Still failed
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }