@theihtisham/agent-shadow-brain 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +837 -73
  2. package/dist/adapters/aider.d.ts +11 -0
  3. package/dist/adapters/aider.d.ts.map +1 -0
  4. package/dist/adapters/aider.js +149 -0
  5. package/dist/adapters/aider.js.map +1 -0
  6. package/dist/adapters/index.d.ts +3 -1
  7. package/dist/adapters/index.d.ts.map +1 -1
  8. package/dist/adapters/index.js +5 -3
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/roo-code.d.ts +14 -0
  11. package/dist/adapters/roo-code.d.ts.map +1 -0
  12. package/dist/adapters/roo-code.js +186 -0
  13. package/dist/adapters/roo-code.js.map +1 -0
  14. package/dist/brain/adr-engine.d.ts +58 -0
  15. package/dist/brain/adr-engine.d.ts.map +1 -0
  16. package/dist/brain/adr-engine.js +400 -0
  17. package/dist/brain/adr-engine.js.map +1 -0
  18. package/dist/brain/code-similarity.d.ts +43 -0
  19. package/dist/brain/code-similarity.d.ts.map +1 -0
  20. package/dist/brain/code-similarity.js +227 -0
  21. package/dist/brain/code-similarity.js.map +1 -0
  22. package/dist/brain/context-completion.d.ts +39 -0
  23. package/dist/brain/context-completion.d.ts.map +1 -0
  24. package/dist/brain/context-completion.js +851 -0
  25. package/dist/brain/context-completion.js.map +1 -0
  26. package/dist/brain/dependency-graph.d.ts +35 -0
  27. package/dist/brain/dependency-graph.d.ts.map +1 -0
  28. package/dist/brain/dependency-graph.js +310 -0
  29. package/dist/brain/dependency-graph.js.map +1 -0
  30. package/dist/brain/learning-engine.d.ts +54 -0
  31. package/dist/brain/learning-engine.d.ts.map +1 -0
  32. package/dist/brain/learning-engine.js +855 -0
  33. package/dist/brain/learning-engine.js.map +1 -0
  34. package/dist/brain/mcp-server.d.ts +30 -0
  35. package/dist/brain/mcp-server.d.ts.map +1 -0
  36. package/dist/brain/mcp-server.js +408 -0
  37. package/dist/brain/mcp-server.js.map +1 -0
  38. package/dist/brain/multi-project.d.ts +13 -0
  39. package/dist/brain/multi-project.d.ts.map +1 -0
  40. package/dist/brain/multi-project.js +163 -0
  41. package/dist/brain/multi-project.js.map +1 -0
  42. package/dist/brain/neural-mesh.d.ts +69 -0
  43. package/dist/brain/neural-mesh.d.ts.map +1 -0
  44. package/dist/brain/neural-mesh.js +677 -0
  45. package/dist/brain/neural-mesh.js.map +1 -0
  46. package/dist/brain/orchestrator.d.ts +111 -1
  47. package/dist/brain/orchestrator.d.ts.map +1 -1
  48. package/dist/brain/orchestrator.js +302 -0
  49. package/dist/brain/orchestrator.js.map +1 -1
  50. package/dist/brain/perf-profiler.d.ts +14 -0
  51. package/dist/brain/perf-profiler.d.ts.map +1 -0
  52. package/dist/brain/perf-profiler.js +289 -0
  53. package/dist/brain/perf-profiler.js.map +1 -0
  54. package/dist/brain/semantic-analyzer.d.ts +46 -0
  55. package/dist/brain/semantic-analyzer.d.ts.map +1 -0
  56. package/dist/brain/semantic-analyzer.js +496 -0
  57. package/dist/brain/semantic-analyzer.js.map +1 -0
  58. package/dist/brain/team-mode.d.ts +27 -0
  59. package/dist/brain/team-mode.d.ts.map +1 -0
  60. package/dist/brain/team-mode.js +262 -0
  61. package/dist/brain/team-mode.js.map +1 -0
  62. package/dist/brain/type-safety.d.ts +13 -0
  63. package/dist/brain/type-safety.d.ts.map +1 -0
  64. package/dist/brain/type-safety.js +217 -0
  65. package/dist/brain/type-safety.js.map +1 -0
  66. package/dist/cli.js +593 -3
  67. package/dist/cli.js.map +1 -1
  68. package/dist/index.d.ts +15 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +18 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/types.d.ts +228 -0
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +2 -2
@@ -0,0 +1,851 @@
1
+ // src/brain/context-completion.ts — Context Completion Engine for Shadow Brain
2
+ // Analyzes project to build knowledge, identify gaps, and persist context.
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ /** Read directory entries safely, returning empty on failure. */
6
+ async function readDirSafe(dirPath) {
7
+ try {
8
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
9
+ return entries.map((e) => ({
10
+ name: e.name,
11
+ fullPath: path.join(dirPath, e.name),
12
+ isDir: e.isDirectory(),
13
+ }));
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ /** Read a text file safely, returning null on failure. */
20
+ async function readFileSafe(filePath) {
21
+ try {
22
+ return await fs.readFile(filePath, 'utf-8');
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** Check whether a file exists. */
29
+ async function fileExists(filePath) {
30
+ try {
31
+ const stat = await fs.stat(filePath);
32
+ return stat.isFile();
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Check whether a directory exists. */
39
+ async function dirExists(dirPath) {
40
+ try {
41
+ const stat = await fs.stat(dirPath);
42
+ return stat.isDirectory();
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ // ── ContextCompletionEngine ──────────────────────────────────────────────────
49
+ export class ContextCompletionEngine {
50
+ constructor(projectDir) {
51
+ this.projectDir = projectDir;
52
+ }
53
+ // ── Public API ────────────────────────────────────────────────────────────
54
+ /**
55
+ * Analyze the project directory to build a comprehensive ProjectKnowledge
56
+ * object containing name, conventions, architecture, patterns, and deps.
57
+ */
58
+ async buildKnowledge() {
59
+ const [name, conventions, archResult, patternsResult, deps] = await Promise.all([
60
+ this.detectProjectName(),
61
+ this.detectConventions(),
62
+ this.detectArchitecture(),
63
+ this.detectPatterns(),
64
+ this.detectDependencies(),
65
+ ]);
66
+ return {
67
+ name,
68
+ conventions,
69
+ architecture: archResult.description,
70
+ commonPatterns: patternsResult.common,
71
+ avoidPatterns: patternsResult.avoid,
72
+ dependencies: deps,
73
+ lastUpdated: new Date(),
74
+ };
75
+ }
76
+ /**
77
+ * Persist the knowledge object to `.shadow-brain/knowledge.json` inside
78
+ * the project directory.
79
+ */
80
+ async saveKnowledge(knowledge) {
81
+ const brainDir = path.join(this.projectDir, '.shadow-brain');
82
+ await fs.mkdir(brainDir, { recursive: true });
83
+ const filePath = path.join(brainDir, 'knowledge.json');
84
+ const data = JSON.stringify(knowledge, null, 2);
85
+ await fs.writeFile(filePath, data, 'utf-8');
86
+ }
87
+ /**
88
+ * Identify missing project context items (README, .gitignore, tsconfig,
89
+ * CI/CD configs, etc.) and return them as prioritised BrainInsight array.
90
+ */
91
+ async getContextGaps(knowledge) {
92
+ const gaps = [];
93
+ const now = new Date();
94
+ // 1. No README.md → critical
95
+ if (!(await this.hasFile('README.md')) && !(await this.hasFile('README'))) {
96
+ gaps.push({
97
+ type: 'warning',
98
+ priority: 'critical',
99
+ title: 'Missing README.md',
100
+ content: 'The project has no README.md file. A README is essential for onboarding, documentation, and discoverability. Add one with a project description, setup instructions, and usage examples.',
101
+ timestamp: now,
102
+ });
103
+ }
104
+ // 2. No .gitignore → critical
105
+ if (!(await this.hasFile('.gitignore'))) {
106
+ gaps.push({
107
+ type: 'warning',
108
+ priority: 'critical',
109
+ title: 'Missing .gitignore',
110
+ content: 'No .gitignore file found. Without it, build artifacts, node_modules, secrets, and editor configs may be committed accidentally. Create a .gitignore appropriate for this project type.',
111
+ timestamp: now,
112
+ });
113
+ }
114
+ // 3. No CONTRIBUTING.md in open-source projects → medium
115
+ if (!(await this.hasFile('CONTRIBUTING.md'))) {
116
+ // Heuristic: if there's a LICENSE file, assume open-source
117
+ const hasLicense = (await this.hasFile('LICENSE')) ||
118
+ (await this.hasFile('LICENSE.md')) ||
119
+ (await this.hasFile('LICENSE.txt'));
120
+ if (hasLicense) {
121
+ gaps.push({
122
+ type: 'suggestion',
123
+ priority: 'medium',
124
+ title: 'Missing CONTRIBUTING.md',
125
+ content: 'This project has a license (suggesting open-source) but no CONTRIBUTING.md. Adding contribution guidelines helps the community participate effectively.',
126
+ timestamp: now,
127
+ });
128
+ }
129
+ }
130
+ // 4. No tsconfig.json for TS files → high
131
+ const hasTsFiles = await this.hasFileExtension('.ts');
132
+ if (hasTsFiles && !(await this.hasFile('tsconfig.json'))) {
133
+ gaps.push({
134
+ type: 'warning',
135
+ priority: 'high',
136
+ title: 'Missing tsconfig.json',
137
+ content: 'TypeScript files were found but no tsconfig.json exists. A tsconfig is required for proper type-checking, module resolution, and compiler options.',
138
+ timestamp: now,
139
+ });
140
+ }
141
+ // 5. No .env.example when .env exists → high (security)
142
+ if (await this.hasFile('.env')) {
143
+ if (!(await this.hasFile('.env.example')) &&
144
+ !(await this.hasFile('.env.sample')) &&
145
+ !(await this.hasFile('.env.template'))) {
146
+ gaps.push({
147
+ type: 'warning',
148
+ priority: 'high',
149
+ title: 'Missing .env.example (security risk)',
150
+ content: 'A .env file exists but there is no .env.example. Collaborators may not know which environment variables are required. Create .env.example with dummy values so the project is documented and secrets are not accidentally shared.',
151
+ timestamp: now,
152
+ });
153
+ }
154
+ }
155
+ // 6. No LICENSE file → medium
156
+ if (!(await this.hasFile('LICENSE')) &&
157
+ !(await this.hasFile('LICENSE.md')) &&
158
+ !(await this.hasFile('LICENSE.txt'))) {
159
+ gaps.push({
160
+ type: 'suggestion',
161
+ priority: 'medium',
162
+ title: 'Missing LICENSE file',
163
+ content: 'No license file detected. Without a license, the project defaults to full copyright reservation, which may prevent others from using or contributing. Add a LICENSE file (e.g., MIT, Apache-2.0).',
164
+ timestamp: now,
165
+ });
166
+ }
167
+ // 7. No CI/CD config → low
168
+ const hasCI = (await this.hasFile('.github/workflows')) ||
169
+ (await this.hasFile('.gitlab-ci.yml')) ||
170
+ (await this.hasFile('.circleci')) ||
171
+ (await this.hasFile('Jenkinsfile')) ||
172
+ (await this.hasFile('azure-pipelines.yml'));
173
+ if (!hasCI) {
174
+ gaps.push({
175
+ type: 'suggestion',
176
+ priority: 'low',
177
+ title: 'No CI/CD configuration',
178
+ content: 'No CI/CD pipeline configuration found. Adding automated testing and deployment (e.g., GitHub Actions) catches regressions early and improves code quality.',
179
+ timestamp: now,
180
+ });
181
+ }
182
+ // 8. Missing type definitions (no @types packages for TS) → medium
183
+ if (hasTsFiles) {
184
+ const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
185
+ if (pkgContent) {
186
+ try {
187
+ const pkg = JSON.parse(pkgContent);
188
+ const allDeps = {
189
+ ...(pkg.dependencies || {}),
190
+ ...(pkg.devDependencies || {}),
191
+ };
192
+ // Check for common libraries that usually need @types
193
+ const typeCandidates = {
194
+ express: '@types/express',
195
+ node: '@types/node',
196
+ jest: '@types/jest',
197
+ react: '@types/react',
198
+ lodash: '@types/lodash',
199
+ mongoose: '@types/mongoose',
200
+ cors: '@types/cors',
201
+ };
202
+ const missingTypes = [];
203
+ for (const [dep, typesPkg] of Object.entries(typeCandidates)) {
204
+ if (allDeps[dep] && !allDeps[typesPkg]) {
205
+ missingTypes.push(typesPkg);
206
+ }
207
+ }
208
+ if (missingTypes.length > 0) {
209
+ gaps.push({
210
+ type: 'suggestion',
211
+ priority: 'medium',
212
+ title: 'Missing @types packages',
213
+ content: `The following type definition packages are recommended but not installed: ${missingTypes.join(', ')}. Install them with: npm i -D ${missingTypes.join(' ')}`,
214
+ timestamp: now,
215
+ });
216
+ }
217
+ }
218
+ catch {
219
+ // Invalid package.json, skip this check
220
+ }
221
+ }
222
+ }
223
+ // 9. No test configuration → medium
224
+ const hasTestConfig = (await this.hasFile('jest.config.js')) ||
225
+ (await this.hasFile('jest.config.ts')) ||
226
+ (await this.hasFile('vitest.config.ts')) ||
227
+ (await this.hasFile('vitest.config.js')) ||
228
+ (await this.hasFile('mocha.opts')) ||
229
+ (await this.hasFile('.mocharc.yml')) ||
230
+ (await this.hasFile('.mocharc.json')) ||
231
+ (await this.hasFile('karma.conf.js')) ||
232
+ (await this.hasFile('pytest.ini')) ||
233
+ (await this.hasFile('pyproject.toml'));
234
+ const hasTestDir = (await dirExists(path.join(this.projectDir, 'test'))) ||
235
+ (await dirExists(path.join(this.projectDir, 'tests'))) ||
236
+ (await dirExists(path.join(this.projectDir, '__tests__'))) ||
237
+ (await dirExists(path.join(this.projectDir, 'spec')));
238
+ if (!hasTestConfig && !hasTestDir) {
239
+ gaps.push({
240
+ type: 'suggestion',
241
+ priority: 'medium',
242
+ title: 'No test configuration found',
243
+ content: 'No test framework configuration or test directories detected. Adding tests (unit, integration) is critical for maintainability and confidence in refactoring.',
244
+ timestamp: now,
245
+ });
246
+ }
247
+ // 10. Empty conventions → low
248
+ if (knowledge.conventions.length === 0) {
249
+ gaps.push({
250
+ type: 'suggestion',
251
+ priority: 'low',
252
+ title: 'No coding conventions detected',
253
+ content: 'No linting, formatting, or style configuration files were found. Consider adding ESLint, Prettier, or an EditorConfig to enforce consistent code style across the project.',
254
+ timestamp: now,
255
+ });
256
+ }
257
+ return gaps;
258
+ }
259
+ // ── Private: Name Detection ───────────────────────────────────────────────
260
+ async detectProjectName() {
261
+ // Try package.json first
262
+ const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
263
+ if (pkgContent) {
264
+ try {
265
+ const pkg = JSON.parse(pkgContent);
266
+ if (pkg.name && typeof pkg.name === 'string') {
267
+ // Strip scope prefix: @scope/name → name
268
+ return pkg.name.replace(/^@[^/]+\//, '');
269
+ }
270
+ }
271
+ catch {
272
+ // Fall through
273
+ }
274
+ }
275
+ // Fallback to directory name
276
+ return path.basename(this.projectDir);
277
+ }
278
+ // ── Private: Convention Detection ─────────────────────────────────────────
279
+ async detectConventions() {
280
+ const conventions = [];
281
+ // ESLint
282
+ const eslintFiles = [
283
+ '.eslintrc',
284
+ '.eslintrc.js',
285
+ '.eslintrc.json',
286
+ '.eslintrc.yml',
287
+ '.eslintrc.yaml',
288
+ 'eslint.config.js',
289
+ 'eslint.config.mjs',
290
+ 'eslint.config.ts',
291
+ ];
292
+ for (const f of eslintFiles) {
293
+ const content = await readFileSafe(path.join(this.projectDir, f));
294
+ if (content) {
295
+ conventions.push(this.summarizeEslint(content, f));
296
+ break; // Only one ESLint config matters
297
+ }
298
+ }
299
+ // Also check package.json for eslintConfig
300
+ const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
301
+ if (pkgContent && !conventions.some((c) => c.startsWith('ESLint'))) {
302
+ try {
303
+ const pkg = JSON.parse(pkgContent);
304
+ if (pkg.eslintConfig) {
305
+ conventions.push(this.summarizeEslint(JSON.stringify(pkg.eslintConfig), 'package.json#eslintConfig'));
306
+ }
307
+ }
308
+ catch {
309
+ // skip
310
+ }
311
+ }
312
+ // Prettier
313
+ const prettierFiles = ['.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml', '.prettierrc.yaml'];
314
+ for (const f of prettierFiles) {
315
+ const content = await readFileSafe(path.join(this.projectDir, f));
316
+ if (content) {
317
+ conventions.push(this.summarizePrettier(content, f));
318
+ break;
319
+ }
320
+ }
321
+ // Check package.json for prettier config
322
+ if (pkgContent && !conventions.some((c) => c.startsWith('Prettier'))) {
323
+ try {
324
+ const pkg = JSON.parse(pkgContent);
325
+ if (pkg.prettier) {
326
+ conventions.push(this.summarizePrettier(JSON.stringify(pkg.prettier), 'package.json#prettier'));
327
+ }
328
+ }
329
+ catch {
330
+ // skip
331
+ }
332
+ }
333
+ // EditorConfig
334
+ if (await fileExists(path.join(this.projectDir, '.editorconfig'))) {
335
+ const content = await readFileSafe(path.join(this.projectDir, '.editorconfig'));
336
+ if (content) {
337
+ conventions.push(this.summarizeEditorConfig(content));
338
+ }
339
+ }
340
+ // tsconfig patterns
341
+ const tsconfigContent = await readFileSafe(path.join(this.projectDir, 'tsconfig.json'));
342
+ if (tsconfigContent) {
343
+ conventions.push(this.summarizeTsconfig(tsconfigContent));
344
+ }
345
+ return conventions;
346
+ }
347
+ summarizeEslint(content, fileName) {
348
+ const features = ['ESLint configured'];
349
+ try {
350
+ // Attempt to parse as JSON for structured configs
351
+ const cleaned = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
352
+ const parsed = JSON.parse(cleaned);
353
+ if (parsed.extends) {
354
+ const exts = Array.isArray(parsed.extends) ? parsed.extends : [parsed.extends];
355
+ for (const ext of exts) {
356
+ if (typeof ext === 'string') {
357
+ const base = path.basename(ext);
358
+ features.push(`extends ${base}`);
359
+ }
360
+ }
361
+ }
362
+ if (parsed.parser) {
363
+ features.push(`parser: ${parsed.parser}`);
364
+ }
365
+ if (parsed.rules) {
366
+ const ruleNames = Object.keys(parsed.rules);
367
+ features.push(`${ruleNames.length} custom rules`);
368
+ }
369
+ }
370
+ catch {
371
+ // JS/YAML config — summarize from text
372
+ if (/typescript/.test(content))
373
+ features.push('TypeScript support');
374
+ if (/react/.test(content))
375
+ features.push('React plugin');
376
+ if (/import\//.test(content))
377
+ features.push('import plugin');
378
+ }
379
+ return `${features.join(', ')} (${fileName})`;
380
+ }
381
+ summarizePrettier(content, fileName) {
382
+ const settings = ['Prettier configured'];
383
+ try {
384
+ const parsed = JSON.parse(content);
385
+ if (parsed.semi === false)
386
+ settings.push('no semicolons');
387
+ if (parsed.singleQuote)
388
+ settings.push('single quotes');
389
+ if (parsed.tabWidth)
390
+ settings.push(`tab width: ${parsed.tabWidth}`);
391
+ if (parsed.printWidth)
392
+ settings.push(`print width: ${parsed.printWidth}`);
393
+ if (parsed.trailingComma)
394
+ settings.push(`trailing comma: ${parsed.trailingComma}`);
395
+ }
396
+ catch {
397
+ // JS config — best effort text scan
398
+ if (/semi\s*:\s*false/.test(content))
399
+ settings.push('no semicolons');
400
+ if (/singleQuote\s*:\s*true/.test(content))
401
+ settings.push('single quotes');
402
+ }
403
+ return `${settings.join(', ')} (${fileName})`;
404
+ }
405
+ summarizeEditorConfig(content) {
406
+ const settings = ['EditorConfig'];
407
+ if (/indent_style\s*=\s*space/.test(content))
408
+ settings.push('indent: spaces');
409
+ if (/indent_style\s*=\s*tab/.test(content))
410
+ settings.push('indent: tabs');
411
+ if (/end_of_line\s*=\s*lf/.test(content))
412
+ settings.push('LF line endings');
413
+ if (/insert_final_newline\s*=\s*true/.test(content))
414
+ settings.push('final newline');
415
+ if (/charset\s*=\s*utf-8/.test(content))
416
+ settings.push('UTF-8');
417
+ return settings.join(', ');
418
+ }
419
+ summarizeTsconfig(content) {
420
+ const features = ['TypeScript'];
421
+ try {
422
+ const parsed = JSON.parse(content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''));
423
+ const co = parsed.compilerOptions || {};
424
+ if (co.strict)
425
+ features.push('strict mode');
426
+ if (co.esModuleInterop)
427
+ features.push('ESM interop');
428
+ if (co.moduleResolution === 'bundler' || co.moduleResolution === 'node') {
429
+ features.push(`${co.moduleResolution} module resolution`);
430
+ }
431
+ if (co.target)
432
+ features.push(`target: ${co.target}`);
433
+ if (co.module)
434
+ features.push(`module: ${co.module}`);
435
+ if (co.jsx)
436
+ features.push(`JSX: ${co.jsx}`);
437
+ if (co.declaration)
438
+ features.push('declarations enabled');
439
+ if (co.noUncheckedIndexedAccess)
440
+ features.push('unchecked indexed access check');
441
+ if (co.noImplicitReturns)
442
+ features.push('implicit returns check');
443
+ }
444
+ catch {
445
+ // Text-based heuristic
446
+ if (/strict/.test(content))
447
+ features.push('strict mode');
448
+ if (/esModuleInterop/.test(content))
449
+ features.push('ESM interop');
450
+ }
451
+ return features.join(', ');
452
+ }
453
+ // ── Private: Architecture Detection ───────────────────────────────────────
454
+ async detectArchitecture() {
455
+ const rootEntries = await readDirSafe(this.projectDir);
456
+ const dirNames = new Set(rootEntries.filter((e) => e.isDir).map((e) => e.name));
457
+ const fileNames = new Set(rootEntries.filter((e) => !e.isDir).map((e) => e.name));
458
+ // Read package.json for framework/library clues
459
+ const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
460
+ let pkg = {};
461
+ if (pkgContent) {
462
+ try {
463
+ pkg = JSON.parse(pkgContent);
464
+ }
465
+ catch {
466
+ // ignore
467
+ }
468
+ }
469
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
470
+ // Detect architecture patterns
471
+ const parts = [];
472
+ // Monorepo detection
473
+ if (dirNames.has('packages') || dirNames.has('apps')) {
474
+ parts.push('monorepo');
475
+ // Check for turborepo/nx
476
+ if (fileNames.has('turbo.json') || allDeps['turbo'])
477
+ parts.push('Turborepo');
478
+ if (dirNames.has('nx.json') || allDeps['nx'])
479
+ parts.push('Nx');
480
+ }
481
+ // Frontend frameworks
482
+ if (allDeps['next'])
483
+ parts.push('Next.js');
484
+ else if (allDeps['nuxt'] || allDeps['nuxt3'])
485
+ parts.push('Nuxt');
486
+ else if (allDeps['gatsby'])
487
+ parts.push('Gatsby');
488
+ else if (allDeps['react'])
489
+ parts.push('React');
490
+ else if (allDeps['vue'])
491
+ parts.push('Vue');
492
+ else if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
493
+ parts.push('Svelte');
494
+ else if (allDeps['angular'] || allDeps['@angular/core'])
495
+ parts.push('Angular');
496
+ else if (allDeps['astro'])
497
+ parts.push('Astro');
498
+ // Backend frameworks
499
+ if (allDeps['express'])
500
+ parts.push('Express');
501
+ else if (allDeps['fastify'])
502
+ parts.push('Fastify');
503
+ else if (allDeps['koa'])
504
+ parts.push('Koa');
505
+ else if (allDeps['nestjs'] || allDeps['@nestjs/core'])
506
+ parts.push('NestJS');
507
+ else if (allDeps['hono'])
508
+ parts.push('Hono');
509
+ else if (allDeps['hapi'] || allDeps['@hapi/hapi'])
510
+ parts.push('Hapi');
511
+ // Full-stack
512
+ if (allDeps['@remix-run/react'])
513
+ parts.push('Remix');
514
+ // Databases / ORM
515
+ if (allDeps['prisma'] || allDeps['@prisma/client'])
516
+ parts.push('Prisma');
517
+ else if (allDeps['mongoose'])
518
+ parts.push('MongoDB/Mongoose');
519
+ else if (allDeps['typeorm'])
520
+ parts.push('TypeORM');
521
+ else if (allDeps['drizzle-orm'])
522
+ parts.push('Drizzle');
523
+ else if (allDeps['pg'])
524
+ parts.push('PostgreSQL');
525
+ else if (allDeps['mysql2'])
526
+ parts.push('MySQL');
527
+ // Testing
528
+ if (allDeps['jest'])
529
+ parts.push('Jest');
530
+ else if (allDeps['vitest'])
531
+ parts.push('Vitest');
532
+ else if (allDeps['mocha'])
533
+ parts.push('Mocha');
534
+ // Build tools
535
+ if (allDeps['vite'])
536
+ parts.push('Vite');
537
+ else if (allDeps['webpack'] || allDeps['webpack-cli'])
538
+ parts.push('Webpack');
539
+ else if (allDeps['esbuild'])
540
+ parts.push('esbuild');
541
+ else if (allDeps['rollup'])
542
+ parts.push('Rollup');
543
+ else if (allDeps['tsup'])
544
+ parts.push('tsup');
545
+ // Language
546
+ if (fileNames.has('tsconfig.json'))
547
+ parts.unshift('TypeScript');
548
+ else if (this.hasFileExtensionSync(fileNames, '.py'))
549
+ parts.unshift('Python');
550
+ else if (this.hasFileExtensionSync(fileNames, '.go'))
551
+ parts.unshift('Go');
552
+ else if (this.hasFileExtensionSync(fileNames, '.rs'))
553
+ parts.unshift('Rust');
554
+ // Directory structure hints
555
+ if (dirNames.has('src')) {
556
+ if (dirNames.has('src/routes') || dirNames.has('src/controllers')) {
557
+ parts.push('MVC pattern');
558
+ }
559
+ if (dirNames.has('src/components')) {
560
+ parts.push('component-based');
561
+ }
562
+ if (dirNames.has('src/services')) {
563
+ parts.push('service layer');
564
+ }
565
+ if (dirNames.has('src/lib') || dirNames.has('src/utils')) {
566
+ parts.push('utility module');
567
+ }
568
+ }
569
+ if (dirNames.has('api') && dirNames.has('web')) {
570
+ parts.push('API + frontend split');
571
+ }
572
+ // Docker
573
+ if (fileNames.has('Dockerfile') || fileNames.has('docker-compose.yml') || fileNames.has('docker-compose.yaml')) {
574
+ parts.push('Docker');
575
+ }
576
+ const description = parts.length > 0 ? parts.join(' + ') : 'Unknown project structure';
577
+ return { description };
578
+ }
579
+ hasFileExtensionSync(fileNames, ext) {
580
+ return Array.from(fileNames).some((f) => f.endsWith(ext));
581
+ }
582
+ // ── Private: Pattern Detection ────────────────────────────────────────────
583
+ async detectPatterns() {
584
+ const common = [];
585
+ const avoid = [];
586
+ const srcDir = path.join(this.projectDir, 'src');
587
+ const scanDir = (await dirExists(srcDir)) ? srcDir : this.projectDir;
588
+ // Collect source files (limit to 200 for performance)
589
+ const sourceFiles = await this.collectSourceFiles(scanDir, 200);
590
+ if (sourceFiles.length === 0) {
591
+ return { common, avoid };
592
+ }
593
+ // Sample up to 30 files for content analysis
594
+ const sampleFiles = sourceFiles.slice(0, 30);
595
+ const contents = new Map();
596
+ for (const filePath of sampleFiles) {
597
+ const content = await readFileSafe(filePath);
598
+ if (content) {
599
+ contents.set(filePath, content);
600
+ }
601
+ }
602
+ // Analyze import patterns
603
+ const importPatterns = new Map();
604
+ for (const [, content] of Array.from(contents.entries())) {
605
+ const importMatches = Array.from(content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g));
606
+ for (const match of importMatches) {
607
+ const mod = match[1];
608
+ // Normalize relative imports to pattern
609
+ const pattern = mod.startsWith('.')
610
+ ? 'relative-import'
611
+ : mod.startsWith('@')
612
+ ? `scoped-import:${mod.split('/').slice(0, 2).join('/')}`
613
+ : `external-import:${mod.split('/')[0]}`;
614
+ importPatterns.set(pattern, (importPatterns.get(pattern) || 0) + 1);
615
+ }
616
+ // CommonJS require
617
+ const requireMatches = Array.from(content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g));
618
+ for (const match of requireMatches) {
619
+ const mod = match[1];
620
+ const pattern = mod.startsWith('.')
621
+ ? 'relative-require'
622
+ : `external-require:${mod.split('/')[0]}`;
623
+ importPatterns.set(pattern, (importPatterns.get(pattern) || 0) + 1);
624
+ }
625
+ }
626
+ // Report frequent import patterns
627
+ for (const [pattern, count] of Array.from(importPatterns.entries())) {
628
+ if (count >= 3) {
629
+ if (pattern === 'relative-import') {
630
+ common.push('ES module relative imports');
631
+ }
632
+ else if (pattern === 'relative-require') {
633
+ common.push('CommonJS relative requires');
634
+ }
635
+ else {
636
+ common.push(`Uses ${pattern.replace(/-/g, ' ')} (${count} occurrences)`);
637
+ }
638
+ }
639
+ }
640
+ // Code style patterns
641
+ let arrowFnCount = 0;
642
+ let asyncAwaitCount = 0;
643
+ let classCount = 0;
644
+ let exportDefaultCount = 0;
645
+ let namedExportCount = 0;
646
+ let typeOnlyImportCount = 0;
647
+ let consoleLogCount = 0;
648
+ let anyTypeCount = 0;
649
+ let evalCount = 0;
650
+ let varCount = 0;
651
+ for (const [, content] of Array.from(contents.entries())) {
652
+ if (/=>\s*\{/.test(content) || /=>\s*[^\{]/.test(content))
653
+ arrowFnCount++;
654
+ if (/async\s+/.test(content) && /await\s+/.test(content))
655
+ asyncAwaitCount++;
656
+ if (/class\s+\w+/.test(content))
657
+ classCount++;
658
+ if (/export\s+default\s+/.test(content))
659
+ exportDefaultCount++;
660
+ if (/export\s+(const|function|class|interface|type)\s/.test(content))
661
+ namedExportCount++;
662
+ if (/import\s+type\s+/.test(content) || /type\s+.*\s+from/.test(content))
663
+ typeOnlyImportCount++;
664
+ if (/console\.log/.test(content))
665
+ consoleLogCount++;
666
+ if (/:\s*any\b/.test(content))
667
+ anyTypeCount++;
668
+ if (/\beval\s*\(/.test(content))
669
+ evalCount++;
670
+ if (/\bvar\s+\w/.test(content))
671
+ varCount++;
672
+ }
673
+ const fileCount = contents.size || 1;
674
+ if (arrowFnCount > fileCount * 0.3)
675
+ common.push('arrow functions preferred');
676
+ if (asyncAwaitCount > fileCount * 0.2)
677
+ common.push('async/await pattern');
678
+ if (classCount > fileCount * 0.2)
679
+ common.push('class-based modules');
680
+ if (exportDefaultCount > namedExportCount)
681
+ common.push('default exports');
682
+ else if (namedExportCount > exportDefaultCount)
683
+ common.push('named exports');
684
+ if (typeOnlyImportCount > 0)
685
+ common.push('type-only imports (isolatedModules compatible)');
686
+ // Anti-patterns
687
+ if (consoleLogCount > fileCount * 0.5) {
688
+ avoid.push('excessive console.log — consider a proper logging library');
689
+ }
690
+ if (anyTypeCount > fileCount * 0.3) {
691
+ avoid.push('excessive use of `any` type — use specific types for safety');
692
+ }
693
+ if (evalCount > 0) {
694
+ avoid.push('use of eval() — security and performance risk');
695
+ }
696
+ if (varCount > fileCount * 0.2) {
697
+ avoid.push('use of var — prefer const or let');
698
+ }
699
+ // Detect naming conventions from filenames
700
+ const fileBasenames = sourceFiles.map((f) => path.basename(f, path.extname(f)));
701
+ const kebabCount = fileBasenames.filter((n) => /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(n)).length;
702
+ const camelCount = fileBasenames.filter((n) => /^[a-z][a-zA-Z0-9]+$/.test(n)).length;
703
+ const pascalCount = fileBasenames.filter((n) => /^[A-Z][a-zA-Z0-9]+$/.test(n)).length;
704
+ const snakeCount = fileBasenames.filter((n) => /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(n)).length;
705
+ if (kebabCount > fileBasenames.length * 0.5)
706
+ common.push('kebab-case file naming');
707
+ else if (camelCount > fileBasenames.length * 0.5)
708
+ common.push('camelCase file naming');
709
+ else if (pascalCount > fileBasenames.length * 0.5)
710
+ common.push('PascalCase file naming');
711
+ else if (snakeCount > fileBasenames.length * 0.5)
712
+ common.push('snake_case file naming');
713
+ return { common, avoid };
714
+ }
715
+ // ── Private: Dependency Detection ─────────────────────────────────────────
716
+ async detectDependencies() {
717
+ const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
718
+ if (!pkgContent) {
719
+ // Try other package managers
720
+ const cargoContent = await readFileSafe(path.join(this.projectDir, 'Cargo.toml'));
721
+ if (cargoContent) {
722
+ return this.parseCargoDeps(cargoContent);
723
+ }
724
+ const requirementsContent = await readFileSafe(path.join(this.projectDir, 'requirements.txt'));
725
+ if (requirementsContent) {
726
+ return requirementsContent
727
+ .split('\n')
728
+ .map((l) => l.split('==')[0].split('>=')[0].split('~=')[0].trim())
729
+ .filter((l) => l && !l.startsWith('#'));
730
+ }
731
+ const goModContent = await readFileSafe(path.join(this.projectDir, 'go.mod'));
732
+ if (goModContent) {
733
+ return goModContent
734
+ .split('\n')
735
+ .filter((l) => l.includes('/'))
736
+ .map((l) => l.trim().split(' ')[0])
737
+ .filter((l) => l && !l.startsWith('module') && !l.startsWith('go '));
738
+ }
739
+ return [];
740
+ }
741
+ try {
742
+ const pkg = JSON.parse(pkgContent);
743
+ const deps = {
744
+ ...(pkg.dependencies || {}),
745
+ ...(pkg.devDependencies || {}),
746
+ };
747
+ return Object.keys(deps).sort();
748
+ }
749
+ catch {
750
+ return [];
751
+ }
752
+ }
753
+ parseCargoDeps(content) {
754
+ const deps = [];
755
+ let inDeps = false;
756
+ for (const line of content.split('\n')) {
757
+ const trimmed = line.trim();
758
+ if (trimmed === '[dependencies]') {
759
+ inDeps = true;
760
+ continue;
761
+ }
762
+ if (trimmed.startsWith('[')) {
763
+ inDeps = false;
764
+ continue;
765
+ }
766
+ if (inDeps && trimmed.includes('=')) {
767
+ const name = trimmed.split('=')[0].trim();
768
+ if (name)
769
+ deps.push(name);
770
+ }
771
+ }
772
+ return deps;
773
+ }
774
+ // ── Private: File Utilities ───────────────────────────────────────────────
775
+ async hasFile(name) {
776
+ return fileExists(path.join(this.projectDir, name));
777
+ }
778
+ async hasFileExtension(ext) {
779
+ // Check top-level src directory
780
+ const candidates = [this.projectDir];
781
+ const srcDir = path.join(this.projectDir, 'src');
782
+ if (await dirExists(srcDir)) {
783
+ candidates.push(srcDir);
784
+ }
785
+ for (const dir of candidates) {
786
+ const entries = await readDirSafe(dir);
787
+ for (const entry of entries) {
788
+ if (!entry.isDir && entry.name.endsWith(ext)) {
789
+ return true;
790
+ }
791
+ // Check one level deep
792
+ if (entry.isDir && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
793
+ const subEntries = await readDirSafe(entry.fullPath);
794
+ for (const sub of subEntries) {
795
+ if (!sub.isDir && sub.name.endsWith(ext)) {
796
+ return true;
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
802
+ return false;
803
+ }
804
+ /**
805
+ * Recursively collect source files up to a limit.
806
+ * Skips node_modules, .git, dist, build, coverage, and hidden directories.
807
+ */
808
+ async collectSourceFiles(dir, limit) {
809
+ const results = [];
810
+ const skipDirs = new Set([
811
+ 'node_modules',
812
+ '.git',
813
+ 'dist',
814
+ 'build',
815
+ 'coverage',
816
+ '.next',
817
+ '.nuxt',
818
+ '.cache',
819
+ '.turbo',
820
+ '__pycache__',
821
+ 'target',
822
+ 'vendor',
823
+ ]);
824
+ const sourceExtensions = new Set([
825
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
826
+ '.py', '.go', '.rs', '.java', '.rb',
827
+ ]);
828
+ const queue = [dir];
829
+ while (queue.length > 0 && results.length < limit) {
830
+ const current = queue.shift();
831
+ const entries = await readDirSafe(current);
832
+ for (const entry of entries) {
833
+ if (results.length >= limit)
834
+ break;
835
+ if (entry.isDir) {
836
+ if (!entry.name.startsWith('.') && !skipDirs.has(entry.name)) {
837
+ queue.push(entry.fullPath);
838
+ }
839
+ }
840
+ else {
841
+ const ext = path.extname(entry.name);
842
+ if (sourceExtensions.has(ext)) {
843
+ results.push(entry.fullPath);
844
+ }
845
+ }
846
+ }
847
+ }
848
+ return results;
849
+ }
850
+ }
851
+ //# sourceMappingURL=context-completion.js.map