minovative-mind-cli 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 (53) hide show
  1. package/README.md +418 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/chat.d.ts +7 -0
  7. package/dist/commands/chat.js +30 -0
  8. package/dist/commands/login.d.ts +5 -0
  9. package/dist/commands/login.js +18 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +12 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/services/agent-tools.d.ts +36 -0
  15. package/dist/services/agent-tools.js +764 -0
  16. package/dist/services/agent.d.ts +21 -0
  17. package/dist/services/agent.js +648 -0
  18. package/dist/services/ai.d.ts +60 -0
  19. package/dist/services/ai.js +331 -0
  20. package/dist/services/auth.d.ts +3 -0
  21. package/dist/services/auth.js +183 -0
  22. package/dist/services/changeLogger.d.ts +23 -0
  23. package/dist/services/changeLogger.js +57 -0
  24. package/dist/services/contextAgent.d.ts +20 -0
  25. package/dist/services/contextAgent.js +440 -0
  26. package/dist/services/proxyClient.d.ts +21 -0
  27. package/dist/services/proxyClient.js +119 -0
  28. package/dist/services/verificationService.d.ts +10 -0
  29. package/dist/services/verificationService.js +148 -0
  30. package/dist/utils/atomicWrite.d.ts +6 -0
  31. package/dist/utils/atomicWrite.js +29 -0
  32. package/dist/utils/config.d.ts +17 -0
  33. package/dist/utils/config.js +17 -0
  34. package/dist/utils/contextPrompts.d.ts +3 -0
  35. package/dist/utils/contextPrompts.js +34 -0
  36. package/dist/utils/dependencyTracer.d.ts +48 -0
  37. package/dist/utils/dependencyTracer.js +647 -0
  38. package/dist/utils/excludedExtensions.d.ts +8 -0
  39. package/dist/utils/excludedExtensions.js +125 -0
  40. package/dist/utils/fuzzyMatch.d.ts +21 -0
  41. package/dist/utils/fuzzyMatch.js +121 -0
  42. package/dist/utils/logger.d.ts +8 -0
  43. package/dist/utils/logger.js +17 -0
  44. package/dist/utils/pathSecurity.d.ts +10 -0
  45. package/dist/utils/pathSecurity.js +26 -0
  46. package/dist/utils/symbolExtractor.d.ts +6 -0
  47. package/dist/utils/symbolExtractor.js +249 -0
  48. package/dist/utils/syntaxValidator.d.ts +5 -0
  49. package/dist/utils/syntaxValidator.js +81 -0
  50. package/dist/utils/systemPrompts.d.ts +5 -0
  51. package/dist/utils/systemPrompts.js +119 -0
  52. package/oclif.manifest.json +69 -0
  53. package/package.json +81 -0
