skannr 0.1.1
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/.eslintrc.js +22 -0
- package/README.md +109 -0
- package/dist/agent-cli.d.ts +7 -0
- package/dist/agent-cli.d.ts.map +1 -0
- package/dist/agent-cli.js +364 -0
- package/dist/agent-cli.js.map +1 -0
- package/dist/agent.d.ts +115 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +340 -0
- package/dist/agent.js.map +1 -0
- package/dist/benchmark.d.ts +53 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +307 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/cache.d.ts +97 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +284 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +238 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +52 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +176 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/GenericAdapter.d.ts +10 -0
- package/dist/languages/GenericAdapter.d.ts.map +1 -0
- package/dist/languages/GenericAdapter.js +41 -0
- package/dist/languages/GenericAdapter.js.map +1 -0
- package/dist/languages/LanguageAdapter.d.ts +14 -0
- package/dist/languages/LanguageAdapter.d.ts.map +1 -0
- package/dist/languages/LanguageAdapter.js +3 -0
- package/dist/languages/LanguageAdapter.js.map +1 -0
- package/dist/languages/PythonAdapter.d.ts +10 -0
- package/dist/languages/PythonAdapter.d.ts.map +1 -0
- package/dist/languages/PythonAdapter.js +98 -0
- package/dist/languages/PythonAdapter.js.map +1 -0
- package/dist/languages/TypeScriptAdapter.d.ts +17 -0
- package/dist/languages/TypeScriptAdapter.d.ts.map +1 -0
- package/dist/languages/TypeScriptAdapter.js +321 -0
- package/dist/languages/TypeScriptAdapter.js.map +1 -0
- package/dist/languages/registry.d.ts +4 -0
- package/dist/languages/registry.d.ts.map +1 -0
- package/dist/languages/registry.js +86 -0
- package/dist/languages/registry.js.map +1 -0
- package/dist/mapper.d.ts +49 -0
- package/dist/mapper.d.ts.map +1 -0
- package/dist/mapper.js +386 -0
- package/dist/mapper.js.map +1 -0
- package/dist/ranker-enhanced.d.ts +37 -0
- package/dist/ranker-enhanced.d.ts.map +1 -0
- package/dist/ranker-enhanced.js +395 -0
- package/dist/ranker-enhanced.js.map +1 -0
- package/dist/ranker.d.ts +14 -0
- package/dist/ranker.d.ts.map +1 -0
- package/dist/ranker.js +105 -0
- package/dist/ranker.js.map +1 -0
- package/dist/rocket-chat-scope.d.ts +7 -0
- package/dist/rocket-chat-scope.d.ts.map +1 -0
- package/dist/rocket-chat-scope.js +95 -0
- package/dist/rocket-chat-scope.js.map +1 -0
- package/dist/scanner.d.ts +16 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +228 -0
- package/dist/scanner.js.map +1 -0
- package/dist/skeletonizer.d.ts +5 -0
- package/dist/skeletonizer.d.ts.map +1 -0
- package/dist/skeletonizer.js +52 -0
- package/dist/skeletonizer.js.map +1 -0
- package/dist/tokenizer.d.ts +9 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +21 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/gemini-extension/GEMINI.md +26 -0
- package/gemini-extension/gemini-extension.json +12 -0
- package/gemini-extension/package.json +14 -0
- package/gemini-extension/src/server.ts +63 -0
- package/gemini-extension/tsconfig.json +14 -0
- package/jest.config.js +5 -0
- package/package.json +46 -0
- package/src/agent-cli.ts +383 -0
- package/src/agent.ts +344 -0
- package/src/benchmark.ts +389 -0
- package/src/cache.ts +317 -0
- package/src/cli.ts +223 -0
- package/src/config.ts +22 -0
- package/src/index.ts +215 -0
- package/src/languages/GenericAdapter.ts +44 -0
- package/src/languages/LanguageAdapter.ts +14 -0
- package/src/languages/PythonAdapter.ts +74 -0
- package/src/languages/TypeScriptAdapter.ts +338 -0
- package/src/languages/registry.ts +49 -0
- package/src/mapper.ts +448 -0
- package/src/ranker-enhanced.ts +460 -0
- package/src/ranker.ts +92 -0
- package/src/scanner.ts +201 -0
- package/src/skeletonizer.ts +16 -0
- package/src/tokenizer.ts +20 -0
- package/src/types.ts +71 -0
- package/tests/agent.tools.test.ts +81 -0
- package/tests/benchmark.test.ts +31 -0
- package/tests/fixtures/sample.py +17 -0
- package/tests/fixtures/sample.ts +13 -0
- package/tests/fixtures/src/api/routes.ts +1 -0
- package/tests/fixtures/src/auth/permission.ts +3 -0
- package/tests/ranker-enhanced.test.ts +68 -0
- package/tests/ranker.test.ts +75 -0
- package/tests/scanner.scope.test.ts +41 -0
- package/tests/setup-fixtures.js +29 -0
- package/tests/skeletonizer.test.ts +142 -0
- package/tsconfig.json +21 -0
- package/uca-landing/index.html +17 -0
- package/uca-landing/package.json +23 -0
- package/uca-landing/postcss.config.js +6 -0
- package/uca-landing/src/App.jsx +43 -0
- package/uca-landing/src/components/AgentMode.jsx +45 -0
- package/uca-landing/src/components/CliReference.jsx +49 -0
- package/uca-landing/src/components/Features.jsx +63 -0
- package/uca-landing/src/components/Footer.jsx +35 -0
- package/uca-landing/src/components/Hero.jsx +124 -0
- package/uca-landing/src/components/HowItWorks.jsx +60 -0
- package/uca-landing/src/components/Install.jsx +90 -0
- package/uca-landing/src/components/LanguageSupport.jsx +63 -0
- package/uca-landing/src/components/Navbar.jsx +86 -0
- package/uca-landing/src/components/Problem.jsx +51 -0
- package/uca-landing/src/components/Reveal.jsx +40 -0
- package/uca-landing/src/components/WorksWith.jsx +59 -0
- package/uca-landing/src/hooks/useScrollNav.js +13 -0
- package/uca-landing/src/hooks/useTypewriter.js +41 -0
- package/uca-landing/src/index.css +13 -0
- package/uca-landing/src/main.jsx +10 -0
- package/uca-landing/tailwind.config.js +68 -0
- package/uca-landing/vite.config.js +6 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for skannr
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { analyzeProject, getCacheManager } from './index';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { loadConfig } from './config';
|
|
10
|
+
import { discoverModules } from './scanner';
|
|
11
|
+
|
|
12
|
+
interface CliArgs {
|
|
13
|
+
root: string;
|
|
14
|
+
question: string;
|
|
15
|
+
limit: number;
|
|
16
|
+
limitProvided: boolean;
|
|
17
|
+
generateMapping: boolean;
|
|
18
|
+
mappingOutputPath?: string;
|
|
19
|
+
moduleKeys?: string[];
|
|
20
|
+
lang: 'typescript' | 'javascript' | 'python' | 'auto';
|
|
21
|
+
skipCache?: boolean;
|
|
22
|
+
cacheCommand?: 'clear' | 'stats';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse command line arguments
|
|
27
|
+
*/
|
|
28
|
+
function parseArgs(): CliArgs | null {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
|
|
31
|
+
let root = process.cwd();
|
|
32
|
+
let question = '';
|
|
33
|
+
let limit = 10;
|
|
34
|
+
let limitProvided = false;
|
|
35
|
+
let generateMapping = false;
|
|
36
|
+
let mappingOutputPath: string | undefined;
|
|
37
|
+
let moduleKeys: string[] | undefined;
|
|
38
|
+
let lang: 'typescript' | 'javascript' | 'python' | 'auto' = 'auto';
|
|
39
|
+
let skipCache = false;
|
|
40
|
+
let cacheCommand: 'clear' | 'stats' | undefined;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
|
|
45
|
+
if (arg === '--root' && i + 1 < args.length) {
|
|
46
|
+
root = args[i + 1];
|
|
47
|
+
i++;
|
|
48
|
+
} else if (arg === '--question' && i + 1 < args.length) {
|
|
49
|
+
question = args[i + 1];
|
|
50
|
+
i++;
|
|
51
|
+
} else if (arg === '--limit' && i + 1 < args.length) {
|
|
52
|
+
limit = parseInt(args[i + 1], 10);
|
|
53
|
+
if (isNaN(limit) || limit < 1) {
|
|
54
|
+
console.error('Error: --limit must be a positive number');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
limitProvided = true;
|
|
58
|
+
i++;
|
|
59
|
+
} else if (arg === '--with-mapping') {
|
|
60
|
+
generateMapping = true;
|
|
61
|
+
} else if (arg === '--mapping-output' && i + 1 < args.length) {
|
|
62
|
+
mappingOutputPath = args[i + 1];
|
|
63
|
+
generateMapping = true; // Implies mapping generation
|
|
64
|
+
i++;
|
|
65
|
+
} else if (arg === '--modules' && i + 1 < args.length) {
|
|
66
|
+
moduleKeys = args[i + 1].split(',').map(k => k.trim());
|
|
67
|
+
i++;
|
|
68
|
+
} else if (arg === '--lang' && i + 1 < args.length) {
|
|
69
|
+
const langArg = args[i + 1].toLowerCase();
|
|
70
|
+
if (langArg !== 'typescript' && langArg !== 'javascript' && langArg !== 'python' && langArg !== 'auto') {
|
|
71
|
+
console.error('Error: --lang must be one of: typescript, javascript, python, auto');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
lang = langArg as CliArgs['lang'];
|
|
75
|
+
i++;
|
|
76
|
+
} else if (arg === '--skip-cache') {
|
|
77
|
+
skipCache = true;
|
|
78
|
+
} else if (arg === '--cache-clear') {
|
|
79
|
+
cacheCommand = 'clear';
|
|
80
|
+
} else if (arg === '--cache-stats') {
|
|
81
|
+
cacheCommand = 'stats';
|
|
82
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
83
|
+
printHelp();
|
|
84
|
+
return null;
|
|
85
|
+
} else {
|
|
86
|
+
console.error(`Unknown argument: ${arg}`);
|
|
87
|
+
printHelp();
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle cache commands
|
|
93
|
+
if (cacheCommand) {
|
|
94
|
+
return { root, question: '', limit, limitProvided, generateMapping, moduleKeys, lang, cacheCommand };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!question) {
|
|
98
|
+
console.error('Error: --question is required (or use --cache-* for cache management)\n');
|
|
99
|
+
printHelp();
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { root, question, limit, limitProvided, generateMapping, mappingOutputPath, moduleKeys, lang, skipCache };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Print help message
|
|
108
|
+
*/
|
|
109
|
+
function printHelp(): void {
|
|
110
|
+
console.log(`
|
|
111
|
+
uca - Universal Code Analyzer
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
uca --question "<text>" [options]
|
|
115
|
+
uca --cache-clear # Clear all caches
|
|
116
|
+
uca --cache-stats # Show cache statistics
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
--root <path> Project root directory (default: current directory)
|
|
120
|
+
--question "<text>" Natural language question about the codebase (required)
|
|
121
|
+
--limit <number> Number of top files to analyze (default: 10)
|
|
122
|
+
--with-mapping Generate symbol mapping for on-demand retrieval
|
|
123
|
+
--modules <keys> Comma-separated module keys (auto-discovered when omitted)
|
|
124
|
+
--lang <mode> typescript | javascript | python | auto (default: auto)
|
|
125
|
+
--mapping-output <path> Custom path for mapping file (implies --with-mapping)
|
|
126
|
+
--skip-cache Skip cache and force full analysis
|
|
127
|
+
--cache-clear Clear all cached analysis results
|
|
128
|
+
--cache-stats Show cache statistics (hits, misses, size)
|
|
129
|
+
--help, -h Show this help message
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
uca --question "authentication logic" --limit 5
|
|
133
|
+
uca --root ./my-project --question "database models"
|
|
134
|
+
uca --question "API endpoints" --with-mapping
|
|
135
|
+
uca --question "permissions" --modules auth,middleware --skip-cache
|
|
136
|
+
uca --question "class structure" --lang python
|
|
137
|
+
uca --cache-stats
|
|
138
|
+
uca --cache-clear
|
|
139
|
+
|
|
140
|
+
Output:
|
|
141
|
+
JSON object with ranked files and their code skeletons
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Main CLI function
|
|
147
|
+
*/
|
|
148
|
+
async function main(): Promise<void> {
|
|
149
|
+
const args = parseArgs();
|
|
150
|
+
|
|
151
|
+
if (!args) {
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Handle cache commands
|
|
157
|
+
if (args.cacheCommand === 'clear') {
|
|
158
|
+
const cacheManager = getCacheManager();
|
|
159
|
+
cacheManager.clear();
|
|
160
|
+
console.log('✅ Cache cleared successfully');
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (args.cacheCommand === 'stats') {
|
|
165
|
+
const cacheManager = getCacheManager();
|
|
166
|
+
const stats = cacheManager.getStats();
|
|
167
|
+
console.log('\n📊 Cache Statistics:');
|
|
168
|
+
console.log(` Hits: ${stats.hits}`);
|
|
169
|
+
console.log(` Misses: ${stats.misses}`);
|
|
170
|
+
console.log(` Hit Rate: ${stats.hitRate}%`);
|
|
171
|
+
console.log(` Cache Size: ${(stats.cacheSize / 1024).toFixed(2)} KB`);
|
|
172
|
+
console.log(` Cache Dir: ${cacheManager.getCacheDir()}\n`);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const absoluteRoot = path.resolve(args.root);
|
|
177
|
+
const config = loadConfig(absoluteRoot);
|
|
178
|
+
const moduleKeys =
|
|
179
|
+
args.moduleKeys
|
|
180
|
+
?? (config.modules ? Object.keys(config.modules) : discoverModules(absoluteRoot));
|
|
181
|
+
|
|
182
|
+
const resolvedLimit = args.limitProvided ? args.limit : (config.defaultLimit ?? args.limit);
|
|
183
|
+
const langExtensions = resolveExtensionsForLanguage(args.lang);
|
|
184
|
+
const extensions = langExtensions ?? config.extensions;
|
|
185
|
+
|
|
186
|
+
const result = await analyzeProject({
|
|
187
|
+
root: absoluteRoot,
|
|
188
|
+
question: args.question,
|
|
189
|
+
limit: resolvedLimit,
|
|
190
|
+
generateMapping: args.generateMapping,
|
|
191
|
+
mappingOutputPath: args.mappingOutputPath,
|
|
192
|
+
moduleKeys,
|
|
193
|
+
moduleDefinitions: config.modules,
|
|
194
|
+
lang: args.lang,
|
|
195
|
+
skipCache: args.skipCache,
|
|
196
|
+
exclude: config.exclude,
|
|
197
|
+
extensions,
|
|
198
|
+
});
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('Error analyzing project:', error instanceof Error ? error.message : error);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Run CLI
|
|
207
|
+
main().catch((error) => {
|
|
208
|
+
console.error('Unexpected error:', error);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
function resolveExtensionsForLanguage(lang: CliArgs['lang']): string[] | undefined {
|
|
213
|
+
switch (lang) {
|
|
214
|
+
case 'typescript':
|
|
215
|
+
case 'javascript':
|
|
216
|
+
return ['.ts', '.tsx', '.js', '.jsx'];
|
|
217
|
+
case 'python':
|
|
218
|
+
return ['.py', '.pyi'];
|
|
219
|
+
case 'auto':
|
|
220
|
+
default:
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface AnalyzerConfig {
|
|
5
|
+
modules?: Record<string, string[]>;
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
extensions?: string[];
|
|
8
|
+
defaultLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadConfig(root: string): AnalyzerConfig {
|
|
12
|
+
const configPath = path.join(root, 'code-analyzer.config.json');
|
|
13
|
+
if (fs.existsSync(configPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
16
|
+
return JSON.parse(raw) as AnalyzerConfig;
|
|
17
|
+
} catch {
|
|
18
|
+
console.warn('[config] Failed to parse code-analyzer.config.json, using defaults');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main library exports for skannr
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { discoverModules, readFileContent, scanFiles, scanRocketChatFiles, scanTypeScriptFiles } from './scanner';
|
|
7
|
+
import { rankFiles } from './ranker';
|
|
8
|
+
import { rankFilesEnhanced } from './ranker-enhanced';
|
|
9
|
+
import { buildSkeletonForFile } from './skeletonizer';
|
|
10
|
+
import { countTokens } from './tokenizer';
|
|
11
|
+
import {
|
|
12
|
+
buildSkeletonWithMapping,
|
|
13
|
+
saveMappingToFile,
|
|
14
|
+
type SymbolMapping,
|
|
15
|
+
} from './mapper';
|
|
16
|
+
import {
|
|
17
|
+
AnalyzeOptions,
|
|
18
|
+
AnalysisResult,
|
|
19
|
+
FileAnalysis,
|
|
20
|
+
} from './types';
|
|
21
|
+
import { getCacheManager, type CacheConfig } from './cache';
|
|
22
|
+
import { detectRepoLanguages, getAdapter } from './languages/registry';
|
|
23
|
+
|
|
24
|
+
export async function analyzeProject(
|
|
25
|
+
options?: AnalyzeOptions & {
|
|
26
|
+
skipCache?: boolean;
|
|
27
|
+
cacheConfig?: Partial<CacheConfig>;
|
|
28
|
+
}
|
|
29
|
+
): Promise<AnalysisResult> {
|
|
30
|
+
if (!options) {
|
|
31
|
+
throw new Error('Options are required (must include root and question)');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
root,
|
|
36
|
+
question,
|
|
37
|
+
limit = 10,
|
|
38
|
+
generateMapping = false,
|
|
39
|
+
mappingOutputPath,
|
|
40
|
+
enhancedRanking = false,
|
|
41
|
+
moduleKeys,
|
|
42
|
+
moduleDefinitions,
|
|
43
|
+
exclude,
|
|
44
|
+
extensions,
|
|
45
|
+
lang = 'auto',
|
|
46
|
+
skipCache = false,
|
|
47
|
+
cacheConfig,
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
const absoluteRoot = path.resolve(root);
|
|
51
|
+
const effectiveModuleKeys =
|
|
52
|
+
moduleKeys && moduleKeys.length > 0 ? moduleKeys : discoverModules(absoluteRoot);
|
|
53
|
+
const effectiveExtensions = resolveExtensions(absoluteRoot, lang, extensions);
|
|
54
|
+
|
|
55
|
+
const allFiles = scanFiles(absoluteRoot, {
|
|
56
|
+
extensions: effectiveExtensions,
|
|
57
|
+
exclude,
|
|
58
|
+
moduleKeys: effectiveModuleKeys,
|
|
59
|
+
moduleDefinitions,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (allFiles.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
question,
|
|
65
|
+
root: absoluteRoot,
|
|
66
|
+
limit,
|
|
67
|
+
files: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cacheManager = cacheConfig ? getCacheManager() : getCacheManager();
|
|
72
|
+
if (cacheConfig) {
|
|
73
|
+
// cache manager behavior intentionally unchanged
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!skipCache) {
|
|
77
|
+
const cachedResult = cacheManager.get(
|
|
78
|
+
absoluteRoot,
|
|
79
|
+
question,
|
|
80
|
+
allFiles,
|
|
81
|
+
effectiveModuleKeys,
|
|
82
|
+
enhancedRanking,
|
|
83
|
+
limit
|
|
84
|
+
);
|
|
85
|
+
if (cachedResult) {
|
|
86
|
+
console.log('📦 Cache hit! Using cached analysis result');
|
|
87
|
+
return cachedResult;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rankedFiles = enhancedRanking
|
|
92
|
+
? rankFilesEnhanced(allFiles, question, limit, absoluteRoot)
|
|
93
|
+
: rankFiles(allFiles, question, limit);
|
|
94
|
+
|
|
95
|
+
const fileAnalyses: FileAnalysis[] = [];
|
|
96
|
+
const symbolMapping: SymbolMapping = {
|
|
97
|
+
generatedAt: new Date().toISOString(),
|
|
98
|
+
rootPath: absoluteRoot,
|
|
99
|
+
files: {},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const rankedFile of rankedFiles) {
|
|
103
|
+
const originalContent = readFileContent(rankedFile.path);
|
|
104
|
+
if (!originalContent) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const relativePath = path.relative(absoluteRoot, rankedFile.path);
|
|
109
|
+
const adapter = getAdapter(rankedFile.path);
|
|
110
|
+
|
|
111
|
+
let skeleton: string;
|
|
112
|
+
if (generateMapping && adapter.name === 'typescript') {
|
|
113
|
+
const mappingResult = buildSkeletonWithMapping(rankedFile.path, absoluteRoot);
|
|
114
|
+
skeleton = mappingResult.skeleton;
|
|
115
|
+
if (mappingResult.symbols.length > 0) {
|
|
116
|
+
symbolMapping.files[relativePath] = {
|
|
117
|
+
originalPath: rankedFile.path,
|
|
118
|
+
symbols: mappingResult.symbols,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
skeleton = buildSkeletonForFile(rankedFile.path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fileAnalyses.push({
|
|
126
|
+
path: relativePath,
|
|
127
|
+
score: rankedFile.score,
|
|
128
|
+
skeleton,
|
|
129
|
+
originalTokenCount: countTokens(originalContent),
|
|
130
|
+
skeletonTokenCount: countTokens(skeleton),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result: AnalysisResult = {
|
|
135
|
+
question,
|
|
136
|
+
root: absoluteRoot,
|
|
137
|
+
limit,
|
|
138
|
+
files: fileAnalyses,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (!skipCache) {
|
|
142
|
+
cacheManager.set(
|
|
143
|
+
absoluteRoot,
|
|
144
|
+
question,
|
|
145
|
+
allFiles,
|
|
146
|
+
result,
|
|
147
|
+
effectiveModuleKeys,
|
|
148
|
+
enhancedRanking,
|
|
149
|
+
limit
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (generateMapping && Object.keys(symbolMapping.files).length > 0) {
|
|
154
|
+
const mappingPath = mappingOutputPath || path.join(absoluteRoot, 'code-analyzer.mapping.json');
|
|
155
|
+
saveMappingToFile(symbolMapping, mappingPath);
|
|
156
|
+
console.log(`Symbol mapping saved to: ${mappingPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveExtensions(
|
|
163
|
+
root: string,
|
|
164
|
+
lang: AnalyzeOptions['lang'],
|
|
165
|
+
explicit?: string[]
|
|
166
|
+
): string[] | undefined {
|
|
167
|
+
if (explicit && explicit.length > 0) {
|
|
168
|
+
return explicit.map((ext) => ext.toLowerCase());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (lang === 'typescript' || lang === 'javascript') {
|
|
172
|
+
return ['.ts', '.tsx', '.js', '.jsx'];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (lang === 'python') {
|
|
176
|
+
return ['.py', '.pyi'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (lang === 'auto') {
|
|
180
|
+
detectRepoLanguages(root);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { rankFiles } from './ranker';
|
|
188
|
+
export { rankFilesEnhanced } from './ranker-enhanced';
|
|
189
|
+
export { buildSkeletonForFile } from './skeletonizer';
|
|
190
|
+
export { scanTypeScriptFiles, scanRocketChatFiles, scanFiles, discoverModules } from './scanner';
|
|
191
|
+
export { countTokens } from './tokenizer';
|
|
192
|
+
export {
|
|
193
|
+
buildSkeletonWithMapping,
|
|
194
|
+
getSymbolDetails,
|
|
195
|
+
saveMappingToFile,
|
|
196
|
+
loadMappingFromFile,
|
|
197
|
+
type SymbolMapping,
|
|
198
|
+
type SymbolLocation,
|
|
199
|
+
} from './mapper';
|
|
200
|
+
|
|
201
|
+
export type {
|
|
202
|
+
AnalyzeOptions,
|
|
203
|
+
AnalysisResult,
|
|
204
|
+
FileAnalysis,
|
|
205
|
+
RankedFile,
|
|
206
|
+
} from './types';
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
CacheManager,
|
|
210
|
+
getCacheManager,
|
|
211
|
+
initializeCacheManager,
|
|
212
|
+
DEFAULT_CACHE_CONFIG,
|
|
213
|
+
type CacheConfig,
|
|
214
|
+
type CacheEntry,
|
|
215
|
+
} from './cache';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LanguageAdapter, Symbol } from './LanguageAdapter';
|
|
2
|
+
|
|
3
|
+
export class GenericAdapter implements LanguageAdapter {
|
|
4
|
+
name = 'generic';
|
|
5
|
+
extensions: string[] = [];
|
|
6
|
+
|
|
7
|
+
canHandle(): boolean {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
generateSkeleton(content: string): string {
|
|
12
|
+
return content.split('\n').slice(0, 50).join('\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
extractSymbols(content: string): Symbol[] {
|
|
16
|
+
const symbols: Symbol[] = [];
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
const seen = new Set<string>();
|
|
19
|
+
|
|
20
|
+
lines.forEach((line, idx) => {
|
|
21
|
+
const matches = line.match(/\b([A-Z][A-Za-z0-9]+|[a-z]+_[a-z0-9_]+)\b/g) || [];
|
|
22
|
+
for (const match of matches) {
|
|
23
|
+
if (seen.has(match)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seen.add(match);
|
|
27
|
+
symbols.push({
|
|
28
|
+
name: match,
|
|
29
|
+
kind: 'variable',
|
|
30
|
+
line: idx + 1,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return symbols;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
extractImports(content: string): string[] {
|
|
39
|
+
const lines = content.split('\n').slice(0, 100);
|
|
40
|
+
return lines
|
|
41
|
+
.filter((line) => /^\s*(import|from|#include)\b/.test(line))
|
|
42
|
+
.map((line) => line.trim());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Symbol {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'export';
|
|
4
|
+
line: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LanguageAdapter {
|
|
8
|
+
name: string;
|
|
9
|
+
extensions: string[];
|
|
10
|
+
canHandle(filePath: string): boolean;
|
|
11
|
+
generateSkeleton(content: string, filePath: string): string;
|
|
12
|
+
extractSymbols(content: string): Symbol[];
|
|
13
|
+
extractImports(content: string): string[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { LanguageAdapter, Symbol } from './LanguageAdapter';
|
|
3
|
+
|
|
4
|
+
export class PythonAdapter implements LanguageAdapter {
|
|
5
|
+
name = 'python';
|
|
6
|
+
extensions = ['.py', '.pyi'];
|
|
7
|
+
|
|
8
|
+
canHandle(filePath: string): boolean {
|
|
9
|
+
return this.extensions.includes(path.extname(filePath).toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
generateSkeleton(content: string): string {
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
const output: string[] = [];
|
|
15
|
+
const keepers = /^(?:\s*(?:@[\w.]+(?:\([^)]*\))?|(?:async\s+)?def\s+[\w_]+\s*\(.*\)\s*(?:->\s*[^:]+)?\s*:|class\s+[\w_]+(?:\([^)]*\))?\s*:|(?:from\s+\S+\s+import\s+.+)|(?:import\s+.+)|[A-Za-z_]\w*\s*=.+))/;
|
|
16
|
+
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (!keepers.test(line)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (/^\s*(?:async\s+)?def\s+/.test(line)) {
|
|
23
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? '';
|
|
24
|
+
output.push(line);
|
|
25
|
+
output.push(`${indent} ...`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
output.push(line);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return output.join('\n').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
extractSymbols(content: string): Symbol[] {
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
const symbols: Symbol[] = [];
|
|
38
|
+
|
|
39
|
+
lines.forEach((line, idx) => {
|
|
40
|
+
const classMatch = line.match(/^\s*class\s+([A-Za-z_]\w*)/);
|
|
41
|
+
if (classMatch) {
|
|
42
|
+
symbols.push({ name: classMatch[1], kind: 'class', line: idx + 1 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const functionMatch = line.match(/^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)/);
|
|
46
|
+
if (functionMatch) {
|
|
47
|
+
symbols.push({ name: functionMatch[1], kind: 'function', line: idx + 1 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const variableMatch = line.match(/^([A-Za-z_]\w*)\s*=/);
|
|
51
|
+
if (variableMatch) {
|
|
52
|
+
symbols.push({ name: variableMatch[1], kind: 'variable', line: idx + 1 });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return symbols;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
extractImports(content: string): string[] {
|
|
60
|
+
const imports: string[] = [];
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const fromMatch = line.match(/^\s*from\s+([A-Za-z0-9_.]+)\s+import\s+/);
|
|
64
|
+
if (fromMatch) {
|
|
65
|
+
imports.push(fromMatch[1]);
|
|
66
|
+
}
|
|
67
|
+
const importMatch = line.match(/^\s*import\s+(.+)/);
|
|
68
|
+
if (importMatch) {
|
|
69
|
+
imports.push(importMatch[1].split(',')[0].trim());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return imports;
|
|
73
|
+
}
|
|
74
|
+
}
|