ghostpatch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/__tests__/detectors.test.ts +224 -0
  4. package/__tests__/rules.test.ts +117 -0
  5. package/__tests__/scanner.test.ts +222 -0
  6. package/dist/ai/anthropic.d.ts +11 -0
  7. package/dist/ai/anthropic.d.ts.map +1 -0
  8. package/dist/ai/anthropic.js +76 -0
  9. package/dist/ai/anthropic.js.map +1 -0
  10. package/dist/ai/huggingface.d.ts +12 -0
  11. package/dist/ai/huggingface.d.ts.map +1 -0
  12. package/dist/ai/huggingface.js +95 -0
  13. package/dist/ai/huggingface.js.map +1 -0
  14. package/dist/ai/openai.d.ts +11 -0
  15. package/dist/ai/openai.d.ts.map +1 -0
  16. package/dist/ai/openai.js +71 -0
  17. package/dist/ai/openai.js.map +1 -0
  18. package/dist/ai/prompts.d.ts +5 -0
  19. package/dist/ai/prompts.d.ts.map +1 -0
  20. package/dist/ai/prompts.js +101 -0
  21. package/dist/ai/prompts.js.map +1 -0
  22. package/dist/ai/provider.d.ts +9 -0
  23. package/dist/ai/provider.d.ts.map +1 -0
  24. package/dist/ai/provider.js +66 -0
  25. package/dist/ai/provider.js.map +1 -0
  26. package/dist/cli/index.d.ts +3 -0
  27. package/dist/cli/index.d.ts.map +1 -0
  28. package/dist/cli/index.js +318 -0
  29. package/dist/cli/index.js.map +1 -0
  30. package/dist/core/reporter.d.ts +7 -0
  31. package/dist/core/reporter.d.ts.map +1 -0
  32. package/dist/core/reporter.js +366 -0
  33. package/dist/core/reporter.js.map +1 -0
  34. package/dist/core/rules.d.ts +8 -0
  35. package/dist/core/rules.d.ts.map +1 -0
  36. package/dist/core/rules.js +1077 -0
  37. package/dist/core/rules.js.map +1 -0
  38. package/dist/core/scanner.d.ts +6 -0
  39. package/dist/core/scanner.d.ts.map +1 -0
  40. package/dist/core/scanner.js +217 -0
  41. package/dist/core/scanner.js.map +1 -0
  42. package/dist/core/severity.d.ts +100 -0
  43. package/dist/core/severity.d.ts.map +1 -0
  44. package/dist/core/severity.js +52 -0
  45. package/dist/core/severity.js.map +1 -0
  46. package/dist/detectors/auth.d.ts +3 -0
  47. package/dist/detectors/auth.d.ts.map +1 -0
  48. package/dist/detectors/auth.js +138 -0
  49. package/dist/detectors/auth.js.map +1 -0
  50. package/dist/detectors/crypto.d.ts +3 -0
  51. package/dist/detectors/crypto.d.ts.map +1 -0
  52. package/dist/detectors/crypto.js +128 -0
  53. package/dist/detectors/crypto.js.map +1 -0
  54. package/dist/detectors/dependency.d.ts +4 -0
  55. package/dist/detectors/dependency.d.ts.map +1 -0
  56. package/dist/detectors/dependency.js +267 -0
  57. package/dist/detectors/dependency.js.map +1 -0
  58. package/dist/detectors/deserialize.d.ts +3 -0
  59. package/dist/detectors/deserialize.d.ts.map +1 -0
  60. package/dist/detectors/deserialize.js +107 -0
  61. package/dist/detectors/deserialize.js.map +1 -0
  62. package/dist/detectors/injection.d.ts +3 -0
  63. package/dist/detectors/injection.d.ts.map +1 -0
  64. package/dist/detectors/injection.js +158 -0
  65. package/dist/detectors/injection.js.map +1 -0
  66. package/dist/detectors/misconfig.d.ts +3 -0
  67. package/dist/detectors/misconfig.d.ts.map +1 -0
  68. package/dist/detectors/misconfig.js +153 -0
  69. package/dist/detectors/misconfig.js.map +1 -0
  70. package/dist/detectors/pathtraversal.d.ts +3 -0
  71. package/dist/detectors/pathtraversal.d.ts.map +1 -0
  72. package/dist/detectors/pathtraversal.js +90 -0
  73. package/dist/detectors/pathtraversal.js.map +1 -0
  74. package/dist/detectors/prototype.d.ts +3 -0
  75. package/dist/detectors/prototype.d.ts.map +1 -0
  76. package/dist/detectors/prototype.js +79 -0
  77. package/dist/detectors/prototype.js.map +1 -0
  78. package/dist/detectors/secrets.d.ts +4 -0
  79. package/dist/detectors/secrets.d.ts.map +1 -0
  80. package/dist/detectors/secrets.js +137 -0
  81. package/dist/detectors/secrets.js.map +1 -0
  82. package/dist/detectors/ssrf.d.ts +3 -0
  83. package/dist/detectors/ssrf.d.ts.map +1 -0
  84. package/dist/detectors/ssrf.js +78 -0
  85. package/dist/detectors/ssrf.js.map +1 -0
  86. package/dist/detectors/zeroday.d.ts +9 -0
  87. package/dist/detectors/zeroday.d.ts.map +1 -0
  88. package/dist/detectors/zeroday.js +77 -0
  89. package/dist/detectors/zeroday.js.map +1 -0
  90. package/dist/index.d.ts +10 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +42 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/mcp/server.d.ts +2 -0
  95. package/dist/mcp/server.d.ts.map +1 -0
  96. package/dist/mcp/server.js +358 -0
  97. package/dist/mcp/server.js.map +1 -0
  98. package/dist/utils/config.d.ts +4 -0
  99. package/dist/utils/config.d.ts.map +1 -0
  100. package/dist/utils/config.js +97 -0
  101. package/dist/utils/config.js.map +1 -0
  102. package/dist/utils/fingerprint.d.ts +5 -0
  103. package/dist/utils/fingerprint.d.ts.map +1 -0
  104. package/dist/utils/fingerprint.js +55 -0
  105. package/dist/utils/fingerprint.js.map +1 -0
  106. package/dist/utils/languages.d.ts +8 -0
  107. package/dist/utils/languages.d.ts.map +1 -0
  108. package/dist/utils/languages.js +128 -0
  109. package/dist/utils/languages.js.map +1 -0
  110. package/package.json +53 -0
  111. package/src/ai/anthropic.ts +82 -0
  112. package/src/ai/huggingface.ts +111 -0
  113. package/src/ai/openai.ts +75 -0
  114. package/src/ai/prompts.ts +100 -0
  115. package/src/ai/provider.ts +68 -0
  116. package/src/cli/index.ts +314 -0
  117. package/src/core/reporter.ts +356 -0
  118. package/src/core/rules.ts +1089 -0
  119. package/src/core/scanner.ts +201 -0
  120. package/src/core/severity.ts +140 -0
  121. package/src/detectors/auth.ts +152 -0
  122. package/src/detectors/crypto.ts +128 -0
  123. package/src/detectors/dependency.ts +240 -0
  124. package/src/detectors/deserialize.ts +106 -0
  125. package/src/detectors/injection.ts +172 -0
  126. package/src/detectors/misconfig.ts +152 -0
  127. package/src/detectors/pathtraversal.ts +89 -0
  128. package/src/detectors/prototype.ts +77 -0
  129. package/src/detectors/secrets.ts +138 -0
  130. package/src/detectors/ssrf.ts +77 -0
  131. package/src/detectors/zeroday.ts +93 -0
  132. package/src/index.ts +24 -0
  133. package/src/mcp/server.ts +379 -0
  134. package/src/utils/config.ts +64 -0
  135. package/src/utils/fingerprint.ts +21 -0
  136. package/src/utils/languages.ts +95 -0
  137. package/tsconfig.json +20 -0
  138. package/vitest.config.ts +8 -0
