archicore 0.3.5 → 0.3.7

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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Enterprise Indexer
3
+ *
4
+ * Handles large-scale projects (50K+ files) with intelligent sampling,
5
+ * incremental indexing, and tiered analysis modes.
6
+ */
7
+ /**
8
+ * Enterprise indexing options
9
+ */
10
+ export interface EnterpriseIndexingOptions {
11
+ tier: 'quick' | 'standard' | 'deep';
12
+ sampling: {
13
+ enabled: boolean;
14
+ maxFiles: number;
15
+ strategy: 'hot-files' | 'random' | 'directory-balanced' | 'smart';
16
+ };
17
+ incremental: {
18
+ enabled: boolean;
19
+ since?: string;
20
+ };
21
+ focusDirectories?: string[];
22
+ excludePatterns?: string[];
23
+ memoryLimitMB?: number;
24
+ }
25
+ /**
26
+ * Analysis tier configuration
27
+ */
28
+ declare const TIER_CONFIG: {
29
+ quick: {
30
+ maxFiles: number;
31
+ skipEmbeddings: boolean;
32
+ skipSecurity: boolean;
33
+ skipDuplication: boolean;
34
+ description: string;
35
+ };
36
+ standard: {
37
+ maxFiles: number;
38
+ skipEmbeddings: boolean;
39
+ skipSecurity: boolean;
40
+ skipDuplication: boolean;
41
+ description: string;
42
+ };
43
+ deep: {
44
+ maxFiles: number;
45
+ skipEmbeddings: boolean;
46
+ skipSecurity: boolean;
47
+ skipDuplication: boolean;
48
+ description: string;
49
+ };
50
+ };
51
+ /**
52
+ * Default options for enterprise indexing
53
+ */
54
+ export declare const DEFAULT_ENTERPRISE_OPTIONS: EnterpriseIndexingOptions;
55
+ export declare class EnterpriseIndexer {
56
+ private projectPath;
57
+ private options;
58
+ private gitAvailable;
59
+ constructor(projectPath: string, options?: Partial<EnterpriseIndexingOptions>);
60
+ /**
61
+ * Initialize - check git availability
62
+ */
63
+ initialize(): Promise<void>;
64
+ /**
65
+ * Get project size estimation
66
+ */
67
+ getProjectSize(): Promise<{
68
+ totalFiles: number;
69
+ totalSizeMB: number;
70
+ languageDistribution: Record<string, number>;
71
+ recommendation: 'quick' | 'standard' | 'deep';
72
+ estimatedTimeMinutes: number;
73
+ }>;
74
+ /**
75
+ * Get files to index based on sampling strategy
76
+ */
77
+ getFilesToIndex(): Promise<string[]>;
78
+ /**
79
+ * Get files changed since last index (for incremental indexing)
80
+ */
81
+ getChangedFiles(since: string): Promise<string[]>;
82
+ /**
83
+ * Scan all files in project
84
+ */
85
+ private scanAllFiles;
86
+ /**
87
+ * Select hot files (most frequently changed)
88
+ */
89
+ private selectHotFiles;
90
+ /**
91
+ * Random selection
92
+ */
93
+ private selectRandom;
94
+ /**
95
+ * Directory-balanced selection
96
+ */
97
+ private selectDirectoryBalanced;
98
+ /**
99
+ * Smart selection - combines multiple heuristics
100
+ */
101
+ private selectSmart;
102
+ /**
103
+ * Get import counts for files
104
+ */
105
+ private getImportCounts;
106
+ /**
107
+ * Get commit counts from git
108
+ */
109
+ private getCommitCounts;
110
+ /**
111
+ * Get tier configuration
112
+ */
113
+ getTierConfig(): typeof TIER_CONFIG[keyof typeof TIER_CONFIG];
114
+ }
115
+ export { TIER_CONFIG };
116
+ //# sourceMappingURL=enterprise-indexer.d.ts.map
@@ -0,0 +1,529 @@
1
+ /**
2
+ * Enterprise Indexer
3
+ *
4
+ * Handles large-scale projects (50K+ files) with intelligent sampling,
5
+ * incremental indexing, and tiered analysis modes.
6
+ */
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { existsSync, statSync } from 'fs';
10
+ import { readdir, stat, readFile } from 'fs/promises';
11
+ import path from 'path';
12
+ import { Logger } from '../../utils/logger.js';
13
+ const execAsync = promisify(exec);
14
+ /**
15
+ * Analysis tier configuration
16
+ */
17
+ const TIER_CONFIG = {
18
+ quick: {
19
+ maxFiles: 1000,
20
+ skipEmbeddings: true,
21
+ skipSecurity: true,
22
+ skipDuplication: true,
23
+ description: 'Fast overview - structure and basic metrics only'
24
+ },
25
+ standard: {
26
+ maxFiles: 5000,
27
+ skipEmbeddings: false,
28
+ skipSecurity: false,
29
+ skipDuplication: false,
30
+ description: 'Standard analysis - all features with sampling'
31
+ },
32
+ deep: {
33
+ maxFiles: 50000,
34
+ skipEmbeddings: false,
35
+ skipSecurity: false,
36
+ skipDuplication: false,
37
+ description: 'Deep analysis - comprehensive analysis for entire project'
38
+ }
39
+ };
40
+ /**
41
+ * Default options for enterprise indexing
42
+ */
43
+ export const DEFAULT_ENTERPRISE_OPTIONS = {
44
+ tier: 'standard',
45
+ sampling: {
46
+ enabled: true,
47
+ maxFiles: 5000,
48
+ strategy: 'smart'
49
+ },
50
+ incremental: {
51
+ enabled: false
52
+ },
53
+ excludePatterns: [
54
+ 'node_modules',
55
+ '.git',
56
+ 'dist',
57
+ 'build',
58
+ '__pycache__',
59
+ '.cache',
60
+ 'vendor',
61
+ 'coverage',
62
+ '.next',
63
+ '.nuxt'
64
+ ],
65
+ memoryLimitMB: 2048
66
+ };
67
+ export class EnterpriseIndexer {
68
+ projectPath;
69
+ options;
70
+ gitAvailable = false;
71
+ constructor(projectPath, options = {}) {
72
+ this.projectPath = projectPath;
73
+ this.options = { ...DEFAULT_ENTERPRISE_OPTIONS, ...options };
74
+ // Apply tier config
75
+ const tierConfig = TIER_CONFIG[this.options.tier];
76
+ if (this.options.sampling.enabled && !options.sampling?.maxFiles) {
77
+ this.options.sampling.maxFiles = tierConfig.maxFiles;
78
+ }
79
+ }
80
+ /**
81
+ * Initialize - check git availability
82
+ */
83
+ async initialize() {
84
+ try {
85
+ await execAsync('git --version', { cwd: this.projectPath });
86
+ const gitDir = path.join(this.projectPath, '.git');
87
+ this.gitAvailable = existsSync(gitDir);
88
+ }
89
+ catch {
90
+ this.gitAvailable = false;
91
+ }
92
+ Logger.info(`Enterprise indexer initialized. Git available: ${this.gitAvailable}`);
93
+ }
94
+ /**
95
+ * Get project size estimation
96
+ */
97
+ async getProjectSize() {
98
+ const files = await this.scanAllFiles();
99
+ const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
100
+ // Count by language
101
+ const languageDistribution = {};
102
+ for (const file of files) {
103
+ const ext = path.extname(file.path).toLowerCase() || '.other';
104
+ languageDistribution[ext] = (languageDistribution[ext] || 0) + 1;
105
+ }
106
+ // Recommendation based on size
107
+ let recommendation;
108
+ if (files.length < 1000) {
109
+ recommendation = 'deep';
110
+ }
111
+ else if (files.length < 10000) {
112
+ recommendation = 'standard';
113
+ }
114
+ else {
115
+ recommendation = 'quick';
116
+ }
117
+ // Estimate time (rough)
118
+ // Quick: ~10 files/sec, Standard: ~5 files/sec, Deep: ~2 files/sec
119
+ const filesPerSecond = recommendation === 'quick' ? 10 : recommendation === 'standard' ? 5 : 2;
120
+ const filesToProcess = Math.min(files.length, TIER_CONFIG[recommendation].maxFiles);
121
+ const estimatedTimeMinutes = Math.ceil(filesToProcess / filesPerSecond / 60);
122
+ return {
123
+ totalFiles: files.length,
124
+ totalSizeMB: Math.round(totalSize / (1024 * 1024) * 100) / 100,
125
+ languageDistribution,
126
+ recommendation,
127
+ estimatedTimeMinutes
128
+ };
129
+ }
130
+ /**
131
+ * Get files to index based on sampling strategy
132
+ */
133
+ async getFilesToIndex() {
134
+ const allFiles = await this.scanAllFiles();
135
+ Logger.info(`Total files found: ${allFiles.length}`);
136
+ if (!this.options.sampling.enabled) {
137
+ return allFiles.map(f => f.path);
138
+ }
139
+ const maxFiles = this.options.sampling.maxFiles;
140
+ if (allFiles.length <= maxFiles) {
141
+ Logger.info(`All files within limit (${allFiles.length} <= ${maxFiles})`);
142
+ return allFiles.map(f => f.path);
143
+ }
144
+ // Apply sampling strategy
145
+ let selectedFiles;
146
+ switch (this.options.sampling.strategy) {
147
+ case 'hot-files':
148
+ selectedFiles = await this.selectHotFiles(allFiles, maxFiles);
149
+ break;
150
+ case 'random':
151
+ selectedFiles = this.selectRandom(allFiles, maxFiles);
152
+ break;
153
+ case 'directory-balanced':
154
+ selectedFiles = this.selectDirectoryBalanced(allFiles, maxFiles);
155
+ break;
156
+ case 'smart':
157
+ default:
158
+ selectedFiles = await this.selectSmart(allFiles, maxFiles);
159
+ }
160
+ Logger.info(`Selected ${selectedFiles.length} files using '${this.options.sampling.strategy}' strategy`);
161
+ return selectedFiles;
162
+ }
163
+ /**
164
+ * Get files changed since last index (for incremental indexing)
165
+ */
166
+ async getChangedFiles(since) {
167
+ if (!this.gitAvailable) {
168
+ Logger.warn('Git not available - cannot determine changed files');
169
+ return [];
170
+ }
171
+ try {
172
+ // Try to use git to get changed files
173
+ let command;
174
+ if (since.match(/^[0-9a-f]{7,40}$/i)) {
175
+ // Git commit hash
176
+ command = `git diff --name-only ${since}..HEAD`;
177
+ }
178
+ else {
179
+ // ISO date
180
+ command = `git log --since="${since}" --name-only --pretty=format: | sort -u`;
181
+ }
182
+ const { stdout } = await execAsync(command, { cwd: this.projectPath });
183
+ const changedFiles = stdout.split('\n')
184
+ .map(f => f.trim())
185
+ .filter(f => f.length > 0)
186
+ .map(f => path.join(this.projectPath, f))
187
+ .filter(f => existsSync(f));
188
+ Logger.info(`Found ${changedFiles.length} changed files since ${since}`);
189
+ return changedFiles;
190
+ }
191
+ catch (error) {
192
+ Logger.error('Failed to get changed files:', error);
193
+ return [];
194
+ }
195
+ }
196
+ /**
197
+ * Scan all files in project
198
+ */
199
+ async scanAllFiles() {
200
+ const files = [];
201
+ const excludePatterns = this.options.excludePatterns || [];
202
+ const CODE_EXTENSIONS = new Set([
203
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
204
+ '.py', '.rb', '.go', '.rs', '.java', '.kt', '.scala',
205
+ '.c', '.cpp', '.h', '.hpp', '.cs', '.swift',
206
+ '.vue', '.svelte', '.astro',
207
+ '.php', '.pl', '.pm', '.lua', '.r',
208
+ '.sql', '.graphql', '.gql',
209
+ '.yaml', '.yml', '.json', '.toml', '.xml'
210
+ ]);
211
+ const scanDir = async (dir) => {
212
+ try {
213
+ const entries = await readdir(dir, { withFileTypes: true });
214
+ for (const entry of entries) {
215
+ const fullPath = path.join(dir, entry.name);
216
+ // Check exclude patterns
217
+ if (excludePatterns.some(p => fullPath.includes(p))) {
218
+ continue;
219
+ }
220
+ if (entry.isDirectory()) {
221
+ await scanDir(fullPath);
222
+ }
223
+ else if (entry.isFile()) {
224
+ const ext = path.extname(entry.name).toLowerCase();
225
+ if (CODE_EXTENSIONS.has(ext)) {
226
+ try {
227
+ const stats = await stat(fullPath);
228
+ // Skip very large files (> 1MB)
229
+ if (stats.size < 1024 * 1024) {
230
+ files.push({ path: fullPath, size: stats.size });
231
+ }
232
+ }
233
+ catch {
234
+ // Skip files we can't stat
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ catch {
241
+ // Skip directories we can't read
242
+ }
243
+ };
244
+ await scanDir(this.projectPath);
245
+ return files;
246
+ }
247
+ /**
248
+ * Select hot files (most frequently changed)
249
+ */
250
+ async selectHotFiles(files, maxFiles) {
251
+ if (!this.gitAvailable) {
252
+ Logger.warn('Git not available, falling back to smart selection');
253
+ return this.selectSmart(files, maxFiles);
254
+ }
255
+ const fileScores = [];
256
+ // Get commit counts for each file
257
+ try {
258
+ const { stdout } = await execAsync('git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -5000', { cwd: this.projectPath, maxBuffer: 10 * 1024 * 1024 });
259
+ const commitCounts = new Map();
260
+ for (const line of stdout.split('\n')) {
261
+ const match = line.trim().match(/^\s*(\d+)\s+(.+)$/);
262
+ if (match) {
263
+ const count = parseInt(match[1], 10);
264
+ const filePath = path.join(this.projectPath, match[2]);
265
+ commitCounts.set(filePath, count);
266
+ }
267
+ }
268
+ for (const file of files) {
269
+ const commitCount = commitCounts.get(file.path) || 0;
270
+ fileScores.push({
271
+ path: file.path,
272
+ score: commitCount,
273
+ reasons: [`${commitCount} commits`],
274
+ lastModified: new Date(),
275
+ commitCount
276
+ });
277
+ }
278
+ }
279
+ catch {
280
+ // Fallback to modification time
281
+ for (const file of files) {
282
+ try {
283
+ const stats = statSync(file.path);
284
+ fileScores.push({
285
+ path: file.path,
286
+ score: stats.mtimeMs,
287
+ reasons: ['recent modification'],
288
+ lastModified: stats.mtime
289
+ });
290
+ }
291
+ catch {
292
+ fileScores.push({
293
+ path: file.path,
294
+ score: 0,
295
+ reasons: [],
296
+ lastModified: new Date(0)
297
+ });
298
+ }
299
+ }
300
+ }
301
+ // Sort by score and select top N
302
+ fileScores.sort((a, b) => b.score - a.score);
303
+ return fileScores.slice(0, maxFiles).map(f => f.path);
304
+ }
305
+ /**
306
+ * Random selection
307
+ */
308
+ selectRandom(files, maxFiles) {
309
+ const shuffled = [...files].sort(() => Math.random() - 0.5);
310
+ return shuffled.slice(0, maxFiles).map(f => f.path);
311
+ }
312
+ /**
313
+ * Directory-balanced selection
314
+ */
315
+ selectDirectoryBalanced(files, maxFiles) {
316
+ // Group by top-level directory
317
+ const byDirectory = new Map();
318
+ for (const file of files) {
319
+ const relativePath = path.relative(this.projectPath, file.path);
320
+ const topDir = relativePath.split(path.sep)[0] || '.';
321
+ if (!byDirectory.has(topDir)) {
322
+ byDirectory.set(topDir, []);
323
+ }
324
+ byDirectory.get(topDir).push(file);
325
+ }
326
+ // Calculate files per directory
327
+ const dirCount = byDirectory.size;
328
+ const filesPerDir = Math.ceil(maxFiles / dirCount);
329
+ const selected = [];
330
+ for (const [, dirFiles] of byDirectory) {
331
+ // Sort by recency (assume larger files are more important)
332
+ dirFiles.sort((a, b) => b.size - a.size);
333
+ const toTake = Math.min(filesPerDir, dirFiles.length);
334
+ selected.push(...dirFiles.slice(0, toTake).map(f => f.path));
335
+ }
336
+ // If we have room, add more from the largest directories
337
+ if (selected.length < maxFiles) {
338
+ const remaining = maxFiles - selected.length;
339
+ const allRemaining = files
340
+ .filter(f => !selected.includes(f.path))
341
+ .sort((a, b) => b.size - a.size)
342
+ .slice(0, remaining);
343
+ selected.push(...allRemaining.map(f => f.path));
344
+ }
345
+ return selected.slice(0, maxFiles);
346
+ }
347
+ /**
348
+ * Smart selection - combines multiple heuristics
349
+ */
350
+ async selectSmart(files, maxFiles) {
351
+ const fileScores = [];
352
+ // Priority patterns (entry points, configs, core logic)
353
+ const HIGH_PRIORITY_PATTERNS = [
354
+ /index\.(ts|js|tsx|jsx)$/,
355
+ /main\.(ts|js|py|go|rs)$/,
356
+ /app\.(ts|js|tsx|jsx|vue)$/,
357
+ /server\.(ts|js)$/,
358
+ /config\.(ts|js|json|yaml|yml)$/,
359
+ /routes?\.(ts|js)$/,
360
+ /api\.(ts|js)$/,
361
+ /service\.(ts|js)$/,
362
+ /controller\.(ts|js)$/,
363
+ /model\.(ts|js|py)$/,
364
+ /schema\.(ts|js|graphql)$/,
365
+ /types?\.(ts|d\.ts)$/,
366
+ /package\.json$/,
367
+ /tsconfig\.json$/,
368
+ /\.env\.example$/
369
+ ];
370
+ // Get import counts if possible
371
+ const importCounts = await this.getImportCounts(files);
372
+ // Get commit counts if git available
373
+ let commitCounts = new Map();
374
+ if (this.gitAvailable) {
375
+ commitCounts = await this.getCommitCounts();
376
+ }
377
+ for (const file of files) {
378
+ const relativePath = path.relative(this.projectPath, file.path);
379
+ let score = 0;
380
+ const reasons = [];
381
+ // Check priority patterns
382
+ for (const pattern of HIGH_PRIORITY_PATTERNS) {
383
+ if (pattern.test(relativePath)) {
384
+ score += 100;
385
+ reasons.push('priority file');
386
+ break;
387
+ }
388
+ }
389
+ // Focus directories get higher priority
390
+ if (this.options.focusDirectories) {
391
+ for (const focusDir of this.options.focusDirectories) {
392
+ if (relativePath.startsWith(focusDir)) {
393
+ score += 50;
394
+ reasons.push('focus directory');
395
+ break;
396
+ }
397
+ }
398
+ }
399
+ // Bonus for shallow depth (closer to root = more important)
400
+ const depth = relativePath.split(path.sep).length;
401
+ score += Math.max(0, 20 - depth * 2);
402
+ // Bonus for import count
403
+ const imports = importCounts.get(file.path) || 0;
404
+ if (imports > 0) {
405
+ score += Math.min(imports * 5, 50);
406
+ reasons.push(`${imports} imports`);
407
+ }
408
+ // Bonus for commit count
409
+ const commits = commitCounts.get(file.path) || 0;
410
+ if (commits > 0) {
411
+ score += Math.min(commits * 2, 30);
412
+ reasons.push(`${commits} commits`);
413
+ }
414
+ // Penalty for test files (still include some)
415
+ if (relativePath.includes('test') || relativePath.includes('spec') || relativePath.includes('__tests__')) {
416
+ score -= 30;
417
+ }
418
+ // Penalty for generated files
419
+ if (relativePath.includes('.generated') || relativePath.includes('.d.ts')) {
420
+ score -= 40;
421
+ }
422
+ fileScores.push({
423
+ path: file.path,
424
+ score,
425
+ reasons,
426
+ lastModified: new Date(),
427
+ commitCount: commits,
428
+ importCount: imports
429
+ });
430
+ }
431
+ // Sort by score
432
+ fileScores.sort((a, b) => b.score - a.score);
433
+ // Log top 10 for debugging
434
+ Logger.debug('Top 10 files by score:');
435
+ for (const f of fileScores.slice(0, 10)) {
436
+ Logger.debug(` ${f.score}: ${path.relative(this.projectPath, f.path)} (${f.reasons.join(', ')})`);
437
+ }
438
+ return fileScores.slice(0, maxFiles).map(f => f.path);
439
+ }
440
+ /**
441
+ * Get import counts for files
442
+ */
443
+ async getImportCounts(files) {
444
+ const importCounts = new Map();
445
+ // Quick scan for imports (simplified)
446
+ const importPatterns = [
447
+ /from\s+['"]([^'"]+)['"]/g,
448
+ /import\s+['"]([^'"]+)['"]/g,
449
+ /require\(['"]([^'"]+)['"]\)/g
450
+ ];
451
+ // Normalize import path to file path
452
+ const normalizeImport = (importPath, fromFile) => {
453
+ // Skip external packages
454
+ if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
455
+ return null;
456
+ }
457
+ const fromDir = path.dirname(fromFile);
458
+ let resolved = path.resolve(fromDir, importPath);
459
+ // Try common extensions
460
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.json', ''];
461
+ for (const ext of extensions) {
462
+ const withExt = resolved + ext;
463
+ if (existsSync(withExt)) {
464
+ return withExt;
465
+ }
466
+ // Try index files
467
+ const indexPath = path.join(resolved, 'index' + ext);
468
+ if (existsSync(indexPath)) {
469
+ return indexPath;
470
+ }
471
+ }
472
+ return null;
473
+ };
474
+ // Sample files for import counting (don't scan all)
475
+ const sampleSize = Math.min(1000, files.length);
476
+ const sample = files.slice(0, sampleSize);
477
+ for (const file of sample) {
478
+ try {
479
+ const content = await readFile(file.path, 'utf-8');
480
+ for (const pattern of importPatterns) {
481
+ let match;
482
+ while ((match = pattern.exec(content)) !== null) {
483
+ const importPath = match[1];
484
+ const resolvedPath = normalizeImport(importPath, file.path);
485
+ if (resolvedPath) {
486
+ importCounts.set(resolvedPath, (importCounts.get(resolvedPath) || 0) + 1);
487
+ }
488
+ }
489
+ }
490
+ }
491
+ catch {
492
+ // Skip files we can't read
493
+ }
494
+ }
495
+ return importCounts;
496
+ }
497
+ /**
498
+ * Get commit counts from git
499
+ */
500
+ async getCommitCounts() {
501
+ const commitCounts = new Map();
502
+ if (!this.gitAvailable) {
503
+ return commitCounts;
504
+ }
505
+ try {
506
+ const { stdout } = await execAsync('git log --name-only --pretty=format: --since="1 year ago" | sort | uniq -c | sort -rn | head -2000', { cwd: this.projectPath, maxBuffer: 10 * 1024 * 1024 });
507
+ for (const line of stdout.split('\n')) {
508
+ const match = line.trim().match(/^\s*(\d+)\s+(.+)$/);
509
+ if (match) {
510
+ const count = parseInt(match[1], 10);
511
+ const filePath = path.join(this.projectPath, match[2]);
512
+ commitCounts.set(filePath, count);
513
+ }
514
+ }
515
+ }
516
+ catch {
517
+ // Ignore errors
518
+ }
519
+ return commitCounts;
520
+ }
521
+ /**
522
+ * Get tier configuration
523
+ */
524
+ getTierConfig() {
525
+ return TIER_CONFIG[this.options.tier];
526
+ }
527
+ }
528
+ export { TIER_CONFIG };
529
+ //# sourceMappingURL=enterprise-indexer.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archicore",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "AI Software Architect - code analysis, impact prediction, semantic search",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -80,6 +80,7 @@
80
80
  "passport-github2": "^0.1.12",
81
81
  "pg": "^8.13.0",
82
82
  "ora": "^8.1.1",
83
+ "tar": "^7.4.3",
83
84
  "tree-sitter": "^0.21.1",
84
85
  "tree-sitter-javascript": "^0.21.4",
85
86
  "tree-sitter-python": "^0.21.0",