@@ -0,0 +1,647 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { EXCLUDED_EXTENSIONS } from './excludedExtensions.js';
4
+ // ─── Language Profile Registry ───────────────────────────────────────
5
+ //
6
+ // Each profile defines regex patterns that extract import specifiers for
7
+ // a family of languages. Patterns use named capture groups for uniform
8
+ // extraction. The registry is intentionally exhaustive — covering edge
9
+ // cases like dynamic imports, re-exports, CSS @use, Go multi-import
10
+ // blocks, and Rust `mod` declarations.
11
+ const LANGUAGE_PROFILES = [
12
+ // ── JavaScript / TypeScript ────────────────────────────────────────
13
+ {
14
+ extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'],
15
+ patterns: [
16
+ // Static imports: import X from 'specifier' / import 'specifier'
17
+ // Covers: import Foo from '...', import { a, b } from '...', import * as X from '...'
18
+ // Also covers: import '...' (side-effect only)
19
+ /import\s+(?:[\s\S]*?\s+from\s+)?['"](?<specifier>[^'"]+)['"]/gm,
20
+ // Dynamic imports: import('specifier') / await import('specifier')
21
+ /import\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
22
+ // CommonJS require: require('specifier')
23
+ /require\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
24
+ // Re-exports: export { X } from 'specifier' / export * from 'specifier'
25
+ /export\s+(?:[\s\S]*?\s+from\s+)['"](?<specifier>[^'"]+)['"]/gm,
26
+ ],
27
+ },
28
+ // ── Python ─────────────────────────────────────────────────────────
29
+ {
30
+ extensions: ['.py'],
31
+ patterns: [
32
+ // from package.module import X → captures 'package.module'
33
+ /^from\s+(?<specifier>[^\s]+)\s+import\s/gm,
34
+ // import package.module → captures 'package.module'
35
+ // Negative lookahead prevents matching 'from X import Y' (handled above)
36
+ /^import\s+(?!.*\sfrom\s)(?<specifier>[^\s,]+)/gm,
37
+ ],
38
+ normalizeSpecifier: (spec) => {
39
+ // Convert dotted module path to filesystem path: foo.bar.baz → foo/bar/baz.py
40
+ if (spec.startsWith('.')) {
41
+ // Relative import: .foo → ./foo.py, ..foo → ../foo.py
42
+ const dots = spec.match(/^\.+/)[0];
43
+ const rest = spec.slice(dots.length);
44
+ const prefix = dots.length === 1 ? './' : '../'.repeat(dots.length - 1);
45
+ return rest ? `${prefix}${rest.replace(/\./g, '/')}.py` : prefix.slice(0, -1);
46
+ }
47
+ return spec.replace(/\./g, '/') + '.py';
48
+ },
49
+ },
50
+ // ── Rust ────────────────────────────────────────────────────────────
51
+ {
52
+ extensions: ['.rs'],
53
+ patterns: [
54
+ // use crate::module::item → captures 'crate::module::item'
55
+ /use\s+(?<specifier>(?:crate|self|super)(?:::[a-zA-Z_][a-zA-Z0-9_]*)+)/gm,
56
+ // mod module_name; → captures 'module_name' (file-level module declaration)
57
+ /mod\s+(?<specifier>[a-zA-Z_][a-zA-Z0-9_]*)\s*;/gm,
58
+ ],
59
+ normalizeSpecifier: (spec, sourceFile) => {
60
+ if (!spec.includes('::')) {
61
+ // It's a `mod foo;` — resolves to either foo.rs or foo/mod.rs
62
+ return spec + '.rs';
63
+ }
64
+ // crate::foo::bar → src/foo/bar.rs (strip 'crate::' prefix)
65
+ const parts = spec.replace(/^crate::/, '').replace(/^(self|super)::/, '').split('::');
66
+ return parts.join('/') + '.rs';
67
+ },
68
+ },
69
+ // ── Go ─────────────────────────────────────────────────────────────
70
+ {
71
+ extensions: ['.go'],
72
+ patterns: [
73
+ // Single import: import "fmt" or import alias "pkg/path"
74
+ /import\s+(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']/gm,
75
+ // Multi-line import block: import ( "fmt" \n "os" )
76
+ // This captures individual lines inside the block
77
+ /^\s*(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']\s*$/gm,
78
+ ],
79
+ },
80
+ // ── C / C++ ────────────────────────────────────────────────────────
81
+ {
82
+ extensions: ['.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'],
83
+ patterns: [
84
+ // #include "local_header.h" → captures local includes (quoted)
85
+ /^\s*#\s*include\s+"(?<specifier>[^"]+)"/gm,
86
+ // #include <system_header> → captures system includes (angled)
87
+ // We still track these — the resolver will skip if not found locally
88
+ /^\s*#\s*include\s+<(?<specifier>[^>]+)>/gm,
89
+ ],
90
+ },
91
+ // ── Java / Kotlin ──────────────────────────────────────────────────
92
+ {
93
+ extensions: ['.java', '.kt', '.kts'],
94
+ patterns: [
95
+ // import com.example.package.ClassName
96
+ /^import\s+(?:static\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*\.[a-zA-Z_][a-zA-Z0-9_]*)/gm,
97
+ ],
98
+ normalizeSpecifier: (spec) => {
99
+ // com.example.Foo → com/example/Foo.java (best-effort)
100
+ return spec.replace(/\./g, '/') + '.java';
101
+ },
102
+ },
103
+ // ── CSS / SCSS / SASS / LESS ───────────────────────────────────────
104
+ {
105
+ extensions: ['.css', '.scss', '.sass', '.less', '.styl'],
106
+ patterns: [
107
+ // @import 'file' or @import url('file')
108
+ /@import\s+(?:url\s*\(\s*)?['"](?<specifier>[^'"]+)['"]/gm,
109
+ // @use 'file' (SCSS)
110
+ /@use\s+['"](?<specifier>[^'"]+)['"]/gm,
111
+ // @forward 'file' (SCSS)
112
+ /@forward\s+['"](?<specifier>[^'"]+)['"]/gm,
113
+ ],
114
+ },
115
+ // ── Ruby ───────────────────────────────────────────────────────────
116
+ {
117
+ extensions: ['.rb'],
118
+ patterns: [
119
+ // require 'file' or require "file"
120
+ /require\s+['"](?<specifier>[^'"]+)['"]/gm,
121
+ // require_relative 'file'
122
+ /require_relative\s+['"](?<specifier>[^'"]+)['"]/gm,
123
+ // load 'file'
124
+ /load\s+['"](?<specifier>[^'"]+)['"]/gm,
125
+ ],
126
+ },
127
+ // ── PHP ────────────────────────────────────────────────────────────
128
+ {
129
+ extensions: ['.php'],
130
+ patterns: [
131
+ // use Namespace\ClassName
132
+ /use\s+(?<specifier>[A-Z][a-zA-Z0-9_\\]+)/gm,
133
+ // include/require 'file.php' or include_once/require_once 'file.php'
134
+ /(?:include|require)(?:_once)?\s+['"](?<specifier>[^'"]+)['"]/gm,
135
+ // include/require with parentheses
136
+ /(?:include|require)(?:_once)?\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
137
+ ],
138
+ normalizeSpecifier: (spec) => {
139
+ // Convert PHP namespace to path: App\Models\User → App/Models/User.php
140
+ if (spec.includes('\\')) {
141
+ return spec.replace(/\\/g, '/') + '.php';
142
+ }
143
+ return spec;
144
+ },
145
+ },
146
+ // ── Swift ──────────────────────────────────────────────────────────
147
+ {
148
+ extensions: ['.swift'],
149
+ patterns: [
150
+ // import ModuleName or import class Module.Type
151
+ /^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|typealias\s+|func\s+|var\s+|let\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*)/gm,
152
+ ],
153
+ },
154
+ // ── Dart ───────────────────────────────────────────────────────────
155
+ {
156
+ extensions: ['.dart'],
157
+ patterns: [
158
+ // import 'package:...' or import 'relative/path.dart'
159
+ /import\s+['"](?<specifier>[^'"]+)['"]/gm,
160
+ // export 'file.dart'
161
+ /export\s+['"](?<specifier>[^'"]+)['"]/gm,
162
+ // part 'file.dart'
163
+ /part\s+['"](?<specifier>[^'"]+)['"]/gm,
164
+ // part of 'file.dart'
165
+ /part\s+of\s+['"](?<specifier>[^'"]+)['"]/gm,
166
+ ],
167
+ },
168
+ ];
169
+ // ─── Extension → Profile Lookup ──────────────────────────────────────
170
+ //
171
+ // Pre-computed for O(1) lookups during the workspace walk.
172
+ const PROFILE_BY_EXT = new Map();
173
+ for (const profile of LANGUAGE_PROFILES) {
174
+ for (const ext of profile.extensions) {
175
+ const existing = PROFILE_BY_EXT.get(ext) || [];
176
+ existing.push(profile);
177
+ PROFILE_BY_EXT.set(ext, existing);
178
+ }
179
+ }
180
+ // ─── Ignored Directories ─────────────────────────────────────────────
181
+ //
182
+ // Mirrors the ignore lists from agent-tools.ts to stay consistent.
183
+ const WALK_IGNORED_DIRS = new Set([
184
+ 'node_modules',
185
+ '.git',
186
+ 'dist',
187
+ '.next',
188
+ '.nuxt',
189
+ '__pycache__',
190
+ '.venv',
191
+ 'venv',
192
+ '.cache',
193
+ 'coverage',
194
+ '.turbo',
195
+ 'build',
196
+ 'out',
197
+ 'target', // Rust/Java build output
198
+ '.gradle',
199
+ '.idea',
200
+ ]);
201
+ const WALK_IGNORED_FILES = new Set([
202
+ 'package-lock.json',
203
+ 'yarn.lock',
204
+ 'pnpm-lock.yaml',
205
+ '.DS_Store',
206
+ ]);
207
+ // Build a set of bare extensions from EXCLUDED_EXTENSIONS for fast lookup
208
+ const EXCLUDED_EXT_SET = new Set(EXCLUDED_EXTENSIONS.map((glob) => glob.replace('*', '')));
209
+ let cachedAliases = null;
210
+ let cachedAliasRoot = null;
211
+ /**
212
+ * Reads and caches `tsconfig.json` or `jsconfig.json` path aliases.
213
+ * Returns an array of alias mappings, or an empty array if none found.
214
+ */
215
+ async function loadPathAliases(workspaceRoot) {
216
+ if (cachedAliasRoot === workspaceRoot && cachedAliases !== null) {
217
+ return cachedAliases;
218
+ }
219
+ const aliases = [];
220
+ for (const configName of ['tsconfig.json', 'jsconfig.json']) {
221
+ try {
222
+ const configPath = path.join(workspaceRoot, configName);
223
+ const raw = await fs.readFile(configPath, 'utf-8');
224
+ // Strip single-line comments (// ...) and trailing commas for lenient JSON parsing
225
+ const cleaned = raw
226
+ .replace(/\/\/.*$/gm, '')
227
+ .replace(/,\s*([\]}])/g, '$1');
228
+ const config = JSON.parse(cleaned);
229
+ const paths = config?.compilerOptions?.paths;
230
+ const baseUrl = config?.compilerOptions?.baseUrl || '.';
231
+ if (paths && typeof paths === 'object') {
232
+ for (const [pattern, targets] of Object.entries(paths)) {
233
+ if (!Array.isArray(targets))
234
+ continue;
235
+ // Convert tsconfig paths pattern to prefix matching
236
+ // e.g., "@/*" → prefix "@/", targets ["./src/*"] → ["./src/"]
237
+ const prefix = pattern.replace(/\*$/, '');
238
+ const resolvedTargets = targets.map((t) => {
239
+ const targetBase = t.replace(/\*$/, '');
240
+ return path.join(baseUrl, targetBase);
241
+ });
242
+ aliases.push({ prefix, targets: resolvedTargets });
243
+ }
244
+ }
245
+ break; // Found a config, stop looking
246
+ }
247
+ catch {
248
+ // Config doesn't exist or is invalid — try the next one
249
+ }
250
+ }
251
+ cachedAliases = aliases;
252
+ cachedAliasRoot = workspaceRoot;
253
+ return aliases;
254
+ }
255
+ // ─── Import Extraction ───────────────────────────────────────────────
256
+ /**
257
+ * Extracts all import specifiers from a file's content based on its extension.
258
+ * Returns an array of raw specifier strings (not yet resolved to paths).
259
+ */
260
+ function extractImports(content, ext) {
261
+ const profiles = PROFILE_BY_EXT.get(ext);
262
+ if (!profiles || profiles.length === 0)
263
+ return [];
264
+ const specifiers = new Set();
265
+ for (const profile of profiles) {
266
+ for (const pattern of profile.patterns) {
267
+ // Reset lastIndex for safety (patterns are reused across files)
268
+ pattern.lastIndex = 0;
269
+ for (const match of content.matchAll(pattern)) {
270
+ const raw = match.groups?.specifier;
271
+ if (raw && raw.trim().length > 0) {
272
+ const normalized = profile.normalizeSpecifier
273
+ ? profile.normalizeSpecifier(raw.trim(), '')
274
+ : raw.trim();
275
+ specifiers.add(normalized);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ return Array.from(specifiers);
281
+ }
282
+ // ─── Path Resolution ─────────────────────────────────────────────────
283
+ /** Extensions to probe when the import specifier has no extension */
284
+ const PROBE_EXTENSIONS = [
285
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
286
+ '.py', '.rs', '.go', '.rb', '.php', '.swift', '.dart',
287
+ '.css', '.scss', '.sass', '.less',
288
+ '.json',
289
+ ];
290
+ /** Index file basenames to probe for directory imports */
291
+ const INDEX_FILES = [
292
+ 'index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs',
293
+ 'mod.rs', // Rust convention
294
+ '__init__.py', // Python convention
295
+ ];
296
+ /**
297
+ * Resolves a raw import specifier to a workspace-relative file path.
298
+ * Returns `null` if the specifier is external (node_modules, system import)
299
+ * or if the target file doesn't exist.
300
+ *
301
+ * Resolution cascade:
302
+ * 1. Path alias expansion (tsconfig/jsconfig `compilerOptions.paths`)
303
+ * 2. Relative path resolution
304
+ * 3. Extension probing (try appending .ts, .js, etc.)
305
+ * 4. Index/barrel file probing (try /index.ts, /index.js, etc.)
306
+ * 5. Existence verification
307
+ */
308
+ async function resolveImportPath(workspaceRoot, sourceFile, specifier) {
309
+ // ── Skip clearly external / unresolvable specifiers ──
310
+ // Node built-ins (fs, path, etc.), npm packages, protocol imports
311
+ if (specifier.startsWith('node:') ||
312
+ specifier.startsWith('bun:') ||
313
+ specifier.startsWith('deno:') ||
314
+ specifier.startsWith('package:') || // Dart packages
315
+ specifier.startsWith('dart:') || // Dart SDK
316
+ specifier.startsWith('http://') ||
317
+ specifier.startsWith('https://')) {
318
+ return null;
319
+ }
320
+ const isRelative = specifier.startsWith('.') || specifier.startsWith('/');
321
+ // ── Try alias expansion first ──
322
+ if (!isRelative) {
323
+ const aliases = await loadPathAliases(workspaceRoot);
324
+ let expanded = null;
325
+ for (const alias of aliases) {
326
+ if (specifier.startsWith(alias.prefix)) {
327
+ const remainder = specifier.slice(alias.prefix.length);
328
+ // Try each target mapping (usually just one)
329
+ for (const target of alias.targets) {
330
+ const candidate = path.join(workspaceRoot, target, remainder);
331
+ const resolved = await probeFilePath(candidate);
332
+ if (resolved) {
333
+ expanded = path.relative(workspaceRoot, resolved);
334
+ break;
335
+ }
336
+ }
337
+ if (expanded)
338
+ break;
339
+ }
340
+ }
341
+ if (expanded)
342
+ return normalizeSlashes(expanded);
343
+ // If it's not relative and no alias matched, it's likely an external package
344
+ // But still try to resolve it from workspace root as a last resort
345
+ // (handles cases like monorepo internal packages)
346
+ const fromRoot = path.join(workspaceRoot, specifier);
347
+ const rootResolved = await probeFilePath(fromRoot);
348
+ if (rootResolved) {
349
+ return normalizeSlashes(path.relative(workspaceRoot, rootResolved));
350
+ }
351
+ return null; // External package
352
+ }
353
+ // ── Resolve relative path ──
354
+ const sourceDir = path.dirname(path.join(workspaceRoot, sourceFile));
355
+ const absoluteCandidate = path.resolve(sourceDir, specifier);
356
+ // Security: ensure it's still within workspace
357
+ if (!absoluteCandidate.startsWith(workspaceRoot)) {
358
+ return null;
359
+ }
360
+ const resolved = await probeFilePath(absoluteCandidate);
361
+ if (resolved) {
362
+ return normalizeSlashes(path.relative(workspaceRoot, resolved));
363
+ }
364
+ return null;
365
+ }
366
+ /**
367
+ * Given an absolute path (possibly without extension), probes for
368
+ * the actual file by trying the path as-is, then with common extensions,
369
+ * then as a directory with index files.
370
+ */
371
+ async function probeFilePath(candidate) {
372
+ // 1. Try exact path
373
+ if (await fileExists(candidate)) {
374
+ const stat = await fs.stat(candidate);
375
+ if (stat.isFile())
376
+ return candidate;
377
+ // It's a directory — fall through to index probing
378
+ }
379
+ // 2. Try appending extensions
380
+ for (const ext of PROBE_EXTENSIONS) {
381
+ const withExt = candidate + ext;
382
+ if (await fileExists(withExt)) {
383
+ return withExt;
384
+ }
385
+ }
386
+ // 3. Try as directory with index files
387
+ for (const indexFile of INDEX_FILES) {
388
+ const withIndex = path.join(candidate, indexFile);
389
+ if (await fileExists(withIndex)) {
390
+ return withIndex;
391
+ }
392
+ }
393
+ // 4. For CSS imports, try prepending underscore (SCSS partials: _file.scss)
394
+ const basename = path.basename(candidate);
395
+ if (!basename.startsWith('_')) {
396
+ const dir = path.dirname(candidate);
397
+ for (const ext of ['.scss', '.sass', '.less', '.css']) {
398
+ const partial = path.join(dir, `_${basename}${ext}`);
399
+ if (await fileExists(partial)) {
400
+ return partial;
401
+ }
402
+ // Also try if basename already has extension
403
+ if (basename.endsWith(ext)) {
404
+ const partialWithExt = path.join(dir, `_${basename}`);
405
+ if (await fileExists(partialWithExt)) {
406
+ return partialWithExt;
407
+ }
408
+ }
409
+ }
410
+ }
411
+ return null;
412
+ }
413
+ async function fileExists(filePath) {
414
+ try {
415
+ await fs.access(filePath);
416
+ return true;
417
+ }
418
+ catch {
419
+ return false;
420
+ }
421
+ }
422
+ function normalizeSlashes(p) {
423
+ return p.replace(/\\/g, '/');
424
+ }
425
+ // ─── Workspace Walking ───────────────────────────────────────────────
426
+ /**
427
+ * Walks the workspace recursively, collecting all source files.
428
+ * Respects the same ignore rules as agent-tools.ts.
429
+ */
430
+ async function walkWorkspace(workspaceRoot) {
431
+ const files = [];
432
+ // Parse .gitignore for supplemental ignore rules
433
+ const extraIgnored = new Set();
434
+ try {
435
+ const gitignoreContent = await fs.readFile(path.join(workspaceRoot, '.gitignore'), 'utf-8');
436
+ const lines = gitignoreContent
437
+ .split('\n')
438
+ .map((l) => l.trim())
439
+ .filter((l) => l && !l.startsWith('#'));
440
+ for (const line of lines) {
441
+ const clean = line.replace(/^\//, '').replace(/\/$/, '');
442
+ if (!clean.includes('*')) {
443
+ extraIgnored.add(clean);
444
+ }
445
+ }
446
+ }
447
+ catch {
448
+ // No .gitignore
449
+ }
450
+ async function walk(dir) {
451
+ let entries;
452
+ try {
453
+ entries = await fs.readdir(dir, { withFileTypes: true });
454
+ }
455
+ catch {
456
+ return; // Permission denied or other error — skip silently
457
+ }
458
+ for (const entry of entries) {
459
+ const name = entry.name;
460
+ // Skip hidden entries
461
+ if (name.startsWith('.'))
462
+ continue;
463
+ if (entry.isDirectory()) {
464
+ if (WALK_IGNORED_DIRS.has(name) || extraIgnored.has(name))
465
+ continue;
466
+ await walk(path.join(dir, name));
467
+ }
468
+ else if (entry.isFile()) {
469
+ if (WALK_IGNORED_FILES.has(name) || extraIgnored.has(name))
470
+ continue;
471
+ // Skip binary/generated extensions
472
+ const ext = path.extname(name).toLowerCase();
473
+ if (EXCLUDED_EXT_SET.has(ext))
474
+ continue;
475
+ // Only process files we have profiles for (source code)
476
+ if (!PROFILE_BY_EXT.has(ext))
477
+ continue;
478
+ const relPath = normalizeSlashes(path.relative(workspaceRoot, path.join(dir, name)));
479
+ files.push(relPath);
480
+ }
481
+ }
482
+ }
483
+ await walk(workspaceRoot);
484
+ return files;
485
+ }
486
+ // ─── Graph Builder ───────────────────────────────────────────────────
487
+ /**
488
+ * Builds a complete bidirectional dependency graph of the workspace.
489
+ *
490
+ * Performance: This is pure regex + filesystem walking — no AST parsing.
491
+ * For a typical project (<5,000 source files), this completes in <1s.
492
+ * The graph is ephemeral: built once per `gatherContext` call and discarded.
493
+ */
494
+ export async function buildDependencyGraph(workspaceRoot) {
495
+ const nodes = new Map();
496
+ function getOrCreate(filePath) {
497
+ let node = nodes.get(filePath);
498
+ if (!node) {
499
+ node = { imports: new Set(), importedBy: new Set() };
500
+ nodes.set(filePath, node);
501
+ }
502
+ return node;
503
+ }
504
+ // 1. Walk workspace to find all source files
505
+ const sourceFiles = await walkWorkspace(workspaceRoot);
506
+ // 2. Process each file: extract imports → resolve paths → populate graph
507
+ await Promise.all(sourceFiles.map(async (relPath) => {
508
+ const absPath = path.join(workspaceRoot, relPath);
509
+ const ext = path.extname(relPath).toLowerCase();
510
+ let content;
511
+ try {
512
+ content = await fs.readFile(absPath, 'utf-8');
513
+ }
514
+ catch {
515
+ return; // Unreadable file — skip
516
+ }
517
+ const rawImports = extractImports(content, ext);
518
+ const sourceNode = getOrCreate(relPath);
519
+ for (const specifier of rawImports) {
520
+ const resolved = await resolveImportPath(workspaceRoot, relPath, specifier);
521
+ if (resolved) {
522
+ sourceNode.imports.add(resolved);
523
+ const targetNode = getOrCreate(resolved);
524
+ targetNode.importedBy.add(relPath);
525
+ }
526
+ }
527
+ }));
528
+ // 3. Return the graph with query methods
529
+ return {
530
+ nodes,
531
+ getImports(filePath) {
532
+ return Array.from(nodes.get(filePath)?.imports || []);
533
+ },
534
+ getImportedBy(filePath) {
535
+ return Array.from(nodes.get(filePath)?.importedBy || []);
536
+ },
537
+ getReverseDependencyTree(filePath, maxDepth = 3) {
538
+ return bfsTraverse(nodes, filePath, 'importedBy', maxDepth);
539
+ },
540
+ getForwardDependencyTree(filePath, maxDepth = 3) {
541
+ return bfsTraverse(nodes, filePath, 'imports', maxDepth);
542
+ },
543
+ };
544
+ }
545
+ /**
546
+ * BFS traversal of the dependency graph in a given direction.
547
+ * Returns all unique files reachable from `startFile` within `maxDepth` hops.
548
+ * The start file itself is NOT included in the result.
549
+ */
550
+ function bfsTraverse(nodes, startFile, direction, maxDepth) {
551
+ const visited = new Set();
552
+ visited.add(startFile); // Exclude the starting file from results
553
+ const queue = [{ file: startFile, depth: 0 }];
554
+ const result = [];
555
+ while (queue.length > 0) {
556
+ const { file, depth } = queue.shift();
557
+ if (depth >= maxDepth)
558
+ continue;
559
+ const node = nodes.get(file);
560
+ if (!node)
561
+ continue;
562
+ const neighbors = node[direction];
563
+ for (const neighbor of neighbors) {
564
+ if (!visited.has(neighbor)) {
565
+ visited.add(neighbor);
566
+ result.push(neighbor);
567
+ queue.push({ file: neighbor, depth: depth + 1 });
568
+ }
569
+ }
570
+ }
571
+ return result;
572
+ }
573
+ /**
574
+ * High-level function exposed as a tool to the agents.
575
+ * Builds the dependency graph (or reuses if already built in this invocation),
576
+ * then queries it for the specified file.
577
+ */
578
+ export async function findDependencies(workspaceRoot, filePath, direction = 'both', maxDepth = 3) {
579
+ const graph = await buildDependencyGraph(workspaceRoot);
580
+ const forwardDeps = direction !== 'reverse' ? graph.getImports(filePath) : [];
581
+ const reverseDeps = direction !== 'forward' ? graph.getImportedBy(filePath) : [];
582
+ const forwardTree = direction !== 'reverse' ? graph.getForwardDependencyTree(filePath, maxDepth) : [];
583
+ const reverseTree = direction !== 'forward' ? graph.getReverseDependencyTree(filePath, maxDepth) : [];
584
+ return { filePath, forwardDeps, reverseDeps, forwardTree, reverseTree };
585
+ }
586
+ /**
587
+ * Formats a FindDependenciesResult into a human-readable string
588
+ * suitable for feeding back to the LLM.
589
+ */
590
+ export function formatDependencyResult(result) {
591
+ const lines = [];
592
+ lines.push(`Dependency analysis for: ${result.filePath}`);
593
+ lines.push('');
594
+ if (result.forwardDeps.length > 0 || result.forwardTree.length > 0) {
595
+ lines.push('─── Direct Imports (what this file imports) ───');
596
+ if (result.forwardDeps.length > 0) {
597
+ for (const dep of result.forwardDeps) {
598
+ lines.push(` → ${dep}`);
599
+ }
600
+ }
601
+ else {
602
+ lines.push(' (none)');
603
+ }
604
+ lines.push('');
605
+ if (result.forwardTree.length > result.forwardDeps.length) {
606
+ lines.push('─── Transitive Import Tree ───');
607
+ for (const dep of result.forwardTree) {
608
+ const isDirect = result.forwardDeps.includes(dep);
609
+ lines.push(` ${isDirect ? '→' : '↳'} ${dep}${isDirect ? '' : ' (transitive)'}`);
610
+ }
611
+ lines.push('');
612
+ }
613
+ }
614
+ if (result.reverseDeps.length > 0 || result.reverseTree.length > 0) {
615
+ lines.push('─── Direct Dependents (files that import this file) ───');
616
+ if (result.reverseDeps.length > 0) {
617
+ for (const dep of result.reverseDeps) {
618
+ lines.push(` ← ${dep}`);
619
+ }
620
+ }
621
+ else {
622
+ lines.push(' (none)');
623
+ }
624
+ lines.push('');
625
+ if (result.reverseTree.length > result.reverseDeps.length) {
626
+ lines.push('─── Transitive Dependent Tree ───');
627
+ for (const dep of result.reverseTree) {
628
+ const isDirect = result.reverseDeps.includes(dep);
629
+ lines.push(` ${isDirect ? '←' : '↰'} ${dep}${isDirect ? '' : ' (transitive)'}`);
630
+ }
631
+ lines.push('');
632
+ }
633
+ }
634
+ if (result.forwardDeps.length === 0 && result.reverseDeps.length === 0) {
635
+ lines.push('No dependencies found. This file appears to be standalone (no imports and nothing imports it).');
636
+ }
637
+ const totalImpact = new Set([...result.forwardTree, ...result.reverseTree]).size;
638
+ lines.push(`Total impact radius: ${totalImpact} file(s)`);
639
+ return lines.join('\n');
640
+ }
641
+ /**
642
+ * Resets the tsconfig alias cache (useful between workspace changes).
643
+ */
644
+ export function resetAliasCache() {
645
+ cachedAliases = null;
646
+ cachedAliasRoot = null;
647
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comprehensive list of binary/generated file extensions that grep should skip.
3
+ * Covers images, video, audio, fonts, archives, documents, compiled artifacts,
4
+ * IDE files, and tool-specific generated files across all major languages.
5
+ * These are used to prevent commands like `grep` from searching through
6
+ * non-textual or compiled files, which can lead to incorrect results or errors.
7
+ */
8
+ export declare const EXCLUDED_EXTENSIONS: string[];