@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.
- package/README.md +238 -0
- package/dist/agents/analyst.d.ts +24 -0
- package/dist/agents/analyst.js +128 -0
- package/dist/agents/editor.d.ts +26 -0
- package/dist/agents/editor.js +157 -0
- package/dist/agents/planner-validator.d.ts +7 -0
- package/dist/agents/planner-validator.js +125 -0
- package/dist/agents/planner.d.ts +56 -0
- package/dist/agents/planner.js +186 -0
- package/dist/agents/writer.d.ts +25 -0
- package/dist/agents/writer.js +164 -0
- package/dist/commands/analyze.d.ts +14 -0
- package/dist/commands/analyze.js +562 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/report/diagrams.d.ts +27 -0
- package/dist/report/diagrams.js +74 -0
- package/dist/report/graphCompiler.d.ts +37 -0
- package/dist/report/graphCompiler.js +277 -0
- package/dist/report/markdownGenerator.d.ts +71 -0
- package/dist/report/markdownGenerator.js +148 -0
- package/dist/tools/additional.d.ts +116 -0
- package/dist/tools/additional.js +349 -0
- package/dist/tools/extended.d.ts +101 -0
- package/dist/tools/extended.js +586 -0
- package/dist/tools/index.d.ts +86 -0
- package/dist/tools/index.js +362 -0
- package/dist/types/agents.types.d.ts +139 -0
- package/dist/types/agents.types.js +6 -0
- package/dist/types/graphSemantics.d.ts +99 -0
- package/dist/types/graphSemantics.js +104 -0
- package/dist/utils/debug.d.ts +28 -0
- package/dist/utils/debug.js +125 -0
- package/dist/utils/limoConfigParser.d.ts +21 -0
- package/dist/utils/limoConfigParser.js +274 -0
- package/dist/utils/reviewMonitor.d.ts +20 -0
- package/dist/utils/reviewMonitor.js +121 -0
- package/package.json +62 -0
- package/prompts/analyst.md +343 -0
- package/prompts/editor.md +196 -0
- package/prompts/planner.md +388 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|