@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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Semantic Structure (Intermediate Representation)
|
|
3
|
+
*
|
|
4
|
+
* This IR defines the STRUCTURE of a graph, not its visual representation.
|
|
5
|
+
* LLM outputs this IR, and the code compiles it to DOT deterministically.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL CONSTRAINTS:
|
|
8
|
+
* - LLM MUST NOT output DOT/Graphviz syntax
|
|
9
|
+
* - LLM MUST NOT specify layout, coordinates, styles, or visual properties
|
|
10
|
+
* - Graph quality depends on structural modeling, not LLM's text expression
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* DOT reserved keywords that cannot be used as node IDs
|
|
14
|
+
*/
|
|
15
|
+
const DOT_KEYWORDS = new Set(['node', 'edge', 'graph', 'digraph', 'subgraph', 'strict']);
|
|
16
|
+
/**
|
|
17
|
+
* Validate graph semantic structure
|
|
18
|
+
*/
|
|
19
|
+
export function validateGraphSemantics(graph) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
// Check for empty graph
|
|
23
|
+
if (graph.nodes.length === 0) {
|
|
24
|
+
errors.push('Graph must contain at least one node');
|
|
25
|
+
}
|
|
26
|
+
// Check for duplicate node IDs
|
|
27
|
+
const nodeIds = new Set();
|
|
28
|
+
for (const node of graph.nodes) {
|
|
29
|
+
if (nodeIds.has(node.id)) {
|
|
30
|
+
errors.push(`Duplicate node ID: ${node.id}`);
|
|
31
|
+
}
|
|
32
|
+
nodeIds.add(node.id);
|
|
33
|
+
// Validate node ID legality
|
|
34
|
+
// Check for DOT reserved keywords
|
|
35
|
+
if (DOT_KEYWORDS.has(node.id.toLowerCase())) {
|
|
36
|
+
warnings.push(`Node ID '${node.id}' is a DOT reserved keyword. ` +
|
|
37
|
+
`It will be sanitized to 'n_${node.id}' during compilation.`);
|
|
38
|
+
}
|
|
39
|
+
// Check for digit-starting IDs
|
|
40
|
+
if (/^[0-9]/.test(node.id)) {
|
|
41
|
+
warnings.push(`Node ID '${node.id}' starts with a digit. ` +
|
|
42
|
+
`It will be sanitized to 'n_${node.id}' during compilation.`);
|
|
43
|
+
}
|
|
44
|
+
// Check for special characters
|
|
45
|
+
if (/[^a-zA-Z0-9_]/.test(node.id)) {
|
|
46
|
+
warnings.push(`Node ID '${node.id}' contains special characters. ` +
|
|
47
|
+
`They will be replaced with underscores during compilation.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Check for duplicate group IDs
|
|
51
|
+
if (graph.groups) {
|
|
52
|
+
const groupIds = new Set();
|
|
53
|
+
for (const group of graph.groups) {
|
|
54
|
+
if (groupIds.has(group.id)) {
|
|
55
|
+
errors.push(`Duplicate group ID: ${group.id}`);
|
|
56
|
+
}
|
|
57
|
+
groupIds.add(group.id);
|
|
58
|
+
// Validate group ID legality
|
|
59
|
+
if (DOT_KEYWORDS.has(group.id.toLowerCase())) {
|
|
60
|
+
warnings.push(`Group ID '${group.id}' is a DOT reserved keyword. ` +
|
|
61
|
+
`It will be sanitized during compilation.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Validate edges reference existing nodes
|
|
66
|
+
for (const edge of graph.edges) {
|
|
67
|
+
if (!nodeIds.has(edge.from)) {
|
|
68
|
+
errors.push(`Edge references non-existent source node: ${edge.from}`);
|
|
69
|
+
}
|
|
70
|
+
if (!nodeIds.has(edge.to)) {
|
|
71
|
+
errors.push(`Edge references non-existent target node: ${edge.to}`);
|
|
72
|
+
}
|
|
73
|
+
if (edge.from === edge.to) {
|
|
74
|
+
warnings.push(`Self-loop detected: ${edge.from} -> ${edge.to}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Validate node groups reference existing groups
|
|
78
|
+
if (graph.groups) {
|
|
79
|
+
const groupIds = new Set(graph.groups.map(g => g.id));
|
|
80
|
+
for (const node of graph.nodes) {
|
|
81
|
+
if (node.group && !groupIds.has(node.group)) {
|
|
82
|
+
errors.push(`Node ${node.id} references non-existent group: ${node.group}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check for circular group references
|
|
86
|
+
for (const group of graph.groups) {
|
|
87
|
+
if (group.parent && !groupIds.has(group.parent)) {
|
|
88
|
+
errors.push(`Group ${group.id} references non-existent parent: ${group.parent}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Warn if graph is too complex
|
|
93
|
+
if (graph.nodes.length > 50) {
|
|
94
|
+
warnings.push(`Graph has ${graph.nodes.length} nodes, consider splitting into multiple diagrams`);
|
|
95
|
+
}
|
|
96
|
+
if (graph.edges.length > 100) {
|
|
97
|
+
warnings.push(`Graph has ${graph.edges.length} edges, consider simplifying`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
valid: errors.length === 0,
|
|
101
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
102
|
+
warnings: warnings.length > 0 ? warnings : undefined
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug utilities for Limo CLI
|
|
3
|
+
* Provides structured logging with different levels
|
|
4
|
+
*/
|
|
5
|
+
export declare class DebugLogger {
|
|
6
|
+
private startTime;
|
|
7
|
+
constructor();
|
|
8
|
+
private get enabled();
|
|
9
|
+
private get apiDebugEnabled();
|
|
10
|
+
private getTimestamp;
|
|
11
|
+
debug(...args: any[]): void;
|
|
12
|
+
info(...args: any[]): void;
|
|
13
|
+
warn(...args: any[]): void;
|
|
14
|
+
error(...args: any[]): void;
|
|
15
|
+
success(...args: any[]): void;
|
|
16
|
+
api(direction: 'request' | 'response', data: any): void;
|
|
17
|
+
toolCall(toolName: string, params: any): void;
|
|
18
|
+
toolResult(toolName: string, result: any): void;
|
|
19
|
+
agentStart(agentName: string, inputs: any): void;
|
|
20
|
+
agentEnd(agentName: string, outputs: any): void;
|
|
21
|
+
section(title: string): void;
|
|
22
|
+
object(label: string, obj: any): void;
|
|
23
|
+
memory(action: 'store' | 'recall', key: string, data?: any): void;
|
|
24
|
+
progress(current: number, total: number, message: string): void;
|
|
25
|
+
}
|
|
26
|
+
export declare const debug: DebugLogger;
|
|
27
|
+
export declare function isDebugEnabled(): boolean;
|
|
28
|
+
export declare function isApiDebugEnabled(): boolean;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug utilities for Limo CLI
|
|
3
|
+
* Provides structured logging with different levels
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
export class DebugLogger {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.startTime = Date.now();
|
|
9
|
+
}
|
|
10
|
+
get enabled() {
|
|
11
|
+
return process.env.LIMO_DEBUG === 'true';
|
|
12
|
+
}
|
|
13
|
+
get apiDebugEnabled() {
|
|
14
|
+
return process.env.LIMO_DEBUG_API === 'true';
|
|
15
|
+
}
|
|
16
|
+
getTimestamp() {
|
|
17
|
+
const elapsed = Date.now() - this.startTime;
|
|
18
|
+
const seconds = (elapsed / 1000).toFixed(3);
|
|
19
|
+
return chalk.gray(`[+${seconds}s]`);
|
|
20
|
+
}
|
|
21
|
+
debug(...args) {
|
|
22
|
+
if (this.enabled) {
|
|
23
|
+
console.log(this.getTimestamp(), chalk.cyan('[DEBUG]'), ...args);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
info(...args) {
|
|
27
|
+
console.log(this.getTimestamp(), chalk.blue('[INFO]'), ...args);
|
|
28
|
+
}
|
|
29
|
+
warn(...args) {
|
|
30
|
+
console.log(this.getTimestamp(), chalk.yellow('[WARN]'), ...args);
|
|
31
|
+
}
|
|
32
|
+
error(...args) {
|
|
33
|
+
console.log(this.getTimestamp(), chalk.red('[ERROR]'), ...args);
|
|
34
|
+
}
|
|
35
|
+
success(...args) {
|
|
36
|
+
console.log(this.getTimestamp(), chalk.green('[SUCCESS]'), ...args);
|
|
37
|
+
}
|
|
38
|
+
api(direction, data) {
|
|
39
|
+
if (this.apiDebugEnabled) {
|
|
40
|
+
const prefix = direction === 'request' ? '→' : '←';
|
|
41
|
+
console.log(this.getTimestamp(), chalk.magenta(`[API ${prefix}]`));
|
|
42
|
+
if (typeof data === 'string') {
|
|
43
|
+
// Truncate long strings
|
|
44
|
+
const truncated = data.length > 500 ? data.substring(0, 500) + '...' : data;
|
|
45
|
+
console.log(chalk.gray(truncated));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(JSON.stringify(data, null, 2));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
toolCall(toolName, params) {
|
|
53
|
+
if (this.enabled) {
|
|
54
|
+
console.log(this.getTimestamp(), chalk.yellow('[TOOL]'), toolName);
|
|
55
|
+
console.log(chalk.gray(' Parameters:'), JSON.stringify(params, null, 2));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
toolResult(toolName, result) {
|
|
59
|
+
if (this.enabled) {
|
|
60
|
+
const status = result.success ? chalk.green('✓') : chalk.red('✗');
|
|
61
|
+
console.log(this.getTimestamp(), chalk.yellow('[TOOL]'), toolName, status);
|
|
62
|
+
if (result.error) {
|
|
63
|
+
console.log(chalk.red(' Error:'), result.error);
|
|
64
|
+
}
|
|
65
|
+
else if (result.data) {
|
|
66
|
+
const dataStr = typeof result.data === 'string'
|
|
67
|
+
? result.data.substring(0, 200)
|
|
68
|
+
: JSON.stringify(result.data, null, 2).substring(0, 200);
|
|
69
|
+
console.log(chalk.gray(' Result:'), dataStr + (dataStr.length >= 200 ? '...' : ''));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
agentStart(agentName, inputs) {
|
|
74
|
+
if (this.enabled) {
|
|
75
|
+
console.log('\n' + chalk.bgCyan.black(` AGENT START: ${agentName.toUpperCase()} `));
|
|
76
|
+
console.log(chalk.gray('Inputs:'), JSON.stringify(inputs, null, 2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
agentEnd(agentName, outputs) {
|
|
80
|
+
if (this.enabled) {
|
|
81
|
+
console.log(chalk.bgGreen.black(` AGENT END: ${agentName.toUpperCase()} `));
|
|
82
|
+
console.log(chalk.gray('Outputs:'), JSON.stringify(outputs, null, 2));
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
section(title) {
|
|
87
|
+
if (this.enabled) {
|
|
88
|
+
console.log('\n' + chalk.cyan('═'.repeat(60)));
|
|
89
|
+
console.log(chalk.cyan.bold(` ${title}`));
|
|
90
|
+
console.log(chalk.cyan('═'.repeat(60)) + '\n');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
object(label, obj) {
|
|
94
|
+
if (this.enabled) {
|
|
95
|
+
console.log(this.getTimestamp(), chalk.cyan('[OBJECT]'), label);
|
|
96
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
memory(action, key, data) {
|
|
100
|
+
if (this.enabled) {
|
|
101
|
+
const icon = action === 'store' ? '💾' : '🔍';
|
|
102
|
+
console.log(this.getTimestamp(), chalk.magenta(`[MEMORY ${icon}]`), key);
|
|
103
|
+
if (data && action === 'store') {
|
|
104
|
+
console.log(chalk.gray(' Content length:'), data.content?.length || 0);
|
|
105
|
+
console.log(chalk.gray(' Importance:'), data.importance);
|
|
106
|
+
console.log(chalk.gray(' Category:'), data.category);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
progress(current, total, message) {
|
|
111
|
+
if (this.enabled) {
|
|
112
|
+
const percentage = ((current / total) * 100).toFixed(0);
|
|
113
|
+
console.log(this.getTimestamp(), chalk.blue('[PROGRESS]'), `[${current}/${total}]`, `${percentage}%`, message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Global singleton instance
|
|
118
|
+
export const debug = new DebugLogger();
|
|
119
|
+
// Helper function to check if debug is enabled
|
|
120
|
+
export function isDebugEnabled() {
|
|
121
|
+
return process.env.LIMO_DEBUG === 'true';
|
|
122
|
+
}
|
|
123
|
+
export function isApiDebugEnabled() {
|
|
124
|
+
return process.env.LIMO_DEBUG_API === 'true';
|
|
125
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LIMO.md Configuration Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses user-provided LIMO.md files to extract analysis requirements,
|
|
5
|
+
* focus areas, constraints, and private context.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* LIMO configuration interface
|
|
9
|
+
*/
|
|
10
|
+
export interface LimoConfiguration {
|
|
11
|
+
rawContent: string;
|
|
12
|
+
focusAreas?: string[];
|
|
13
|
+
excludeModules?: string[];
|
|
14
|
+
scopeDescription?: string;
|
|
15
|
+
privateContext?: string;
|
|
16
|
+
constraints?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse LIMO.md file and extract configuration
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseLimoConfig(workspaceRoot: string): Promise<LimoConfiguration | undefined>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LIMO.md Configuration Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses user-provided LIMO.md files to extract analysis requirements,
|
|
5
|
+
* focus areas, constraints, and private context.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { debug } from './debug.js';
|
|
10
|
+
/**
|
|
11
|
+
* Valid analysis module names
|
|
12
|
+
*/
|
|
13
|
+
const VALID_MODULES = [
|
|
14
|
+
'architecture',
|
|
15
|
+
'dependencies',
|
|
16
|
+
'code-quality',
|
|
17
|
+
'security',
|
|
18
|
+
'database',
|
|
19
|
+
'testing',
|
|
20
|
+
'migration',
|
|
21
|
+
'migration-verification'
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Parse LIMO.md file and extract configuration
|
|
25
|
+
*/
|
|
26
|
+
export async function parseLimoConfig(workspaceRoot) {
|
|
27
|
+
const limoPath = path.join(workspaceRoot, 'LIMO.md');
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(limoPath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
debug.info('No LIMO.md file found - using default analysis scope');
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(limoPath, 'utf-8');
|
|
37
|
+
debug.info('Found LIMO.md configuration file');
|
|
38
|
+
// Parse configuration (flexible, keyword-based parsing)
|
|
39
|
+
return {
|
|
40
|
+
rawContent: content,
|
|
41
|
+
focusAreas: extractFocusAreas(content),
|
|
42
|
+
excludeModules: extractExcludeModules(content),
|
|
43
|
+
scopeDescription: extractScopeDescription(content),
|
|
44
|
+
privateContext: extractPrivateContext(content),
|
|
45
|
+
constraints: extractConstraints(content)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
debug.warn('Failed to parse LIMO.md - using default analysis scope', error);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract focus areas from LIMO.md content
|
|
55
|
+
* Looks for sections like "Focus Areas" or keywords like "only analyze", "focus on"
|
|
56
|
+
*/
|
|
57
|
+
function extractFocusAreas(content) {
|
|
58
|
+
const areas = [];
|
|
59
|
+
// Look for explicit focus areas section
|
|
60
|
+
const focusSectionMatch = content.match(/##?\s*Focus\s*Areas?[:\s]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
61
|
+
if (focusSectionMatch) {
|
|
62
|
+
const focusSection = focusSectionMatch[1];
|
|
63
|
+
// Extract list items
|
|
64
|
+
const listItems = focusSection.match(/^[\s-*]+(.+)$/gm);
|
|
65
|
+
if (listItems) {
|
|
66
|
+
listItems.forEach(item => {
|
|
67
|
+
const cleaned = item.replace(/^[\s-*]+/, '').trim().toLowerCase();
|
|
68
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
69
|
+
areas.push(cleaned);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Look for "Include:" or "What to analyze:" sections
|
|
75
|
+
const includeMatch = content.match(/(?:Include|What to analyze)[:\s]*\n([\s\S]*?)(?=\n##|(?:Exclude|What to skip)|$)/i);
|
|
76
|
+
if (includeMatch) {
|
|
77
|
+
const includeSection = includeMatch[1];
|
|
78
|
+
const listItems = includeSection.match(/^[\s-*]+(.+)$/gm);
|
|
79
|
+
if (listItems) {
|
|
80
|
+
listItems.forEach(item => {
|
|
81
|
+
const cleaned = item.replace(/^[\s-*]+/, '').trim().toLowerCase();
|
|
82
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
83
|
+
areas.push(cleaned);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Look for keywords in natural language
|
|
89
|
+
const naturalLanguagePatterns = [
|
|
90
|
+
/(?:only|just)\s+(?:analyze|focus\s+on|check)\s+(?:the\s+)?(\w+(?:\s+and\s+\w+)*)/gi,
|
|
91
|
+
/focus\s+(?:on|areas?)[:\s]+(.+?)(?:\.|$)/gi,
|
|
92
|
+
/concentrate\s+on[:\s]+(.+?)(?:\.|$)/gi
|
|
93
|
+
];
|
|
94
|
+
naturalLanguagePatterns.forEach(pattern => {
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
97
|
+
const mentioned = match[1].toLowerCase().split(/\s+(?:and|,)\s+/);
|
|
98
|
+
mentioned.forEach(word => {
|
|
99
|
+
const cleaned = word.trim().replace(/[.,;!?]/, '');
|
|
100
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
101
|
+
areas.push(cleaned);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Validate and deduplicate
|
|
107
|
+
const validAreas = [...new Set(areas)].filter(area => VALID_MODULES.includes(area));
|
|
108
|
+
const invalidAreas = [...new Set(areas)].filter(area => !VALID_MODULES.includes(area));
|
|
109
|
+
if (invalidAreas.length > 0) {
|
|
110
|
+
debug.warn(`Invalid module names in LIMO.md: ${invalidAreas.join(', ')}`);
|
|
111
|
+
debug.warn(`Valid modules: ${VALID_MODULES.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
return validAreas.length > 0 ? validAreas : undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract excluded modules from LIMO.md content
|
|
117
|
+
* Looks for keywords like "skip", "ignore", "don't analyze", "exclude"
|
|
118
|
+
*/
|
|
119
|
+
function extractExcludeModules(content) {
|
|
120
|
+
const excludes = [];
|
|
121
|
+
// Look for explicit exclude section
|
|
122
|
+
const excludeSectionMatch = content.match(/##?\s*(?:Exclude|What to skip)[:\s]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
123
|
+
if (excludeSectionMatch) {
|
|
124
|
+
const excludeSection = excludeSectionMatch[1];
|
|
125
|
+
// Extract list items
|
|
126
|
+
const listItems = excludeSection.match(/^[\s-*]+(.+)$/gm);
|
|
127
|
+
if (listItems) {
|
|
128
|
+
listItems.forEach(item => {
|
|
129
|
+
const cleaned = item.replace(/^[\s-*]+/, '').trim().toLowerCase();
|
|
130
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
131
|
+
excludes.push(cleaned);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Look for "Exclude:" in focus areas section
|
|
137
|
+
const excludeMatch = content.match(/Exclude[:\s]*\n([\s\S]*?)(?=\n##|\n\*\*|$)/i);
|
|
138
|
+
if (excludeMatch) {
|
|
139
|
+
const excludeSection = excludeMatch[1];
|
|
140
|
+
const listItems = excludeSection.match(/^[\s-*]+(.+)$/gm);
|
|
141
|
+
if (listItems) {
|
|
142
|
+
listItems.forEach(item => {
|
|
143
|
+
const cleaned = item.replace(/^[\s-*]+/, '').trim().toLowerCase();
|
|
144
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
145
|
+
excludes.push(cleaned);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Look for keywords in natural language
|
|
151
|
+
const naturalLanguagePatterns = [
|
|
152
|
+
/(?:skip|ignore|don't\s+analyze|exclude)\s+(?:the\s+)?(\w+(?:\s+and\s+\w+)*)/gi,
|
|
153
|
+
/no\s+(\w+)\s+(?:analysis|module)/gi
|
|
154
|
+
];
|
|
155
|
+
naturalLanguagePatterns.forEach(pattern => {
|
|
156
|
+
let match;
|
|
157
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
158
|
+
const mentioned = match[1].toLowerCase().split(/\s+(?:and|,)\s+/);
|
|
159
|
+
mentioned.forEach(word => {
|
|
160
|
+
const cleaned = word.trim().replace(/[.,;!?]/, '');
|
|
161
|
+
if (VALID_MODULES.includes(cleaned)) {
|
|
162
|
+
excludes.push(cleaned);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// Validate and deduplicate
|
|
168
|
+
const validExcludes = [...new Set(excludes)].filter(area => VALID_MODULES.includes(area));
|
|
169
|
+
return validExcludes.length > 0 ? validExcludes : undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extract scope description from LIMO.md content
|
|
173
|
+
* Looks for sections describing what user wants analyzed
|
|
174
|
+
*/
|
|
175
|
+
function extractScopeDescription(content) {
|
|
176
|
+
// Look for "Analysis Scope" section
|
|
177
|
+
const scopeSectionMatch = content.match(/##?\s*Analysis\s*Scope[:\s]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
178
|
+
if (scopeSectionMatch) {
|
|
179
|
+
const scopeSection = scopeSectionMatch[1].trim();
|
|
180
|
+
// Remove empty lines and HTML comments
|
|
181
|
+
const cleaned = scopeSection
|
|
182
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
183
|
+
.replace(/^\s*$/gm, '')
|
|
184
|
+
.trim();
|
|
185
|
+
if (cleaned && cleaned.length > 10) {
|
|
186
|
+
return cleaned;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Look for "What to analyze:" section
|
|
190
|
+
const whatToAnalyzeMatch = content.match(/\*\*What to analyze:\*\*\s*\n([\s\S]*?)(?=\n\*\*|$)/i);
|
|
191
|
+
if (whatToAnalyzeMatch) {
|
|
192
|
+
const whatSection = whatToAnalyzeMatch[1].trim();
|
|
193
|
+
const cleaned = whatSection
|
|
194
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
195
|
+
.replace(/^\s*$/gm, '')
|
|
196
|
+
.trim();
|
|
197
|
+
if (cleaned && cleaned.length > 10) {
|
|
198
|
+
return cleaned;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Look for natural language descriptions
|
|
202
|
+
const descriptionPatterns = [
|
|
203
|
+
/(?:analyze|focus\s+on|examine)\s+(?:only|just)?\s*(?:the\s+)?(.+?)(?:\.|$)/i,
|
|
204
|
+
/scope[:\s]+(.+?)(?:\.|$)/i
|
|
205
|
+
];
|
|
206
|
+
for (const pattern of descriptionPatterns) {
|
|
207
|
+
const match = content.match(pattern);
|
|
208
|
+
if (match && match[1].length > 10) {
|
|
209
|
+
return match[1].trim();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Extract private/company context from LIMO.md content
|
|
216
|
+
* Looks for sections with company-specific info, constraints, etc.
|
|
217
|
+
*/
|
|
218
|
+
function extractPrivateContext(content) {
|
|
219
|
+
// Look for context sections
|
|
220
|
+
const contextPatterns = [
|
|
221
|
+
/##?\s*(?:Private|Company|Domain|Project)\s*Context[:\s]*\n([\s\S]*?)(?=\n##|$)/i,
|
|
222
|
+
/##?\s*(?:Production|Environment|Company)\s*(?:Environment|Context|Info)[:\s]*\n([\s\S]*?)(?=\n##|$)/i,
|
|
223
|
+
/##?\s*(?:Security|Business)\s*Requirements?[:\s]*\n([\s\S]*?)(?=\n##|$)/i
|
|
224
|
+
];
|
|
225
|
+
for (const pattern of contextPatterns) {
|
|
226
|
+
const match = content.match(pattern);
|
|
227
|
+
if (match) {
|
|
228
|
+
const context = match[1].trim();
|
|
229
|
+
// Remove HTML comments
|
|
230
|
+
const cleaned = context
|
|
231
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
232
|
+
.replace(/^\s*$/gm, '')
|
|
233
|
+
.trim();
|
|
234
|
+
if (cleaned && cleaned.length > 10) {
|
|
235
|
+
return cleaned;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Extract constraints from LIMO.md content
|
|
243
|
+
* Looks for specific constraints like "no external dependencies", "must use X"
|
|
244
|
+
*/
|
|
245
|
+
function extractConstraints(content) {
|
|
246
|
+
const constraints = [];
|
|
247
|
+
// Look for constraints section
|
|
248
|
+
const constraintsSectionMatch = content.match(/##?\s*Constraints?[:\s]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
249
|
+
if (constraintsSectionMatch) {
|
|
250
|
+
const constraintsSection = constraintsSectionMatch[1];
|
|
251
|
+
// Extract list items or numbered items
|
|
252
|
+
const listItems = constraintsSection.match(/^[\s-*\d.]+(.+)$/gm);
|
|
253
|
+
if (listItems) {
|
|
254
|
+
listItems.forEach(item => {
|
|
255
|
+
const cleaned = item.replace(/^[\s-*\d.]+/, '').trim();
|
|
256
|
+
if (cleaned && cleaned.length > 5) {
|
|
257
|
+
constraints.push(cleaned);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Look for "must" or "cannot" statements
|
|
263
|
+
const mustPattern = /(?:must|cannot|should not|can't|don't)\s+(.+?)(?:\.|$)/gi;
|
|
264
|
+
let match;
|
|
265
|
+
while ((match = mustPattern.exec(content)) !== null) {
|
|
266
|
+
const constraint = match[0].trim();
|
|
267
|
+
if (constraint.length > 10 && constraint.length < 200) {
|
|
268
|
+
constraints.push(constraint);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Deduplicate
|
|
272
|
+
const uniqueConstraints = [...new Set(constraints)];
|
|
273
|
+
return uniqueConstraints.length > 0 ? uniqueConstraints : undefined;
|
|
274
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Folder Monitor
|
|
3
|
+
* Watches for new files in the review/ directory and checks for comments
|
|
4
|
+
*/
|
|
5
|
+
export declare class ReviewMonitor {
|
|
6
|
+
private reviewPath;
|
|
7
|
+
private watcher;
|
|
8
|
+
private processedFiles;
|
|
9
|
+
constructor(workspaceRoot: string);
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
stop(): void;
|
|
12
|
+
private processExistingFiles;
|
|
13
|
+
private processFile;
|
|
14
|
+
private extractComments;
|
|
15
|
+
getStats(): {
|
|
16
|
+
totalFiles: number;
|
|
17
|
+
processedFiles: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export default ReviewMonitor;
|