@@ -0,0 +1,379 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { scan, scanFile } from '../core/scanner';
4
+ import { generateReport } from '../core/reporter';
5
+ import { Finding, Severity, ScanResult } from '../core/severity';
6
+ import { detectLanguage } from '../utils/languages';
7
+ import { getAvailableProvider } from '../ai/provider';
8
+ import { detectSecretsOnly } from '../detectors/secrets';
9
+ import { runNpmAudit } from '../detectors/dependency';
10
+
11
+ // MCP protocol types
12
+ interface MCPRequest {
13
+ jsonrpc: '2.0';
14
+ id: number | string;
15
+ method: string;
16
+ params?: any;
17
+ }
18
+
19
+ interface MCPResponse {
20
+ jsonrpc: '2.0';
21
+ id: number | string;
22
+ result?: any;
23
+ error?: { code: number; message: string };
24
+ }
25
+
26
+ // Cache for scan results
27
+ let lastScanResult: ScanResult | null = null;
28
+
29
+ const TOOLS = [
30
+ {
31
+ name: 'ghostpatch_scan',
32
+ description: 'Run a full security scan on a project directory. Returns findings with severity, CWE codes, and remediation advice.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ path: { type: 'string', description: 'Directory or file path to scan (default: current directory)' },
37
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity level to report' },
38
+ output: { type: 'string', enum: ['json', 'terminal', 'sarif'], description: 'Output format' },
39
+ },
40
+ },
41
+ },
42
+ {
43
+ name: 'ghostpatch_scan_file',
44
+ description: 'Scan a single file for security vulnerabilities.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ path: { type: 'string', description: 'Path to the file to scan' },
49
+ content: { type: 'string', description: 'File content (if not reading from disk)' },
50
+ },
51
+ required: ['path'],
52
+ },
53
+ },
54
+ {
55
+ name: 'ghostpatch_findings',
56
+ description: 'Get current scan findings with optional filters.',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'] },
61
+ file: { type: 'string', description: 'Filter by file path (substring match)' },
62
+ ruleId: { type: 'string', description: 'Filter by rule ID' },
63
+ limit: { type: 'number', description: 'Maximum results to return', default: 50 },
64
+ },
65
+ },
66
+ },
67
+ {
68
+ name: 'ghostpatch_finding',
69
+ description: 'Get detailed information about a specific finding by its ID.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ id: { type: 'string', description: 'Finding ID' },
74
+ },
75
+ required: ['id'],
76
+ },
77
+ },
78
+ {
79
+ name: 'ghostpatch_secrets',
80
+ description: 'Scan for hardcoded secrets, API keys, tokens, and credentials.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ path: { type: 'string', description: 'Path to scan' },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: 'ghostpatch_dependencies',
90
+ description: 'Check project dependencies for known vulnerabilities (npm audit, pip, etc.).',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ path: { type: 'string', description: 'Project directory path' },
95
+ },
96
+ },
97
+ },
98
+ {
99
+ name: 'ghostpatch_ai_analyze',
100
+ description: 'Run AI-powered deep security analysis on code. Uses HuggingFace (free), Anthropic, or OpenAI.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ code: { type: 'string', description: 'Code to analyze' },
105
+ file: { type: 'string', description: 'File path for context' },
106
+ provider: { type: 'string', enum: ['huggingface', 'anthropic', 'openai'], description: 'AI provider' },
107
+ },
108
+ required: ['code'],
109
+ },
110
+ },
111
+ {
112
+ name: 'ghostpatch_status',
113
+ description: 'Get scanner status, configuration, and stats from last scan.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {},
117
+ },
118
+ },
119
+ ];
120
+
121
+ async function handleToolCall(name: string, args: any): Promise<any> {
122
+ switch (name) {
123
+ case 'ghostpatch_scan': {
124
+ const target = args.path || process.cwd();
125
+ const result = await scan(target, {
126
+ severity: args.severity as Severity,
127
+ });
128
+ lastScanResult = result;
129
+
130
+ if (args.output === 'json' || args.output === 'sarif') {
131
+ return generateReport(result, args.output);
132
+ }
133
+
134
+ return {
135
+ target: result.target,
136
+ filesScanned: result.filesScanned,
137
+ duration: `${result.durationMs}ms`,
138
+ summary: result.summary,
139
+ findings: result.findings.slice(0, 100).map(formatFindingForMCP),
140
+ };
141
+ }
142
+
143
+ case 'ghostpatch_scan_file': {
144
+ const filePath = path.resolve(args.path);
145
+ const content = args.content || fs.readFileSync(filePath, 'utf-8');
146
+ const language = detectLanguage(filePath) || 'generic';
147
+ const findings = scanFile(filePath, content, language);
148
+
149
+ return {
150
+ file: filePath,
151
+ language,
152
+ findings: findings.map(formatFindingForMCP),
153
+ total: findings.length,
154
+ };
155
+ }
156
+
157
+ case 'ghostpatch_findings': {
158
+ if (!lastScanResult) {
159
+ return { error: 'No scan results available. Run ghostpatch_scan first.' };
160
+ }
161
+
162
+ let findings = lastScanResult.findings;
163
+
164
+ if (args.severity) {
165
+ const severityOrder: Record<string, number> = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
166
+ const minLevel = severityOrder[args.severity] || 0;
167
+ findings = findings.filter(f => (severityOrder[f.severity] || 0) >= minLevel);
168
+ }
169
+
170
+ if (args.file) {
171
+ findings = findings.filter(f => f.filePath.includes(args.file));
172
+ }
173
+
174
+ if (args.ruleId) {
175
+ findings = findings.filter(f => f.ruleId === args.ruleId);
176
+ }
177
+
178
+ const limit = args.limit || 50;
179
+ return {
180
+ total: findings.length,
181
+ showing: Math.min(findings.length, limit),
182
+ findings: findings.slice(0, limit).map(formatFindingForMCP),
183
+ };
184
+ }
185
+
186
+ case 'ghostpatch_finding': {
187
+ if (!lastScanResult) {
188
+ return { error: 'No scan results available. Run ghostpatch_scan first.' };
189
+ }
190
+
191
+ const finding = lastScanResult.findings.find(f => f.id === args.id);
192
+ if (!finding) {
193
+ return { error: `Finding not found: ${args.id}` };
194
+ }
195
+
196
+ return finding;
197
+ }
198
+
199
+ case 'ghostpatch_secrets': {
200
+ const target = args.path || process.cwd();
201
+ const result = await scan(target, { severity: Severity.LOW });
202
+ const secretsFindings = result.findings.filter(f =>
203
+ f.ruleId.startsWith('SEC-') ||
204
+ f.title.toLowerCase().includes('secret') ||
205
+ f.title.toLowerCase().includes('key') ||
206
+ f.title.toLowerCase().includes('token') ||
207
+ f.title.toLowerCase().includes('password')
208
+ );
209
+
210
+ return {
211
+ total: secretsFindings.length,
212
+ findings: secretsFindings.map(formatFindingForMCP),
213
+ };
214
+ }
215
+
216
+ case 'ghostpatch_dependencies': {
217
+ const target = args.path || process.cwd();
218
+ const result = await scan(target, { severity: Severity.LOW });
219
+ const depFindings = result.findings.filter(f =>
220
+ f.ruleId.startsWith('DEP-') || f.owasp === 'A06'
221
+ );
222
+
223
+ // Also try npm audit
224
+ const npmFindings = runNpmAudit(target);
225
+
226
+ return {
227
+ total: depFindings.length + npmFindings.length,
228
+ staticAnalysis: depFindings.map(formatFindingForMCP),
229
+ npmAudit: npmFindings.map(formatFindingForMCP),
230
+ };
231
+ }
232
+
233
+ case 'ghostpatch_ai_analyze': {
234
+ const provider = getAvailableProvider(args.provider);
235
+ if (!provider) {
236
+ return { error: 'No AI provider available. Set HF_TOKEN, ANTHROPIC_API_KEY, or OPENAI_API_KEY.' };
237
+ }
238
+
239
+ const findings = await provider.analyze(
240
+ args.code,
241
+ args.file ? `File: ${args.file}` : 'Code snippet'
242
+ );
243
+
244
+ return {
245
+ provider: provider.name,
246
+ findings,
247
+ total: findings.length,
248
+ };
249
+ }
250
+
251
+ case 'ghostpatch_status': {
252
+ return {
253
+ version: '1.0.0',
254
+ lastScan: lastScanResult ? {
255
+ target: lastScanResult.target,
256
+ time: lastScanResult.startTime.toISOString(),
257
+ filesScanned: lastScanResult.filesScanned,
258
+ findingsTotal: lastScanResult.summary.total,
259
+ summary: lastScanResult.summary.bySeverity,
260
+ } : null,
261
+ aiProviders: {
262
+ huggingface: 'available (free)',
263
+ anthropic: process.env.ANTHROPIC_API_KEY ? 'configured' : 'not configured',
264
+ openai: process.env.OPENAI_API_KEY ? 'configured' : 'not configured',
265
+ },
266
+ };
267
+ }
268
+
269
+ default:
270
+ throw new Error(`Unknown tool: ${name}`);
271
+ }
272
+ }
273
+
274
+ function formatFindingForMCP(f: Finding) {
275
+ return {
276
+ id: f.id,
277
+ ruleId: f.ruleId,
278
+ title: f.title,
279
+ severity: f.severity,
280
+ confidence: f.confidence,
281
+ file: f.filePath,
282
+ line: f.line,
283
+ description: f.description,
284
+ cwe: f.cwe,
285
+ owasp: f.owasp,
286
+ remediation: f.remediation,
287
+ aiEnhanced: f.aiEnhanced || false,
288
+ };
289
+ }
290
+
291
+ // ============================================================
292
+ // MCP Server (stdio transport)
293
+ // ============================================================
294
+ export async function startMCPServer(): Promise<void> {
295
+ const readline = await import('readline');
296
+
297
+ const rl = readline.createInterface({
298
+ input: process.stdin,
299
+ output: process.stdout,
300
+ terminal: false,
301
+ });
302
+
303
+ function send(response: MCPResponse): void {
304
+ const json = JSON.stringify(response);
305
+ process.stdout.write(json + '\n');
306
+ }
307
+
308
+ rl.on('line', async (line: string) => {
309
+ let request: MCPRequest;
310
+ try {
311
+ request = JSON.parse(line);
312
+ } catch {
313
+ return;
314
+ }
315
+
316
+ try {
317
+ switch (request.method) {
318
+ case 'initialize':
319
+ send({
320
+ jsonrpc: '2.0',
321
+ id: request.id,
322
+ result: {
323
+ protocolVersion: '2024-11-05',
324
+ capabilities: { tools: {} },
325
+ serverInfo: {
326
+ name: 'ghostpatch',
327
+ version: '1.0.0',
328
+ },
329
+ },
330
+ });
331
+ break;
332
+
333
+ case 'notifications/initialized':
334
+ // No response needed for notifications
335
+ break;
336
+
337
+ case 'tools/list':
338
+ send({
339
+ jsonrpc: '2.0',
340
+ id: request.id,
341
+ result: { tools: TOOLS },
342
+ });
343
+ break;
344
+
345
+ case 'tools/call': {
346
+ const { name, arguments: args } = request.params;
347
+ const result = await handleToolCall(name, args || {});
348
+ send({
349
+ jsonrpc: '2.0',
350
+ id: request.id,
351
+ result: {
352
+ content: [{
353
+ type: 'text',
354
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
355
+ }],
356
+ },
357
+ });
358
+ break;
359
+ }
360
+
361
+ default:
362
+ send({
363
+ jsonrpc: '2.0',
364
+ id: request.id,
365
+ error: { code: -32601, message: `Method not found: ${request.method}` },
366
+ });
367
+ }
368
+ } catch (err: any) {
369
+ send({
370
+ jsonrpc: '2.0',
371
+ id: request.id,
372
+ error: { code: -32603, message: err.message },
373
+ });
374
+ }
375
+ });
376
+
377
+ // Keep the server running
378
+ process.stderr.write('GhostPatch MCP server started (stdio)\n');
379
+ }
@@ -0,0 +1,64 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { GhostPatchConfig, Severity } from '../core/severity';
4
+ import { DEFAULT_EXCLUDE } from './languages';
5
+
6
+ const DEFAULT_CONFIG: GhostPatchConfig = {
7
+ exclude: DEFAULT_EXCLUDE,
8
+ severity: Severity.LOW,
9
+ ai: {
10
+ provider: 'huggingface',
11
+ model: 'auto',
12
+ },
13
+ rules: {
14
+ disabled: [],
15
+ custom: [],
16
+ },
17
+ maxFileSize: 1048576, // 1MB
18
+ languages: 'auto',
19
+ };
20
+
21
+ export function loadConfig(configPath?: string, basePath?: string): GhostPatchConfig {
22
+ const searchPaths = configPath
23
+ ? [configPath]
24
+ : [
25
+ path.join(basePath || process.cwd(), '.ghostpatch.json'),
26
+ path.join(basePath || process.cwd(), '.ghostpatchrc'),
27
+ path.join(basePath || process.cwd(), 'ghostpatch.config.json'),
28
+ ];
29
+
30
+ for (const p of searchPaths) {
31
+ try {
32
+ if (fs.existsSync(p)) {
33
+ const content = fs.readFileSync(p, 'utf-8');
34
+ const userConfig = JSON.parse(content);
35
+ return mergeConfig(DEFAULT_CONFIG, userConfig);
36
+ }
37
+ } catch {
38
+ // Skip invalid config files
39
+ }
40
+ }
41
+
42
+ return { ...DEFAULT_CONFIG };
43
+ }
44
+
45
+ function mergeConfig(defaults: GhostPatchConfig, user: Partial<GhostPatchConfig>): GhostPatchConfig {
46
+ return {
47
+ exclude: user.exclude || defaults.exclude,
48
+ severity: user.severity || defaults.severity,
49
+ ai: {
50
+ ...defaults.ai,
51
+ ...(user.ai || {}),
52
+ },
53
+ rules: {
54
+ disabled: user.rules?.disabled || defaults.rules.disabled,
55
+ custom: user.rules?.custom || defaults.rules.custom,
56
+ },
57
+ maxFileSize: user.maxFileSize || defaults.maxFileSize,
58
+ languages: user.languages || defaults.languages,
59
+ };
60
+ }
61
+
62
+ export function getDefaultConfig(): GhostPatchConfig {
63
+ return { ...DEFAULT_CONFIG };
64
+ }
@@ -0,0 +1,21 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ export function generateFingerprint(...parts: string[]): string {
4
+ const hash = crypto.createHash('sha256');
5
+ hash.update(parts.join('::'));
6
+ return hash.digest('hex').substring(0, 16);
7
+ }
8
+
9
+ export function deduplicateFindings<T extends { fingerprint: string }>(findings: T[]): T[] {
10
+ const seen = new Set<string>();
11
+ const result: T[] = [];
12
+
13
+ for (const finding of findings) {
14
+ if (!seen.has(finding.fingerprint)) {
15
+ seen.add(finding.fingerprint);
16
+ result.push(finding);
17
+ }
18
+ }
19
+
20
+ return result;
21
+ }
@@ -0,0 +1,95 @@
1
+ import * as path from 'path';
2
+
3
+ export const LANGUAGE_MAP: Record<string, string> = {
4
+ '.js': 'javascript',
5
+ '.jsx': 'javascript',
6
+ '.mjs': 'javascript',
7
+ '.cjs': 'javascript',
8
+ '.ts': 'typescript',
9
+ '.tsx': 'typescript',
10
+ '.mts': 'typescript',
11
+ '.cts': 'typescript',
12
+ '.py': 'python',
13
+ '.pyw': 'python',
14
+ '.java': 'java',
15
+ '.go': 'go',
16
+ '.rs': 'rust',
17
+ '.c': 'c',
18
+ '.h': 'c',
19
+ '.cpp': 'cpp',
20
+ '.cc': 'cpp',
21
+ '.cxx': 'cpp',
22
+ '.hpp': 'cpp',
23
+ '.hxx': 'cpp',
24
+ '.cs': 'csharp',
25
+ '.php': 'php',
26
+ '.rb': 'ruby',
27
+ '.swift': 'swift',
28
+ '.kt': 'kotlin',
29
+ '.kts': 'kotlin',
30
+ '.sh': 'shell',
31
+ '.bash': 'shell',
32
+ '.zsh': 'shell',
33
+ '.sql': 'sql',
34
+ '.html': 'html',
35
+ '.htm': 'html',
36
+ '.vue': 'javascript',
37
+ '.svelte': 'javascript',
38
+ };
39
+
40
+ export const SUPPORTED_LANGUAGES = [
41
+ 'typescript', 'javascript', 'python', 'java', 'go', 'rust',
42
+ 'c', 'cpp', 'csharp', 'php', 'ruby', 'swift', 'kotlin',
43
+ 'shell', 'sql',
44
+ ];
45
+
46
+ export function detectLanguage(filePath: string): string | null {
47
+ const ext = path.extname(filePath).toLowerCase();
48
+ return LANGUAGE_MAP[ext] || null;
49
+ }
50
+
51
+ export function isSupportedFile(filePath: string): boolean {
52
+ return detectLanguage(filePath) !== null;
53
+ }
54
+
55
+ export const DEFAULT_EXCLUDE = [
56
+ 'node_modules/**',
57
+ 'dist/**',
58
+ 'build/**',
59
+ 'out/**',
60
+ '.git/**',
61
+ '.next/**',
62
+ '.nuxt/**',
63
+ 'vendor/**',
64
+ '__pycache__/**',
65
+ '*.min.js',
66
+ '*.min.css',
67
+ '*.map',
68
+ '*.lock',
69
+ 'package-lock.json',
70
+ 'yarn.lock',
71
+ 'pnpm-lock.yaml',
72
+ '.env',
73
+ '.env.*',
74
+ 'coverage/**',
75
+ '.nyc_output/**',
76
+ '*.d.ts',
77
+ '*.bundle.js',
78
+ '*.chunk.js',
79
+ ];
80
+
81
+ export const CONFIG_FILES = [
82
+ 'package.json',
83
+ 'requirements.txt',
84
+ 'Pipfile',
85
+ 'pom.xml',
86
+ 'go.mod',
87
+ 'Gemfile',
88
+ 'Cargo.toml',
89
+ 'composer.json',
90
+ ];
91
+
92
+ export function isConfigFile(filePath: string): boolean {
93
+ const basename = path.basename(filePath);
94
+ return CONFIG_FILES.includes(basename);
95
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "__tests__"]
20
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['__tests__/**/*.test.ts'],
6
+ globals: true,
7
+ },
8
+ });