wiggum-cli 0.4.9 → 0.5.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.
Files changed (40) hide show
  1. package/dist/ai/agents/codebase-analyzer.d.ts +24 -0
  2. package/dist/ai/agents/codebase-analyzer.d.ts.map +1 -0
  3. package/dist/ai/agents/codebase-analyzer.js +375 -0
  4. package/dist/ai/agents/codebase-analyzer.js.map +1 -0
  5. package/dist/ai/agents/context-enricher.d.ts +4 -0
  6. package/dist/ai/agents/context-enricher.d.ts.map +1 -1
  7. package/dist/ai/agents/context-enricher.js +4 -0
  8. package/dist/ai/agents/context-enricher.js.map +1 -1
  9. package/dist/ai/agents/evaluator-optimizer.d.ts +4 -0
  10. package/dist/ai/agents/evaluator-optimizer.d.ts.map +1 -1
  11. package/dist/ai/agents/evaluator-optimizer.js +4 -0
  12. package/dist/ai/agents/evaluator-optimizer.js.map +1 -1
  13. package/dist/ai/agents/index.d.ts +20 -14
  14. package/dist/ai/agents/index.d.ts.map +1 -1
  15. package/dist/ai/agents/index.js +38 -107
  16. package/dist/ai/agents/index.js.map +1 -1
  17. package/dist/ai/agents/planning-orchestrator.d.ts +4 -0
  18. package/dist/ai/agents/planning-orchestrator.d.ts.map +1 -1
  19. package/dist/ai/agents/planning-orchestrator.js +4 -0
  20. package/dist/ai/agents/planning-orchestrator.js.map +1 -1
  21. package/dist/ai/agents/synthesis-agent.d.ts +4 -0
  22. package/dist/ai/agents/synthesis-agent.d.ts.map +1 -1
  23. package/dist/ai/agents/synthesis-agent.js +4 -0
  24. package/dist/ai/agents/synthesis-agent.js.map +1 -1
  25. package/dist/ai/agents/tech-researcher.d.ts +4 -0
  26. package/dist/ai/agents/tech-researcher.d.ts.map +1 -1
  27. package/dist/ai/agents/tech-researcher.js +4 -0
  28. package/dist/ai/agents/tech-researcher.js.map +1 -1
  29. package/dist/ai/agents/types.d.ts +8 -0
  30. package/dist/ai/agents/types.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/ai/agents/codebase-analyzer.test.ts +115 -0
  33. package/src/ai/agents/codebase-analyzer.ts +450 -0
  34. package/src/ai/agents/context-enricher.ts +4 -0
  35. package/src/ai/agents/evaluator-optimizer.ts +4 -0
  36. package/src/ai/agents/index.ts +45 -146
  37. package/src/ai/agents/planning-orchestrator.ts +4 -0
  38. package/src/ai/agents/synthesis-agent.ts +4 -0
  39. package/src/ai/agents/tech-researcher.ts +4 -0
  40. package/src/ai/agents/types.ts +9 -0
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Unified Codebase Analyzer (v0.5.0)
3
+ * Single agent that explores codebase and produces full analysis
4
+ * Replaces: planning-orchestrator + context-enricher + tech-researchers + synthesis-agent
5
+ */
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { stepCountIs, type LanguageModel } from 'ai';
10
+ import type { ScanResult } from '../../scanner/types.js';
11
+ import type { MultiAgentAnalysis, CodebaseAnalysis, StackResearch, RalphMcpServers } from './types.js';
12
+ import { createExplorationTools } from '../tools.js';
13
+ import { isReasoningModel } from '../providers.js';
14
+ import { logger } from '../../utils/logger.js';
15
+ import { parseJsonSafe } from '../../utils/json-repair.js';
16
+ import { getTracedAI } from '../../utils/tracing.js';
17
+ import { detectProjectType } from './stack-utils.js';
18
+ import { detectRalphMcpServers, convertToLegacyMcpRecommendations } from './mcp-detector.js';
19
+ import { deriveCommandsFromScripts } from './context-enricher.js';
20
+
21
+ /**
22
+ * Input for the Codebase Analyzer
23
+ */
24
+ export interface CodebaseAnalyzerInput {
25
+ scanResult: ScanResult;
26
+ }
27
+
28
+ /**
29
+ * Internal schema for analyzer output (before conversion to MultiAgentAnalysis)
30
+ */
31
+ interface AnalyzerOutput {
32
+ entryPoints: string[];
33
+ keyDirectories: Record<string, string>;
34
+ projectType: string;
35
+ namingConventions: string;
36
+ architectureFlow: string;
37
+ implementationGuidelines: string[];
38
+ technologyNotes: {
39
+ testingApproach: string;
40
+ buildSystem: string;
41
+ keyPatterns: string[];
42
+ };
43
+ }
44
+
45
+ /**
46
+ * System prompt for the unified Codebase Analyzer
47
+ */
48
+ const CODEBASE_ANALYZER_SYSTEM_PROMPT = `You are a Codebase Analyzer. Your job is to explore a codebase and produce a complete analysis for AI-assisted development.
49
+
50
+ ## Your Mission
51
+ Explore the codebase with tools to discover:
52
+ 1. Entry points (actual file paths from package.json bin/main/module)
53
+ 2. Key directories and their purposes
54
+ 3. Project type (CLI, MCP Server, Web App, Library, API)
55
+ 4. Implementation patterns actually used in the code
56
+ 5. Architecture flow (how data/control flows through the system)
57
+
58
+ ## CRITICAL: Tool Usage Limits
59
+ - You have a MAXIMUM of 8 tool calls before you MUST output JSON
60
+ - Call getPackageInfo (no field) FIRST to get bin, main, scripts
61
+ - Call listDirectory on root to see actual structure
62
+ - Explore 1-2 key directories if needed
63
+ - After 5-6 tool calls, STOP exploring and OUTPUT your JSON
64
+
65
+ ## Tools Available
66
+ - getPackageInfo: Get package.json info - call with NO field parameter first
67
+ - listDirectory: List directory structure
68
+ - readFile: Read file contents (use sparingly)
69
+ - searchCode: Search using ripgrep patterns (use sparingly)
70
+
71
+ ## Project Types & Entry Point Patterns
72
+ - MCP Server: Has @modelcontextprotocol deps → main field or src/index.ts
73
+ - CLI Tool: Has bin entry in package.json → use bin paths directly
74
+ - Next.js App: next in deps → app/page.tsx or pages/index.tsx
75
+ - REST API: Express/Fastify/Hono → main field, app.ts, or server.ts
76
+ - Library: main/module fields → use those paths
77
+
78
+ ## Output Requirements
79
+ - entryPoints: ONLY actual file paths discovered (never instructions)
80
+ - Priority: bin > main > module > framework conventions
81
+ - If nothing found, output empty array []
82
+ - keyDirectories: ONLY directories that actually exist with purposes
83
+ - implementationGuidelines: Describe DISCOVERED patterns (5-10 words each)
84
+ - Format: "[Pattern/Tool] for [purpose]"
85
+ - Examples: "Vitest configured for unit testing", "Zod schemas in src/schemas"
86
+ - NOT instructions like "Run npm test"
87
+ - architectureFlow: Describe data/control flow
88
+ - CLI: "CLI entry → command parser → handlers → output"
89
+ - MCP: "Transport → request router → tool handlers → response"
90
+ - API: "HTTP server → middleware → routes → handlers → data"
91
+
92
+ ## Output Format
93
+ Output ONLY valid JSON:
94
+ {
95
+ "entryPoints": ["bin/cli.js", "src/index.ts"],
96
+ "keyDirectories": {
97
+ "src/commands": "CLI command implementations",
98
+ "src/handlers": "Request handlers"
99
+ },
100
+ "projectType": "CLI Tool",
101
+ "namingConventions": "camelCase files, PascalCase components",
102
+ "architectureFlow": "CLI parses args → routes to commands → handlers process → output formatted",
103
+ "implementationGuidelines": [
104
+ "Vitest configured for unit testing (npm test)",
105
+ "TypeScript strict mode enabled",
106
+ "Commander for CLI argument parsing",
107
+ "Zod for input validation"
108
+ ],
109
+ "technologyNotes": {
110
+ "testingApproach": "Vitest with coverage",
111
+ "buildSystem": "tsup for bundling",
112
+ "keyPatterns": ["Command pattern for CLI", "Dependency injection"]
113
+ }
114
+ }`;
115
+
116
+ /**
117
+ * Run the unified Codebase Analyzer
118
+ */
119
+ export async function runCodebaseAnalyzer(
120
+ model: LanguageModel,
121
+ modelId: string,
122
+ input: CodebaseAnalyzerInput,
123
+ verbose: boolean = false
124
+ ): Promise<MultiAgentAnalysis> {
125
+ const tools = createExplorationTools(input.scanResult.projectRoot);
126
+ const stack = input.scanResult.stack;
127
+
128
+ // Build technology summary for context
129
+ const technologies: string[] = [];
130
+ if (stack.framework) technologies.push(stack.framework.name);
131
+ if (stack.database) technologies.push(stack.database.name);
132
+ if (stack.orm) technologies.push(stack.orm.name);
133
+ if (stack.testing?.unit) technologies.push(stack.testing.unit.name);
134
+ if (stack.testing?.e2e) technologies.push(stack.testing.e2e.name);
135
+ if (stack.mcp?.isProject) technologies.push('MCP Server');
136
+
137
+ const prompt = `Analyze this codebase and produce a complete analysis.
138
+
139
+ Project: ${input.scanResult.projectRoot}
140
+
141
+ ## Already Detected (from scanner)
142
+ Framework: ${stack.framework?.name || 'Unknown'}
143
+ Database: ${stack.database?.name || 'None detected'}
144
+ Testing: ${stack.testing?.unit?.name || 'None detected'}
145
+ Package Manager: ${stack.packageManager?.name || 'npm'}
146
+ Technologies: ${technologies.join(', ') || 'None'}
147
+ ${stack.mcp?.isProject ? 'This is an MCP Server project.' : ''}
148
+
149
+ Start by calling getPackageInfo() to discover entry points, then explore the structure.
150
+ After exploring, output your complete analysis as JSON.`;
151
+
152
+ try {
153
+ const { generateText } = getTracedAI();
154
+
155
+ const MAX_TOOL_CALLS = 10;
156
+
157
+ const result = await generateText({
158
+ model,
159
+ system: CODEBASE_ANALYZER_SYSTEM_PROMPT,
160
+ prompt,
161
+ tools,
162
+ stopWhen: stepCountIs(12), // Allow steps for tool calls + JSON output
163
+ maxOutputTokens: 4000,
164
+ prepareStep: ({ steps }) => {
165
+ const totalToolCalls = steps.reduce(
166
+ (sum, step) => sum + (step.toolCalls?.length || 0),
167
+ 0
168
+ );
169
+
170
+ if (totalToolCalls >= MAX_TOOL_CALLS) {
171
+ if (verbose) {
172
+ logger.info(`Codebase Analyzer: ${totalToolCalls} tool calls, disabling tools`);
173
+ }
174
+ return { activeTools: [], toolChoice: 'none' as const };
175
+ }
176
+
177
+ return {};
178
+ },
179
+ ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
180
+ experimental_telemetry: {
181
+ isEnabled: true,
182
+ metadata: {
183
+ agent: 'codebase-analyzer',
184
+ projectRoot: input.scanResult.projectRoot,
185
+ framework: stack.framework?.name || 'unknown',
186
+ },
187
+ },
188
+ });
189
+
190
+ // Parse the response
191
+ const analyzerOutput = parseAnalyzerOutput(result.text, result.steps, verbose, input);
192
+
193
+ // Detect MCP servers (pure function)
194
+ const mcpServers = detectRalphMcpServers(stack, analyzerOutput.projectType);
195
+
196
+ // Build full MultiAgentAnalysis
197
+ return buildMultiAgentAnalysis(analyzerOutput, mcpServers, input);
198
+ } catch (error) {
199
+ if (verbose) {
200
+ logger.error(`Codebase Analyzer error: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+
203
+ // Return default analysis with derived data from package.json
204
+ return getDefaultMultiAgentAnalysis(input.scanResult);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Parse analyzer output from response text
210
+ */
211
+ function parseAnalyzerOutput(
212
+ text: string,
213
+ steps: Array<{ text?: string }> | undefined,
214
+ verbose: boolean,
215
+ input: CodebaseAnalyzerInput
216
+ ): AnalyzerOutput {
217
+ let textToParse = text;
218
+
219
+ // Try to get text from steps if main text is empty
220
+ if (!textToParse || textToParse.trim() === '') {
221
+ const stepsList = steps || [];
222
+ for (let i = stepsList.length - 1; i >= 0; i--) {
223
+ const step = stepsList[i];
224
+ if (step.text && step.text.trim() !== '') {
225
+ textToParse = step.text;
226
+ break;
227
+ }
228
+ }
229
+ }
230
+
231
+ if (!textToParse || textToParse.trim() === '') {
232
+ if (verbose) {
233
+ logger.warn('Codebase Analyzer: No text output found');
234
+ }
235
+ return getDefaultAnalyzerOutput(input);
236
+ }
237
+
238
+ const parsed = parseJsonSafe<Partial<AnalyzerOutput>>(textToParse);
239
+
240
+ if (!parsed) {
241
+ if (verbose) {
242
+ logger.warn('Codebase Analyzer: Failed to parse JSON response');
243
+ }
244
+ return getDefaultAnalyzerOutput(input);
245
+ }
246
+
247
+ // Derive commands and entry points from package.json as fallback
248
+ const derivedCommands = deriveCommandsFromScripts(input.scanResult.projectRoot);
249
+ const derivedEntryPoints = deriveEntryPointsFromPackageJson(input.scanResult.projectRoot);
250
+
251
+ return {
252
+ entryPoints: parsed.entryPoints?.length ? parsed.entryPoints : derivedEntryPoints,
253
+ keyDirectories: parsed.keyDirectories || {},
254
+ projectType: parsed.projectType || detectProjectType(input.scanResult.stack),
255
+ namingConventions: parsed.namingConventions || 'unknown',
256
+ architectureFlow: parsed.architectureFlow || '',
257
+ implementationGuidelines: parsed.implementationGuidelines || [],
258
+ // Normalize technologyNotes to ensure all fields have defaults (handles partial objects)
259
+ technologyNotes: {
260
+ testingApproach: parsed.technologyNotes?.testingApproach || derivedCommands.test || 'npm test',
261
+ buildSystem: parsed.technologyNotes?.buildSystem || derivedCommands.build || 'npm run build',
262
+ keyPatterns: Array.isArray(parsed.technologyNotes?.keyPatterns) ? parsed.technologyNotes.keyPatterns : [],
263
+ },
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Build MultiAgentAnalysis from analyzer output
269
+ */
270
+ function buildMultiAgentAnalysis(
271
+ output: AnalyzerOutput,
272
+ mcpServers: RalphMcpServers,
273
+ input: CodebaseAnalyzerInput
274
+ ): MultiAgentAnalysis {
275
+ const derivedCommands = deriveCommandsFromScripts(input.scanResult.projectRoot);
276
+
277
+ const codebaseAnalysis: CodebaseAnalysis = {
278
+ projectContext: {
279
+ entryPoints: output.entryPoints,
280
+ keyDirectories: output.keyDirectories,
281
+ namingConventions: output.namingConventions,
282
+ projectType: output.projectType,
283
+ },
284
+ commands: {
285
+ test: derivedCommands.test,
286
+ lint: derivedCommands.lint,
287
+ typecheck: derivedCommands.typecheck,
288
+ build: derivedCommands.build,
289
+ dev: derivedCommands.dev,
290
+ format: derivedCommands.format,
291
+ },
292
+ implementationGuidelines: output.implementationGuidelines.length > 0
293
+ ? output.implementationGuidelines
294
+ : getDefaultGuidelines(output, derivedCommands),
295
+ possibleMissedTechnologies: [],
296
+ };
297
+
298
+ // Add architecture flow to guidelines if present
299
+ if (output.architectureFlow && !codebaseAnalysis.implementationGuidelines.some(g => g.includes('→'))) {
300
+ codebaseAnalysis.implementationGuidelines.unshift(`Architecture: ${output.architectureFlow}`);
301
+ }
302
+
303
+ const stackResearch: StackResearch = {
304
+ bestPractices: output.technologyNotes.keyPatterns.length > 0
305
+ ? output.technologyNotes.keyPatterns
306
+ : ['Follow project conventions'],
307
+ antiPatterns: ['Avoid skipping tests'],
308
+ testingTools: [output.technologyNotes.testingApproach || derivedCommands.test || 'npm test'],
309
+ debuggingTools: ['console.log', 'debugger'],
310
+ validationTools: [derivedCommands.lint || 'npm run lint', derivedCommands.typecheck || 'npx tsc --noEmit'].filter(Boolean),
311
+ documentationHints: ['Check official docs'],
312
+ researchMode: 'knowledge-only',
313
+ };
314
+
315
+ return {
316
+ codebaseAnalysis,
317
+ stackResearch,
318
+ mcpServers: convertToLegacyMcpRecommendations(mcpServers),
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Derive entry points from package.json
324
+ * Exported for testing
325
+ */
326
+ export function deriveEntryPointsFromPackageJson(projectRoot: string): string[] {
327
+ const entryPoints: string[] = [];
328
+ const packageJsonPath = join(projectRoot, 'package.json');
329
+
330
+ if (!existsSync(packageJsonPath)) {
331
+ return entryPoints;
332
+ }
333
+
334
+ try {
335
+ const content = readFileSync(packageJsonPath, 'utf-8');
336
+ const pkg = JSON.parse(content) as Record<string, unknown>;
337
+
338
+ // CLI tools: use bin field
339
+ if (pkg.bin) {
340
+ if (typeof pkg.bin === 'string') {
341
+ entryPoints.push(pkg.bin);
342
+ } else if (typeof pkg.bin === 'object' && pkg.bin !== null) {
343
+ entryPoints.push(...Object.values(pkg.bin as Record<string, string>));
344
+ }
345
+ }
346
+
347
+ // Libraries: use main/module fields
348
+ if (typeof pkg.main === 'string') {
349
+ entryPoints.push(pkg.main);
350
+ }
351
+ if (typeof pkg.module === 'string') {
352
+ entryPoints.push(pkg.module);
353
+ }
354
+
355
+ // Filter out compiled output (dist/) and dedupe
356
+ return [...new Set(entryPoints.filter(ep => ep && !ep.startsWith('dist/')))];
357
+ } catch {
358
+ return entryPoints;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Get default analyzer output when parsing fails
364
+ */
365
+ function getDefaultAnalyzerOutput(input: CodebaseAnalyzerInput): AnalyzerOutput {
366
+ const projectType = detectProjectType(input.scanResult.stack);
367
+ const entryPoints = deriveEntryPointsFromPackageJson(input.scanResult.projectRoot);
368
+ const commands = deriveCommandsFromScripts(input.scanResult.projectRoot);
369
+
370
+ return {
371
+ entryPoints,
372
+ keyDirectories: {},
373
+ projectType,
374
+ namingConventions: 'unknown',
375
+ architectureFlow: '',
376
+ implementationGuidelines: [],
377
+ technologyNotes: {
378
+ testingApproach: commands.test || 'npm test',
379
+ buildSystem: commands.build || 'npm run build',
380
+ keyPatterns: [],
381
+ },
382
+ };
383
+ }
384
+
385
+ /**
386
+ * Get default implementation guidelines
387
+ */
388
+ function getDefaultGuidelines(output: AnalyzerOutput, commands: Record<string, string>): string[] {
389
+ const guidelines: string[] = [];
390
+
391
+ if (commands.test) {
392
+ guidelines.push(`Testing with ${commands.test}`);
393
+ }
394
+
395
+ if (commands.build) {
396
+ guidelines.push(`Build using ${commands.build}`);
397
+ }
398
+
399
+ if (commands.lint) {
400
+ guidelines.push(`Linting with ${commands.lint}`);
401
+ }
402
+
403
+ guidelines.push('Follow existing code patterns');
404
+ guidelines.push('TypeScript strict mode recommended');
405
+
406
+ return guidelines.slice(0, 7);
407
+ }
408
+
409
+ /**
410
+ * Get default MultiAgentAnalysis when analyzer fails completely
411
+ */
412
+ function getDefaultMultiAgentAnalysis(scanResult: ScanResult): MultiAgentAnalysis {
413
+ const projectType = detectProjectType(scanResult.stack);
414
+ const entryPoints = deriveEntryPointsFromPackageJson(scanResult.projectRoot);
415
+ const commands = deriveCommandsFromScripts(scanResult.projectRoot);
416
+ const mcpServers = detectRalphMcpServers(scanResult.stack, projectType);
417
+
418
+ return {
419
+ codebaseAnalysis: {
420
+ projectContext: {
421
+ entryPoints: entryPoints.length > 0 ? entryPoints : ['src/index.ts'],
422
+ keyDirectories: { src: 'Source code' },
423
+ namingConventions: 'camelCase',
424
+ projectType,
425
+ },
426
+ commands: {
427
+ test: commands.test || 'npm test',
428
+ lint: commands.lint || 'npm run lint',
429
+ build: commands.build || 'npm run build',
430
+ dev: commands.dev || 'npm run dev',
431
+ },
432
+ implementationGuidelines: [
433
+ 'Follow existing patterns',
434
+ 'Run tests after changes',
435
+ 'Use TypeScript strict mode',
436
+ ],
437
+ possibleMissedTechnologies: [],
438
+ },
439
+ stackResearch: {
440
+ bestPractices: ['Follow project conventions'],
441
+ antiPatterns: ['Avoid skipping tests'],
442
+ testingTools: [commands.test || 'npm test'],
443
+ debuggingTools: ['console.log'],
444
+ validationTools: [commands.lint || 'npm run lint', 'npx tsc --noEmit'],
445
+ documentationHints: ['Check official docs'],
446
+ researchMode: 'knowledge-only',
447
+ },
448
+ mcpServers: convertToLegacyMcpRecommendations(mcpServers),
449
+ };
450
+ }
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Context Enricher Worker (Phase 2)
3
3
  * Explores the codebase to gather enriched context based on the analysis plan
4
+ *
5
+ * @deprecated v0.5.0 - No longer used in main pipeline.
6
+ * The unified codebase-analyzer.ts replaces this agent.
7
+ * Kept for backward compatibility and reference.
4
8
  */
5
9
 
6
10
  import { existsSync, readFileSync } from 'node:fs';
@@ -2,6 +2,10 @@
2
2
  * Evaluator-Optimizer Agent (Phase 4)
3
3
  * QA loop that validates and improves the analysis result
4
4
  * Max 2 iterations to ensure quality without endless loops
5
+ *
6
+ * @deprecated v0.5.0 - No longer used in main pipeline.
7
+ * Quality is now handled by fallback derivation from package.json.
8
+ * Kept for backward compatibility and reference.
5
9
  */
6
10
 
7
11
  import { type LanguageModel } from 'ai';