@merlean/analyzer 1.0.0 ā 1.1.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/bin/cli.js +65 -66
- package/lib/analyzer.js +10 -103
- package/package.json +1 -3
- package/lib/uploader.js +0 -37
package/bin/cli.js
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Merlean Analyzer CLI
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Options:
|
|
10
|
-
* --name, -n Site name (required)
|
|
11
|
-
* --upload, -u Backend URL to upload map (required)
|
|
12
|
-
* --output, -o Output file path (optional, default: ./site-map.json)
|
|
13
|
-
* --token, -t Auth token for upload (optional)
|
|
6
|
+
* Scans your codebase and uploads to Merlean backend for AI analysis.
|
|
7
|
+
* No API keys required - analysis happens on our servers.
|
|
14
8
|
*
|
|
15
|
-
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx @merlean/analyzer ./my-project --name "My App"
|
|
16
11
|
*/
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
require('dotenv').config({ path: require('path').join(__dirname, '../../.env') });
|
|
20
|
-
require('dotenv').config();
|
|
21
|
-
|
|
22
|
-
const { analyzeCodebase } = require('../lib/analyzer');
|
|
23
|
-
const { uploadMap } = require('../lib/uploader');
|
|
13
|
+
const { scanCodebase } = require('../lib/analyzer');
|
|
24
14
|
const path = require('path');
|
|
25
15
|
const fs = require('fs');
|
|
26
16
|
|
|
17
|
+
const DEFAULT_BACKEND = 'https://ai-bot-backend.fly.dev';
|
|
18
|
+
|
|
27
19
|
// Parse CLI arguments
|
|
28
20
|
function parseArgs() {
|
|
29
21
|
const args = process.argv.slice(2);
|
|
30
22
|
const options = {
|
|
31
23
|
path: null,
|
|
32
24
|
name: null,
|
|
33
|
-
|
|
34
|
-
output:
|
|
35
|
-
token: null
|
|
25
|
+
backend: DEFAULT_BACKEND,
|
|
26
|
+
output: null
|
|
36
27
|
};
|
|
37
28
|
|
|
38
29
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -40,12 +31,10 @@ function parseArgs() {
|
|
|
40
31
|
|
|
41
32
|
if (arg === '--name' || arg === '-n') {
|
|
42
33
|
options.name = args[++i];
|
|
43
|
-
} else if (arg === '--
|
|
44
|
-
options.
|
|
34
|
+
} else if (arg === '--backend' || arg === '-b') {
|
|
35
|
+
options.backend = args[++i];
|
|
45
36
|
} else if (arg === '--output' || arg === '-o') {
|
|
46
37
|
options.output = args[++i];
|
|
47
|
-
} else if (arg === '--token' || arg === '-t') {
|
|
48
|
-
options.token = args[++i];
|
|
49
38
|
} else if (arg === '--help' || arg === '-h') {
|
|
50
39
|
printHelp();
|
|
51
40
|
process.exit(0);
|
|
@@ -59,35 +48,34 @@ function parseArgs() {
|
|
|
59
48
|
|
|
60
49
|
function printHelp() {
|
|
61
50
|
console.log(`
|
|
62
|
-
|
|
51
|
+
Merlean Analyzer - AI-powered codebase analysis
|
|
63
52
|
|
|
64
53
|
Usage:
|
|
65
|
-
|
|
54
|
+
npx @merlean/analyzer <path> --name <name>
|
|
66
55
|
|
|
67
56
|
Arguments:
|
|
68
57
|
<path> Path to codebase to analyze
|
|
69
58
|
|
|
70
59
|
Options:
|
|
71
60
|
--name, -n <name> Site name (required)
|
|
72
|
-
--
|
|
73
|
-
--output, -o <file>
|
|
74
|
-
--token, -t <token> Auth token for upload
|
|
61
|
+
--backend, -b <url> Backend URL (default: ${DEFAULT_BACKEND})
|
|
62
|
+
--output, -o <file> Save site map locally (optional)
|
|
75
63
|
--help, -h Show this help
|
|
76
64
|
|
|
77
65
|
Examples:
|
|
78
|
-
# Analyze
|
|
79
|
-
|
|
66
|
+
# Analyze your project
|
|
67
|
+
npx @merlean/analyzer ./my-app --name "My App"
|
|
80
68
|
|
|
81
|
-
#
|
|
82
|
-
|
|
69
|
+
# Use custom backend
|
|
70
|
+
npx @merlean/analyzer ./my-app --name "My App" --backend http://localhost:3004
|
|
83
71
|
|
|
84
|
-
#
|
|
85
|
-
|
|
72
|
+
# Also save map locally
|
|
73
|
+
npx @merlean/analyzer ./my-app --name "My App" --output ./site-map.json
|
|
86
74
|
`);
|
|
87
75
|
}
|
|
88
76
|
|
|
89
77
|
async function main() {
|
|
90
|
-
console.log('\nš
|
|
78
|
+
console.log('\nš Merlean Analyzer\n');
|
|
91
79
|
|
|
92
80
|
const options = parseArgs();
|
|
93
81
|
|
|
@@ -110,42 +98,54 @@ async function main() {
|
|
|
110
98
|
process.exit(1);
|
|
111
99
|
}
|
|
112
100
|
|
|
113
|
-
console.log(`š
|
|
101
|
+
console.log(`š Scanning: ${codebasePath}`);
|
|
114
102
|
console.log(`š Site name: ${options.name}`);
|
|
115
103
|
|
|
116
104
|
try {
|
|
117
|
-
//
|
|
118
|
-
const
|
|
105
|
+
// Scan codebase locally
|
|
106
|
+
const fileContents = await scanCodebase(codebasePath);
|
|
119
107
|
|
|
120
|
-
console.log(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
108
|
+
console.log(`\nš¤ Uploading to backend for analysis...`);
|
|
109
|
+
|
|
110
|
+
// Send to backend for LLM analysis
|
|
111
|
+
const response = await fetch(`${options.backend}/api/analyze`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
siteName: options.name,
|
|
116
|
+
files: fileContents
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const error = await response.text();
|
|
122
|
+
throw new Error(`Backend error: ${response.status} ${error}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await response.json();
|
|
126
|
+
|
|
127
|
+
console.log('\nā
Analysis complete!');
|
|
128
|
+
console.log('\nš Summary:');
|
|
129
|
+
console.log(` Site ID: ${result.siteId}`);
|
|
130
|
+
console.log(` Framework: ${result.framework || 'Unknown'}`);
|
|
131
|
+
console.log(` Routes: ${result.routes?.length || 0}`);
|
|
132
|
+
console.log(` Forms: ${result.forms?.length || 0}`);
|
|
133
|
+
console.log(` Actions: ${result.actions?.length || 0}`);
|
|
134
|
+
|
|
135
|
+
// Save locally if requested
|
|
136
|
+
if (options.output) {
|
|
137
|
+
const outputPath = path.resolve(options.output);
|
|
138
|
+
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
|
139
|
+
console.log(`\nš¾ Saved to: ${outputPath}`);
|
|
147
140
|
}
|
|
148
141
|
|
|
142
|
+
// Show integration instructions
|
|
143
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
144
|
+
console.log('\nš Integration Ready!\n');
|
|
145
|
+
console.log('Add this to your website:\n');
|
|
146
|
+
console.log(`<script src="${options.backend}/bot.js" data-site-id="${result.siteId}"></script>`);
|
|
147
|
+
console.log('\n' + 'ā'.repeat(50) + '\n');
|
|
148
|
+
|
|
149
149
|
} catch (error) {
|
|
150
150
|
console.error(`\nā Error: ${error.message}`);
|
|
151
151
|
process.exit(1);
|
|
@@ -153,4 +153,3 @@ async function main() {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
main();
|
|
156
|
-
|
package/lib/analyzer.js
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Codebase Scanner
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - Forms and their fields
|
|
7
|
-
* - Actions/mutations
|
|
4
|
+
* Scans codebase and prepares file contents for backend analysis.
|
|
5
|
+
* NO LLM calls here - that happens on the backend.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
require('dotenv').config({ path: require('path').join(__dirname, '../../.env') });
|
|
11
|
-
require('dotenv').config();
|
|
12
|
-
|
|
13
|
-
const Anthropic = require('@anthropic-ai/sdk').default;
|
|
14
8
|
const fs = require('fs');
|
|
15
9
|
const path = require('path');
|
|
16
10
|
const { glob } = require('glob');
|
|
17
|
-
const crypto = require('crypto');
|
|
18
|
-
|
|
19
|
-
// Initialize Anthropic
|
|
20
|
-
const anthropic = new Anthropic({
|
|
21
|
-
apiKey: process.env.ANTHROPIC_API_KEY
|
|
22
|
-
});
|
|
23
11
|
|
|
24
12
|
// File patterns to scan
|
|
25
13
|
const SCAN_PATTERNS = [
|
|
@@ -52,11 +40,9 @@ const PRIORITY_KEYWORDS = [
|
|
|
52
40
|
];
|
|
53
41
|
|
|
54
42
|
/**
|
|
55
|
-
*
|
|
43
|
+
* Scan codebase and collect file contents
|
|
56
44
|
*/
|
|
57
|
-
async function
|
|
58
|
-
const siteId = generateSiteId();
|
|
59
|
-
|
|
45
|
+
async function scanCodebase(codebasePath) {
|
|
60
46
|
console.log(' Scanning files...');
|
|
61
47
|
|
|
62
48
|
// Get files to scan
|
|
@@ -70,9 +56,9 @@ async function analyzeCodebase(codebasePath, siteName) {
|
|
|
70
56
|
|
|
71
57
|
// Prioritize and limit files
|
|
72
58
|
const prioritizedFiles = prioritizeFiles(files, codebasePath);
|
|
73
|
-
const filesToAnalyze = prioritizedFiles.slice(0, 50); // Limit
|
|
59
|
+
const filesToAnalyze = prioritizedFiles.slice(0, 50); // Limit for performance
|
|
74
60
|
|
|
75
|
-
console.log(`
|
|
61
|
+
console.log(` Preparing ${filesToAnalyze.length} priority files...`);
|
|
76
62
|
|
|
77
63
|
// Read and prepare file contents
|
|
78
64
|
const fileContents = [];
|
|
@@ -81,7 +67,7 @@ async function analyzeCodebase(codebasePath, siteName) {
|
|
|
81
67
|
const content = fs.readFileSync(file, 'utf-8');
|
|
82
68
|
const relativePath = path.relative(codebasePath, file);
|
|
83
69
|
|
|
84
|
-
// Limit content per file
|
|
70
|
+
// Limit content per file
|
|
85
71
|
const truncatedContent = content.slice(0, 3000);
|
|
86
72
|
|
|
87
73
|
fileContents.push({
|
|
@@ -93,19 +79,7 @@ async function analyzeCodebase(codebasePath, siteName) {
|
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
console.log(' Calling LLM for analysis...');
|
|
98
|
-
const analysis = await analyzeWithLLM(fileContents, siteName);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
siteId,
|
|
102
|
-
siteName,
|
|
103
|
-
analyzedAt: new Date().toISOString(),
|
|
104
|
-
framework: analysis.framework,
|
|
105
|
-
routes: analysis.routes || [],
|
|
106
|
-
forms: analysis.forms || [],
|
|
107
|
-
actions: analysis.actions || []
|
|
108
|
-
};
|
|
82
|
+
return fileContents;
|
|
109
83
|
}
|
|
110
84
|
|
|
111
85
|
/**
|
|
@@ -125,71 +99,4 @@ function prioritizeFiles(files, basePath) {
|
|
|
125
99
|
});
|
|
126
100
|
}
|
|
127
101
|
|
|
128
|
-
|
|
129
|
-
* Analyze files using Claude
|
|
130
|
-
*/
|
|
131
|
-
async function analyzeWithLLM(fileContents, siteName) {
|
|
132
|
-
const filesText = fileContents.map(f =>
|
|
133
|
-
`=== ${f.path} ===\n${f.content}`
|
|
134
|
-
).join('\n\n');
|
|
135
|
-
|
|
136
|
-
const prompt = `Analyze this codebase and extract API information.
|
|
137
|
-
|
|
138
|
-
CODEBASE FILES:
|
|
139
|
-
${filesText}
|
|
140
|
-
|
|
141
|
-
Extract and return as JSON:
|
|
142
|
-
1. framework: detected framework (express, laravel, django, fastapi, nextjs, wordpress, etc.) or null
|
|
143
|
-
2. routes: array of {method, path, description} - API endpoints found
|
|
144
|
-
3. forms: array of {action, method, fields: [{name, type}]} - forms found
|
|
145
|
-
4. actions: array of {name, endpoint, method, description} - API calls/actions found
|
|
146
|
-
|
|
147
|
-
Focus on:
|
|
148
|
-
- REST API routes
|
|
149
|
-
- Form submissions
|
|
150
|
-
- AJAX/fetch calls
|
|
151
|
-
- Controller methods
|
|
152
|
-
|
|
153
|
-
Return ONLY valid JSON, no markdown or explanation:
|
|
154
|
-
{"framework": "...", "routes": [...], "forms": [...], "actions": [...]}`;
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const response = await anthropic.messages.create({
|
|
158
|
-
model: 'claude-sonnet-4-20250514',
|
|
159
|
-
max_tokens: 4096,
|
|
160
|
-
messages: [{
|
|
161
|
-
role: 'user',
|
|
162
|
-
content: prompt
|
|
163
|
-
}]
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
167
|
-
|
|
168
|
-
// Extract JSON from response
|
|
169
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
170
|
-
if (jsonMatch) {
|
|
171
|
-
return JSON.parse(jsonMatch[0]);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return { framework: null, routes: [], forms: [], actions: [] };
|
|
175
|
-
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (error.status === 429) {
|
|
178
|
-
console.log(' ā ļø Rate limited, waiting 30s...');
|
|
179
|
-
await new Promise(r => setTimeout(r, 30000));
|
|
180
|
-
return analyzeWithLLM(fileContents, siteName);
|
|
181
|
-
}
|
|
182
|
-
console.error(' LLM error:', error.message);
|
|
183
|
-
return { framework: null, routes: [], forms: [], actions: [] };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Generate a unique site ID
|
|
189
|
-
*/
|
|
190
|
-
function generateSiteId() {
|
|
191
|
-
const random = crypto.randomBytes(4).toString('hex');
|
|
192
|
-
return `site_${random}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
module.exports = { analyzeCodebase };
|
|
102
|
+
module.exports = { scanCodebase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merlean/analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI Bot codebase analyzer - generates site maps for AI assistant integration",
|
|
5
5
|
"keywords": ["ai", "bot", "analyzer", "claude", "anthropic", "widget"],
|
|
6
6
|
"author": "zmaren",
|
|
@@ -38,8 +38,6 @@
|
|
|
38
38
|
"node": ">=18.0.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
|
-
"dotenv": "^16.3.1",
|
|
43
41
|
"glob": "^10.3.10"
|
|
44
42
|
},
|
|
45
43
|
"devDependencies": {
|
package/lib/uploader.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upload site map to AI Bot backend
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
async function uploadMap(backendUrl, siteMap, token) {
|
|
6
|
-
try {
|
|
7
|
-
const url = `${backendUrl.replace(/\/$/, '')}/api/sites`;
|
|
8
|
-
|
|
9
|
-
const headers = {
|
|
10
|
-
'Content-Type': 'application/json'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
if (token) {
|
|
14
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const response = await fetch(url, {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers,
|
|
20
|
-
body: JSON.stringify(siteMap)
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
const error = await response.text();
|
|
25
|
-
return { success: false, error: `HTTP ${response.status}: ${error}` };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
return { success: true, data };
|
|
30
|
-
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return { success: false, error: error.message };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = { uploadMap };
|
|
37
|
-
|