@limo-labs/limo-cli 0.1.0-alpha.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 (42) hide show
  1. package/README.md +238 -0
  2. package/dist/agents/analyst.d.ts +24 -0
  3. package/dist/agents/analyst.js +128 -0
  4. package/dist/agents/editor.d.ts +26 -0
  5. package/dist/agents/editor.js +157 -0
  6. package/dist/agents/planner-validator.d.ts +7 -0
  7. package/dist/agents/planner-validator.js +125 -0
  8. package/dist/agents/planner.d.ts +56 -0
  9. package/dist/agents/planner.js +186 -0
  10. package/dist/agents/writer.d.ts +25 -0
  11. package/dist/agents/writer.js +164 -0
  12. package/dist/commands/analyze.d.ts +14 -0
  13. package/dist/commands/analyze.js +562 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +41 -0
  16. package/dist/report/diagrams.d.ts +27 -0
  17. package/dist/report/diagrams.js +74 -0
  18. package/dist/report/graphCompiler.d.ts +37 -0
  19. package/dist/report/graphCompiler.js +277 -0
  20. package/dist/report/markdownGenerator.d.ts +71 -0
  21. package/dist/report/markdownGenerator.js +148 -0
  22. package/dist/tools/additional.d.ts +116 -0
  23. package/dist/tools/additional.js +349 -0
  24. package/dist/tools/extended.d.ts +101 -0
  25. package/dist/tools/extended.js +586 -0
  26. package/dist/tools/index.d.ts +86 -0
  27. package/dist/tools/index.js +362 -0
  28. package/dist/types/agents.types.d.ts +139 -0
  29. package/dist/types/agents.types.js +6 -0
  30. package/dist/types/graphSemantics.d.ts +99 -0
  31. package/dist/types/graphSemantics.js +104 -0
  32. package/dist/utils/debug.d.ts +28 -0
  33. package/dist/utils/debug.js +125 -0
  34. package/dist/utils/limoConfigParser.d.ts +21 -0
  35. package/dist/utils/limoConfigParser.js +274 -0
  36. package/dist/utils/reviewMonitor.d.ts +20 -0
  37. package/dist/utils/reviewMonitor.js +121 -0
  38. package/package.json +62 -0
  39. package/prompts/analyst.md +343 -0
  40. package/prompts/editor.md +196 -0
  41. package/prompts/planner.md +388 -0
  42. package/prompts/writer.md +218 -0
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Analyze command - Main entry point for codebase analysis
3
+ */
4
+ import * as path from 'path';
5
+ import * as fs from 'fs/promises';
6
+ import * as url from 'url';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import { runWorkflow, createSequenceNode, createStepNode, createEnhancedContext, InMemoryStore, InMemoryTrace, compileAgent } from '@limo-labs/deity';
10
+ import { CopilotSDKAdapter } from '@limo-labs/deity-adapter-copilot';
11
+ import { createPlannerAgent } from '../agents/planner.js';
12
+ import { createAnalystAgent } from '../agents/analyst.js';
13
+ import { createWriterAgent } from '../agents/writer.js';
14
+ import { createEditorAgent } from '../agents/editor.js';
15
+ import { createAllTools } from '../tools/index.js';
16
+ import { debug } from '../utils/debug.js';
17
+ import { parseLimoConfig } from '../utils/limoConfigParser.js';
18
+ // ES modules compatibility
19
+ const __filename = url.fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ export async function analyze(targetPath, options) {
22
+ // In debug or verbose mode, disable spinner to show full output
23
+ const useSpinner = !options.debug && !options.debugApi && !options.verbose;
24
+ const spinner = useSpinner ? ora('Initializing Limo CLI...').start() : null;
25
+ if (!useSpinner) {
26
+ console.log(chalk.blue('Initializing Limo CLI...'));
27
+ }
28
+ try {
29
+ debug.section('LIMO CLI INITIALIZATION');
30
+ // Resolve paths
31
+ const workspaceRoot = path.resolve(targetPath);
32
+ const outputDir = path.resolve(options.output || '.limo');
33
+ const promptsDir = path.join(__dirname, '../../prompts');
34
+ debug.debug('Workspace root:', workspaceRoot);
35
+ debug.debug('Output directory:', outputDir);
36
+ debug.debug('Prompts directory:', promptsDir);
37
+ // Verify workspace exists
38
+ try {
39
+ await fs.access(workspaceRoot);
40
+ debug.success('Workspace directory exists');
41
+ }
42
+ catch {
43
+ throw new Error(`Directory not found: ${workspaceRoot}`);
44
+ }
45
+ if (useSpinner) {
46
+ spinner.text = 'Verifying GitHub Copilot access...';
47
+ }
48
+ debug.debug('Checking GitHub Copilot access...');
49
+ // Parse LIMO.md configuration (if exists)
50
+ if (useSpinner) {
51
+ spinner.text = 'Checking for LIMO.md configuration...';
52
+ }
53
+ debug.debug('Parsing LIMO.md configuration...');
54
+ const limoConfig = await parseLimoConfig(workspaceRoot);
55
+ if (limoConfig) {
56
+ debug.success('Found LIMO.md configuration');
57
+ if (limoConfig.focusAreas) {
58
+ debug.info('Focus areas:', limoConfig.focusAreas);
59
+ }
60
+ if (limoConfig.excludeModules) {
61
+ debug.info('Excluded modules:', limoConfig.excludeModules);
62
+ }
63
+ if (limoConfig.scopeDescription) {
64
+ debug.info('Scope description:', limoConfig.scopeDescription.substring(0, 100) + '...');
65
+ }
66
+ }
67
+ else {
68
+ debug.info('No LIMO.md file found - using default configuration');
69
+ }
70
+ // Parse modules - Apply LIMO.md configuration
71
+ let modules = options.modules?.split(',') || [
72
+ 'architecture',
73
+ 'dependencies',
74
+ 'code-quality',
75
+ 'security',
76
+ 'database',
77
+ 'testing',
78
+ 'migration'
79
+ ];
80
+ // Apply LIMO.md focus areas (if specified)
81
+ if (limoConfig?.focusAreas && limoConfig.focusAreas.length > 0) {
82
+ modules = limoConfig.focusAreas;
83
+ debug.info('Using LIMO.md focus areas:', modules);
84
+ }
85
+ // Apply LIMO.md excludes (remove from modules)
86
+ if (limoConfig?.excludeModules && limoConfig.excludeModules.length > 0) {
87
+ modules = modules.filter(m => !limoConfig.excludeModules.includes(m));
88
+ debug.info('After excluding modules:', modules);
89
+ }
90
+ debug.debug('Final analysis modules:', modules);
91
+ if (useSpinner) {
92
+ spinner.text = 'Setting up analysis environment...';
93
+ }
94
+ // Create output directory
95
+ const analysisId = generateId();
96
+ const reportDir = path.join(outputDir, 'reports', analysisId);
97
+ await fs.mkdir(reportDir, { recursive: true });
98
+ debug.success('Report directory created:', reportDir);
99
+ // Create tool context
100
+ const toolContext = {
101
+ workspaceRoot,
102
+ outputDir: reportDir,
103
+ memory: new Map(), // Internal runtime state (not AI semantic memory)
104
+ reportSections: new Map(),
105
+ diagrams: new Map()
106
+ };
107
+ debug.debug('Tool context initialized');
108
+ // Create ExecutionContext with memory enabled for Deity's built-in memory tools
109
+ const executionContext = await createEnhancedContext({
110
+ inputs: { workspaceRoot, modules },
111
+ store: new InMemoryStore(),
112
+ trace: new InMemoryTrace(),
113
+ enableMemory: true
114
+ });
115
+ debug.debug('Execution context with memory initialized');
116
+ // Create tools (passing ExecutionContext for Deity's memory tools)
117
+ const allTools = createAllTools(toolContext, executionContext);
118
+ // Filter tools for Planner (only needs basic file operations, memory, and planning)
119
+ const plannerToolNames = ['file_list', 'file_read', 'memory_store', 'planning_create', 'file_search_content'];
120
+ const plannerTools = allTools.filter((t) => plannerToolNames.includes(t.name));
121
+ debug.success(`Created ${allTools.length} total tools (${plannerTools.length} for planner)`);
122
+ if (options.debug) {
123
+ debug.object('Planner tools', plannerTools.map((t) => t.name));
124
+ }
125
+ // Create LLM adapter using Copilot SDK (better performance than gh CLI)
126
+ const model = options.model || 'claude-opus-4.6'; // Use Claude Opus for best quality
127
+ // Streaming callback to show AI thinking in real-time
128
+ let currentLine = '';
129
+ const llmAdapter = new CopilotSDKAdapter({
130
+ model,
131
+ onStreamChunk: (chunk) => {
132
+ // Display streaming tokens in real-time
133
+ if (!useSpinner) {
134
+ process.stdout.write(chalk.gray(chunk));
135
+ }
136
+ else {
137
+ // When using spinner, update spinner text with current line
138
+ currentLine += chunk;
139
+ if (chunk.includes('\n')) {
140
+ const lines = currentLine.split('\n');
141
+ currentLine = lines[lines.length - 1];
142
+ if (lines[0].trim()) {
143
+ spinner.text = chalk.gray(lines[0].substring(0, 80));
144
+ }
145
+ }
146
+ }
147
+ },
148
+ onReasoningChunk: (chunk) => {
149
+ // Display reasoning/thinking process
150
+ if (!useSpinner) {
151
+ process.stdout.write(chalk.dim.italic(chunk));
152
+ }
153
+ }
154
+ });
155
+ // Initialize the adapter
156
+ debug.debug('Initializing Copilot SDK adapter...');
157
+ await llmAdapter.initialize();
158
+ debug.debug('LLM adapter initialized:', model);
159
+ console.log('');
160
+ console.log(chalk.blue('🤖 AI Configuration'));
161
+ console.log(chalk.gray('─'.repeat(50)));
162
+ console.log(chalk.cyan('Model:'), model);
163
+ console.log(chalk.cyan('Provider:'), 'GitHub Copilot SDK (native)');
164
+ console.log(chalk.gray('─'.repeat(50)));
165
+ console.log('');
166
+ if (useSpinner) {
167
+ spinner.text = 'Creating analysis plan...';
168
+ }
169
+ // Create Planner agent
170
+ const plannerAgent = compileAgent(createPlannerAgent(promptsDir));
171
+ // Add tools to agent
172
+ const plannerWithTools = {
173
+ ...plannerAgent,
174
+ tools: plannerTools // Only pass planner-specific tools
175
+ };
176
+ // Run Planner
177
+ const plannerWorkflow = {
178
+ name: 'limo-planner',
179
+ graph: createSequenceNode([createStepNode(plannerWithTools)]),
180
+ defaultModel: { adapter: llmAdapter }
181
+ };
182
+ console.log(''); // New line after spinner
183
+ console.log(chalk.blue('🚀 Starting Limo Analysis'));
184
+ console.log(chalk.gray('─'.repeat(50)));
185
+ console.log(chalk.cyan('Workspace:'), workspaceRoot);
186
+ console.log(chalk.cyan('Output:'), reportDir);
187
+ console.log(chalk.cyan('Modules:'), modules.join(', '));
188
+ console.log(chalk.gray('─'.repeat(50)));
189
+ console.log('');
190
+ if (useSpinner) {
191
+ spinner.start('AI is creating analysis plan...');
192
+ }
193
+ else {
194
+ console.log(chalk.blue('🤖 AI is creating analysis plan...'));
195
+ console.log(chalk.gray('─'.repeat(50)));
196
+ }
197
+ debug.section('PLANNER AGENT EXECUTION');
198
+ debug.agentStart('planner', { workspaceRoot, modules });
199
+ let planResult;
200
+ try {
201
+ debug.debug('Running planner workflow...');
202
+ planResult = await runWorkflow(plannerWorkflow, {
203
+ workspaceRoot,
204
+ modules,
205
+ limoConfig // Pass LIMO.md configuration to Planner
206
+ });
207
+ debug.agentEnd('planner', planResult);
208
+ toolContext.plan = planResult;
209
+ if (useSpinner) {
210
+ spinner.succeed(chalk.green(`Plan created: ${planResult.total_tasks} tasks`));
211
+ }
212
+ else {
213
+ console.log(chalk.green(`✓ Plan created: ${planResult.total_tasks} tasks`));
214
+ }
215
+ debug.success('Planning completed successfully');
216
+ debug.object('Plan summary', {
217
+ complexity: planResult.project_complexity,
218
+ files: planResult.estimated_files,
219
+ loc: planResult.estimated_loc,
220
+ tasks: planResult.total_tasks
221
+ });
222
+ }
223
+ catch (error) {
224
+ if (useSpinner) {
225
+ spinner.fail(chalk.red('Planning failed'));
226
+ }
227
+ else {
228
+ console.log(chalk.red('✖ Planning failed'));
229
+ }
230
+ debug.error('Planner failed:', error);
231
+ console.error('Error:', error);
232
+ throw error;
233
+ }
234
+ console.log('');
235
+ console.log(chalk.yellow('📋 Analysis Plan'));
236
+ console.log(chalk.gray('─'.repeat(50)));
237
+ console.log(chalk.cyan('Complexity:'), planResult.project_complexity);
238
+ console.log(chalk.cyan('Estimated Files:'), planResult.estimated_files);
239
+ console.log(chalk.cyan('Estimated LOC:'), planResult.estimated_loc);
240
+ console.log(chalk.cyan('Total Tasks:'), planResult.total_tasks);
241
+ console.log('');
242
+ console.log(chalk.cyan('Tasks by Module:'));
243
+ Object.entries(planResult.summary.tasks_by_module).forEach(([module, count]) => {
244
+ console.log(` ${chalk.gray('•')} ${module}: ${count}`);
245
+ });
246
+ console.log(chalk.gray('─'.repeat(50)));
247
+ console.log('');
248
+ // Save manifest
249
+ const manifest = {
250
+ id: analysisId,
251
+ displayName: `Analysis Report ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`,
252
+ createdAt: new Date().toISOString(),
253
+ scope: {
254
+ targetPath: workspaceRoot,
255
+ modules
256
+ },
257
+ status: 'planning',
258
+ version: '2.0.0'
259
+ };
260
+ await fs.writeFile(path.join(reportDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
261
+ debug.debug('Manifest saved');
262
+ // Save plan
263
+ await fs.writeFile(path.join(reportDir, 'plan.json'), JSON.stringify(planResult, null, 2));
264
+ debug.debug('Plan saved to plan.json');
265
+ console.log(chalk.green('✓ Analysis plan saved to:'), reportDir);
266
+ console.log('');
267
+ const tasksToProcess = planResult.tasks;
268
+ console.log('');
269
+ debug.section('MULTI-PHASE ANALYSIS');
270
+ // ========================================
271
+ // PHASE 2: ANALYSIS (Analyst only)
272
+ // ========================================
273
+ console.log('');
274
+ console.log(chalk.blue('📊 Phase 2: Analysis'));
275
+ console.log(chalk.gray('─'.repeat(50)));
276
+ console.log(chalk.dim('Running Analyst agent to analyze code and store findings in memory'));
277
+ console.log('');
278
+ for (let i = 0; i < tasksToProcess.length; i++) {
279
+ const task = tasksToProcess[i];
280
+ debug.progress(i + 1, tasksToProcess.length, `Analyzing task: ${task.task_id}`);
281
+ if (useSpinner) {
282
+ spinner.start(`[${i + 1}/${tasksToProcess.length}] 🔍 Analyzing: ${task.title}`);
283
+ }
284
+ else {
285
+ console.log(chalk.blue(`[${i + 1}/${tasksToProcess.length}] 🔍 Analyzing: ${task.title}`));
286
+ }
287
+ try {
288
+ // Run Analyst
289
+ debug.agentStart('analyst', { task_id: task.task_id, title: task.title });
290
+ // Filter tools for Analyst (needs file ops, terminal, memory, web search)
291
+ const analystToolNames = [
292
+ 'file_list', 'file_read', 'file_search_content', 'terminal_execute',
293
+ 'memory_store', 'memory_recall', 'memory_list', 'memory_search',
294
+ 'web_search', 'web_fetch',
295
+ 'task_get_current', 'task_execute_next',
296
+ 'subtask_create_plan', 'subtask_get_current', 'subtask_complete', 'subtask_skip',
297
+ 'context_reset', 'context_stats',
298
+ 'code_analyze_dead', 'code_check_references'
299
+ ];
300
+ const analystTools = allTools.filter((t) => analystToolNames.includes(t.name));
301
+ const analystAgent = compileAgent(createAnalystAgent(promptsDir));
302
+ const analystWithTools = { ...analystAgent, tools: analystTools };
303
+ const analystWorkflow = {
304
+ name: 'limo-analyst',
305
+ graph: createSequenceNode([createStepNode(analystWithTools)]),
306
+ defaultModel: { adapter: llmAdapter }
307
+ };
308
+ debug.debug('Running analyst workflow...');
309
+ const analystResult = await runWorkflow(analystWorkflow, {
310
+ task,
311
+ workspaceRoot,
312
+ promptsDir
313
+ });
314
+ debug.agentEnd('analyst', { memoriesCreated: analystResult.memoriesCreated });
315
+ if (useSpinner) {
316
+ spinner.succeed(chalk.green(`✓ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
317
+ }
318
+ else {
319
+ console.log(chalk.green(`✓ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
320
+ }
321
+ }
322
+ catch (error) {
323
+ if (useSpinner) {
324
+ spinner.fail(chalk.red(`✖ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
325
+ }
326
+ else {
327
+ console.log(chalk.red(`✖ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
328
+ }
329
+ console.error(chalk.gray(` Error: ${error instanceof Error ? error.message : String(error)}`));
330
+ debug.error(`Analysis task ${task.task_id} failed:`, error);
331
+ // Continue with next task
332
+ }
333
+ }
334
+ // ========================================
335
+ // PHASE 3: WRITING (Writer only)
336
+ // ========================================
337
+ console.log('');
338
+ console.log(chalk.blue('✍️ Phase 3: Writing'));
339
+ console.log(chalk.gray('─'.repeat(50)));
340
+ console.log(chalk.dim('Running Writer agent to create report sections from memory'));
341
+ console.log('');
342
+ for (let i = 0; i < tasksToProcess.length; i++) {
343
+ const task = tasksToProcess[i];
344
+ debug.progress(i + 1, tasksToProcess.length, `Writing task: ${task.task_id}`);
345
+ if (useSpinner) {
346
+ spinner.start(`[${i + 1}/${tasksToProcess.length}] ✍️ Writing: ${task.title}`);
347
+ }
348
+ else {
349
+ console.log(chalk.blue(`[${i + 1}/${tasksToProcess.length}] ✍️ Writing: ${task.title}`));
350
+ }
351
+ try {
352
+ // Run Writer
353
+ debug.agentStart('writer', { task_id: task.task_id, title: task.title });
354
+ const writerAgent = compileAgent(createWriterAgent(promptsDir));
355
+ // Filter tools for Writer (only needs memory recall and report writing)
356
+ const writerToolNames = ['memory_recall', 'memory_list', 'memory_search', 'report_write', 'report_add_diagram', 'web_search', 'web_fetch'];
357
+ const writerTools = allTools.filter((t) => writerToolNames.includes(t.name));
358
+ const writerWithTools = { ...writerAgent, tools: writerTools };
359
+ const writerWorkflow = {
360
+ name: 'limo-writer',
361
+ graph: createSequenceNode([createStepNode(writerWithTools)]),
362
+ defaultModel: { adapter: llmAdapter }
363
+ };
364
+ debug.debug('Running writer workflow...');
365
+ const writerResult = await runWorkflow(writerWorkflow, {
366
+ task,
367
+ promptsDir,
368
+ reportLanguage: options.lang || 'English'
369
+ });
370
+ debug.agentEnd('writer', { wordCount: writerResult.wordCount });
371
+ if (useSpinner) {
372
+ spinner.succeed(chalk.green(`✓ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
373
+ }
374
+ else {
375
+ console.log(chalk.green(`✓ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
376
+ }
377
+ }
378
+ catch (error) {
379
+ if (useSpinner) {
380
+ spinner.fail(chalk.red(`✖ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
381
+ }
382
+ else {
383
+ console.log(chalk.red(`✖ [${i + 1}/${tasksToProcess.length}] ${task.title}`));
384
+ }
385
+ console.error(chalk.gray(` Error: ${error instanceof Error ? error.message : String(error)}`));
386
+ debug.error(`Writing task ${task.task_id} failed:`, error);
387
+ // Continue with next task
388
+ }
389
+ }
390
+ // ========================================
391
+ // PHASE 4: EDITING (Editor - NEW!)
392
+ // ========================================
393
+ console.log('');
394
+ console.log(chalk.blue('🎨 Phase 4: Editing & Finalization'));
395
+ console.log(chalk.gray('─'.repeat(50)));
396
+ console.log(chalk.dim('Running Editor agent to generate executive summary and finalize report'));
397
+ console.log('');
398
+ let executiveSummary = '';
399
+ if (useSpinner) {
400
+ spinner.start('Generating executive summary and polishing report...');
401
+ }
402
+ else {
403
+ console.log(chalk.blue('Generating executive summary and polishing report...'));
404
+ }
405
+ try {
406
+ debug.agentStart('editor', { sectionCount: toolContext.reportSections.size });
407
+ const editorAgent = compileAgent(createEditorAgent(promptsDir));
408
+ // Editor needs memory recall and report writing
409
+ const editorToolNames = ['memory_recall', 'memory_list', 'memory_search', 'report_write', 'web_search', 'web_fetch'];
410
+ const editorTools = allTools.filter((t) => editorToolNames.includes(t.name));
411
+ const editorWithTools = { ...editorAgent, tools: editorTools };
412
+ const editorWorkflow = {
413
+ name: 'limo-editor',
414
+ graph: createSequenceNode([createStepNode(editorWithTools)]),
415
+ defaultModel: { adapter: llmAdapter }
416
+ };
417
+ debug.debug('Running editor workflow...');
418
+ // Prepare context for Editor
419
+ const editorContext = {
420
+ projectName: path.basename(workspaceRoot),
421
+ taskCount: tasksToProcess.length,
422
+ sectionCount: toolContext.reportSections.size,
423
+ diagramCount: toolContext.diagrams.size,
424
+ promptsDir,
425
+ reportLanguage: options.lang || 'English'
426
+ };
427
+ const editorResult = await runWorkflow(editorWorkflow, editorContext);
428
+ debug.agentEnd('editor', { summaryGenerated: !!editorResult.summaryGenerated });
429
+ // Extract executive summary from report sections
430
+ // Editor should have created a section with id "executive_summary"
431
+ const execSummarySection = toolContext.reportSections.get('executive_summary');
432
+ if (execSummarySection) {
433
+ executiveSummary = execSummarySection;
434
+ // Remove from sections map so it doesn't appear twice
435
+ toolContext.reportSections.delete('executive_summary');
436
+ }
437
+ if (useSpinner) {
438
+ spinner.succeed(chalk.green('✓ Executive summary generated'));
439
+ }
440
+ else {
441
+ console.log(chalk.green('✓ Executive summary generated'));
442
+ }
443
+ }
444
+ catch (error) {
445
+ console.error(chalk.yellow('⚠ Editor phase failed, continuing without executive summary'));
446
+ console.error(chalk.gray(` Error: ${error instanceof Error ? error.message : String(error)}`));
447
+ debug.error('Editor failed:', error);
448
+ // Continue without executive summary
449
+ }
450
+ console.log('');
451
+ console.log(chalk.blue('📝 Generating Final Report'));
452
+ console.log(chalk.gray('─'.repeat(50)));
453
+ // Combine all report sections into report.md using MarkdownGenerator
454
+ if (useSpinner) {
455
+ spinner.start('Combining report sections...');
456
+ }
457
+ else {
458
+ console.log(chalk.blue('Combining report sections...'));
459
+ }
460
+ // Import MarkdownGenerator
461
+ const { MarkdownGenerator, extractTitle, countAllFindings } = await import('../report/markdownGenerator.js');
462
+ const generator = new MarkdownGenerator();
463
+ // Build report sections with associated diagrams
464
+ const sections = [];
465
+ for (const [sectionId, content] of toolContext.reportSections) {
466
+ // Find diagrams for this section
467
+ const sectionDiagrams = [];
468
+ for (const [diagramId, diagram] of toolContext.diagrams) {
469
+ if (diagram.metadata?.sectionId === sectionId) {
470
+ sectionDiagrams.push(diagram);
471
+ }
472
+ }
473
+ sections.push({
474
+ id: sectionId,
475
+ title: extractTitle(content),
476
+ content,
477
+ diagrams: sectionDiagrams.length > 0 ? sectionDiagrams : undefined
478
+ });
479
+ }
480
+ // Collect orphaned diagrams (not associated with any section)
481
+ const orphanedDiagrams = [];
482
+ for (const [diagramId, diagram] of toolContext.diagrams) {
483
+ const hasSection = diagram.metadata?.sectionId;
484
+ if (!hasSection) {
485
+ orphanedDiagrams.push(diagram);
486
+ }
487
+ }
488
+ // If there are orphaned diagrams, add them as a separate section
489
+ if (orphanedDiagrams.length > 0) {
490
+ const { embedDiagramInMarkdown } = await import('../report/diagrams.js');
491
+ const diagramMarkdown = orphanedDiagrams.map(d => embedDiagramInMarkdown(d)).join('\n\n');
492
+ sections.push({
493
+ id: 'diagrams',
494
+ title: 'Diagrams',
495
+ content: `## Diagrams\n\n${diagramMarkdown}`,
496
+ diagrams: undefined // Already embedded in content
497
+ });
498
+ }
499
+ // Build report metadata
500
+ const reportMetadata = {
501
+ id: analysisId,
502
+ title: `Analysis Report - ${path.basename(workspaceRoot)}`,
503
+ generatedAt: new Date(),
504
+ projectPath: workspaceRoot,
505
+ aiModel: model,
506
+ modules: modules, // Already string[]
507
+ statistics: {
508
+ totalSections: sections.length,
509
+ totalDiagrams: toolContext.diagrams.size,
510
+ totalFindings: countAllFindings(sections)
511
+ }
512
+ };
513
+ // Build complete report object
514
+ const report = {
515
+ metadata: reportMetadata,
516
+ executiveSummary, // From Editor agent
517
+ sections
518
+ };
519
+ // Generate markdown using MarkdownGenerator
520
+ const reportContent = generator.generate(report);
521
+ await fs.writeFile(path.join(reportDir, 'report.md'), reportContent);
522
+ if (useSpinner) {
523
+ spinner.succeed(chalk.green('✓ Final report generated'));
524
+ }
525
+ else {
526
+ console.log(chalk.green('✓ Final report generated'));
527
+ }
528
+ console.log('');
529
+ console.log(chalk.green('✓ Analysis Complete!'));
530
+ console.log(chalk.gray('─'.repeat(50)));
531
+ console.log(chalk.cyan('Report:'), path.join(reportDir, 'report.md'));
532
+ console.log(chalk.cyan('Plan:'), path.join(reportDir, 'plan.json'));
533
+ console.log(chalk.gray('─'.repeat(50)));
534
+ // Update manifest with completion status
535
+ const completedManifest = {
536
+ ...manifest,
537
+ updatedAt: new Date().toISOString(),
538
+ completedAt: new Date().toISOString(),
539
+ status: 'completed',
540
+ hasReport: true,
541
+ hasSession: false // CLI doesn't use session.json
542
+ };
543
+ await fs.writeFile(path.join(reportDir, 'manifest.json'), JSON.stringify(completedManifest, null, 2));
544
+ debug.debug('Manifest updated to completed status');
545
+ // Cleanup LLM adapter
546
+ debug.debug('Cleaning up LLM adapter...');
547
+ await llmAdapter.cleanup();
548
+ debug.success('Cleanup complete');
549
+ }
550
+ catch (error) {
551
+ if (useSpinner && spinner) {
552
+ spinner.fail(chalk.red('Analysis failed'));
553
+ }
554
+ else {
555
+ console.log(chalk.red('✖ Analysis failed'));
556
+ }
557
+ throw error;
558
+ }
559
+ }
560
+ function generateId() {
561
+ return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
562
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { analyze } from './commands/analyze.js';
4
+ import chalk from 'chalk';
5
+ const program = new Command();
6
+ program
7
+ .name('limo')
8
+ .description('Limo CLI - AI-powered codebase analysis tool')
9
+ .version('0.1.0');
10
+ program
11
+ .command('analyze')
12
+ .description('Analyze a codebase and generate a comprehensive report')
13
+ .argument('[path]', 'Path to the codebase to analyze', '.')
14
+ .option('-o, --output <path>', 'Output directory for the report', '.limo')
15
+ .option('--model <model>', 'AI model to use', 'claude-opus-4.6')
16
+ .option('--lang <language>', 'Report language (English, 简体中文, 日本語, Español, Français, Deutsch)', '简体中文')
17
+ .option('--modules <modules>', 'Comma-separated list of modules to analyze', 'architecture,dependencies,code-quality,security,database,testing,migration')
18
+ .option('--verbose', 'Show AI thinking process in real-time', false)
19
+ .option('-d, --debug', 'Enable debug mode with verbose logging', false)
20
+ .option('--debug-api', 'Enable API request/response logging', false)
21
+ .action(async (path, options) => {
22
+ try {
23
+ // Set global debug mode
24
+ if (options.debug || options.debugApi) {
25
+ process.env.LIMO_DEBUG = 'true';
26
+ }
27
+ if (options.debugApi) {
28
+ process.env.LIMO_DEBUG_API = 'true';
29
+ }
30
+ await analyze(path, options);
31
+ }
32
+ catch (error) {
33
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
34
+ if (options.debug) {
35
+ console.error(chalk.gray('\nStack trace:'));
36
+ console.error(error);
37
+ }
38
+ process.exit(1);
39
+ }
40
+ });
41
+ program.parse();
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Diagram rendering utilities for CLI reports
3
+ */
4
+ /**
5
+ * Diagram definition
6
+ */
7
+ export interface DiagramDefinition {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ format: 'svg' | 'dot' | 'mermaid';
12
+ source: string;
13
+ relatedFiles?: string[];
14
+ metadata?: {
15
+ nodeCount?: number;
16
+ edgeCount?: number;
17
+ generatedAt?: string;
18
+ [key: string]: any;
19
+ };
20
+ }
21
+ /**
22
+ * Embeds a diagram in Markdown format
23
+ *
24
+ * For SVG diagrams, embeds as base64 data URI image for universal compatibility.
25
+ * For DOT/Mermaid format, embeds as code blocks for external rendering.
26
+ */
27
+ export declare function embedDiagramInMarkdown(diagram: DiagramDefinition): string;