aibridge-context 1.2.0 → 1.3.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.
@@ -5,8 +5,10 @@ const fsp = require('fs/promises');
5
5
  const path = require('path');
6
6
 
7
7
  const CONTEXT_DIR_NAME = '.ai-context';
8
- const MAX_RECENT_UPDATES = 50;
9
- const MAX_CHANGELOG_ENTRIES = 200;
8
+ const MAX_RECENT_UPDATES = 8;
9
+ const MAX_CHANGELOG_ENTRIES = 50;
10
+ const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
11
+ const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
10
12
 
11
13
  const DEFAULT_CONFIG = {
12
14
  port: 3333,
@@ -59,10 +61,14 @@ function isObject(value) {
59
61
  function detectProjectMetadata(projectRoot) {
60
62
  const packageJsonPath = path.join(projectRoot, 'package.json');
61
63
  const packageManager = detectPackageManager(projectRoot);
64
+ const techStack = detectTechStack(projectRoot);
62
65
  const metadata = {
63
66
  project: path.basename(projectRoot),
64
67
  version: '0.1.0',
65
- stackLabel: 'Node.js project',
68
+ techStack: Object.assign({}, techStack, {
69
+ package_manager: packageManager
70
+ }),
71
+ stackLabel: buildStackLabel(techStack),
66
72
  packageManager
67
73
  };
68
74
 
@@ -73,53 +79,134 @@ function detectProjectMetadata(projectRoot) {
73
79
  try {
74
80
  const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
75
81
  const parsedPackage = JSON.parse(rawPackage);
76
- const dependencies = Object.assign(
77
- {},
78
- parsedPackage.dependencies || {},
79
- parsedPackage.devDependencies || {}
80
- );
81
82
 
82
83
  metadata.project = parsedPackage.name || metadata.project;
83
84
  metadata.version = parsedPackage.version || metadata.version;
84
- metadata.stackLabel = describeStack(dependencies);
85
- metadata.packageManager = packageManager;
86
85
  } catch (error) {
87
- metadata.stackLabel = 'Node.js project';
86
+ metadata.stackLabel = buildStackLabel(metadata.techStack);
88
87
  }
89
88
 
90
89
  return metadata;
91
90
  }
92
91
 
93
- function detectPackageManager(projectRoot) {
94
- if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
95
- return 'pnpm';
92
+ function detectTechStack(projectRoot) {
93
+ const packageJsonPath = path.join(projectRoot, 'package.json');
94
+ const pythonMarkers = ['pyproject.toml', 'requirements.txt', 'setup.py'];
95
+ const hasPackageJson = fs.existsSync(packageJsonPath);
96
+ const hasPythonMarker = pythonMarkers.some((marker) =>
97
+ fs.existsSync(path.join(projectRoot, marker))
98
+ );
99
+ let dependencies = {};
100
+
101
+ if (hasPackageJson) {
102
+ try {
103
+ const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
104
+ const parsedPackage = JSON.parse(rawPackage);
105
+ dependencies = Object.assign(
106
+ {},
107
+ parsedPackage.dependencies || {},
108
+ parsedPackage.devDependencies || {}
109
+ );
110
+ } catch (error) {
111
+ dependencies = {};
112
+ }
96
113
  }
97
114
 
98
- if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
99
- return 'yarn';
115
+ let language = '';
116
+ let runtime = '';
117
+
118
+ if (hasPackageJson || hasAnyFileExtension(projectRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
119
+ language = 'Node.js';
120
+ runtime = 'Node.js';
121
+ } else if (hasPythonMarker || hasAnyFileExtension(projectRoot, ['.py'])) {
122
+ language = 'Python';
123
+ runtime = 'Python';
100
124
  }
101
125
 
102
- return 'npm';
126
+ return {
127
+ language,
128
+ framework: detectFramework(dependencies),
129
+ runtime,
130
+ package_manager: detectPackageManager(projectRoot)
131
+ };
103
132
  }
104
133
 
105
- function describeStack(dependencies) {
134
+ function detectFramework(dependencies) {
106
135
  if (dependencies.next) {
107
- return 'Node.js + Next.js';
136
+ return 'Next.js';
108
137
  }
109
138
 
110
139
  if (dependencies.react) {
111
- return 'Node.js + React';
140
+ return 'React';
112
141
  }
113
142
 
114
143
  if (dependencies.express) {
115
- return 'Node.js + Express';
144
+ return 'Express';
145
+ }
146
+
147
+ return '';
148
+ }
149
+
150
+ function buildStackLabel(techStack) {
151
+ const parts = [techStack.language, techStack.framework].filter(Boolean);
152
+ return parts.length > 0 ? parts.join(' + ') : 'Project';
153
+ }
154
+
155
+ function hasAnyFileExtension(projectRoot, extensions) {
156
+ return scanProjectFiles(projectRoot, 2).some((filePath) =>
157
+ extensions.includes(path.extname(filePath).toLowerCase())
158
+ );
159
+ }
160
+
161
+ function scanProjectFiles(projectRoot, maxDepth) {
162
+ const results = [];
163
+
164
+ function visit(currentDir, depth) {
165
+ if (depth > maxDepth) {
166
+ return;
167
+ }
168
+
169
+ let entries = [];
170
+
171
+ try {
172
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
173
+ } catch (error) {
174
+ return;
175
+ }
176
+
177
+ for (const entry of entries) {
178
+ const fullPath = path.join(currentDir, entry.name);
179
+ const relativePath = normalizeProjectPath(path.relative(projectRoot, fullPath));
180
+
181
+ if (entry.isDirectory()) {
182
+ if (shouldIgnoreProjectFile(relativePath)) {
183
+ continue;
184
+ }
185
+
186
+ visit(fullPath, depth + 1);
187
+ continue;
188
+ }
189
+
190
+ if (!shouldIgnoreProjectFile(relativePath)) {
191
+ results.push(relativePath);
192
+ }
193
+ }
116
194
  }
117
195
 
118
- if (dependencies.typescript) {
119
- return 'Node.js + TypeScript';
196
+ visit(projectRoot, 0);
197
+ return results;
198
+ }
199
+
200
+ function detectPackageManager(projectRoot) {
201
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
202
+ return 'pnpm';
203
+ }
204
+
205
+ if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
206
+ return 'yarn';
120
207
  }
121
208
 
122
- return 'Node.js project';
209
+ return 'npm';
123
210
  }
124
211
 
125
212
  async function ensureContextDirectory(projectRoot) {
@@ -150,19 +237,26 @@ async function writeTextAtomic(filePath, content) {
150
237
 
151
238
  function createDefaultState(projectRoot) {
152
239
  const metadata = detectProjectMetadata(projectRoot);
153
-
154
- return {
240
+ const keyFeatures = deriveKeyFeatures(projectRoot);
241
+ const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
242
+ const currentStage = determineCurrentStage(keyFeatures);
243
+ const state = {
155
244
  project: metadata.project,
156
245
  version: metadata.version,
157
246
  last_updated: new Date(0).toISOString(),
158
- stats: {
159
- files_changed: 0,
160
- last_file: ''
161
- },
247
+ ai_summary: '',
248
+ tech_stack: metadata.techStack,
249
+ current_stage: currentStage,
162
250
  recent_updates: [],
163
- features: [],
251
+ key_features: keyFeatures,
252
+ known_issues: knownIssues,
164
253
  next_steps: []
165
254
  };
255
+
256
+ state.ai_summary = generateAiSummary(state);
257
+ state.next_steps = generateNextSteps(state);
258
+
259
+ return state;
166
260
  }
167
261
 
168
262
  function createDefaultChangelog() {
@@ -204,62 +298,575 @@ async function updateProjectState(projectRoot, changeEvent, options) {
204
298
  );
205
299
  const logger = settings.logger;
206
300
  const contextPaths = getContextPaths(projectRoot);
207
- const state = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
208
- const changelog = await readJsonFile(contextPaths.changelogFile, createDefaultChangelog());
301
+ const metadata = detectProjectMetadata(projectRoot);
302
+ const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
303
+ const existingChangelog = await readJsonFile(
304
+ contextPaths.changelogFile,
305
+ createDefaultChangelog()
306
+ );
209
307
  const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
210
308
  const validEvents = normalizedEvents.filter(Boolean);
309
+ const timestamp = determineUpdateTimestamp(validEvents);
310
+ const meaningfulEvents = collapseEventsByFile(
311
+ validEvents.filter((event) => isMeaningfulEvent(event))
312
+ );
313
+ const interpretedEvents = meaningfulEvents
314
+ .map((event) => interpretChange(event))
315
+ .filter(Boolean);
316
+ const previousRecentUpdates = normalizeStoredUpdates(existingState.recent_updates);
317
+ const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
318
+ const recentUpdates = dedupeRecentUpdates(
319
+ interpretedEvents.map(toStateUpdate).concat(previousRecentUpdates)
320
+ ).slice(0, MAX_RECENT_UPDATES);
321
+ const historyEntries = dedupeHistoryEntries(
322
+ interpretedEvents.concat(previousHistoryEntries)
323
+ ).slice(0, MAX_CHANGELOG_ENTRIES);
324
+ const keyFeatures = deriveKeyFeatures(projectRoot);
325
+ const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
326
+ const nextState = {
327
+ project: metadata.project,
328
+ version: metadata.version,
329
+ last_updated: timestamp,
330
+ ai_summary: '',
331
+ tech_stack: metadata.techStack,
332
+ current_stage: determineCurrentStage(keyFeatures),
333
+ recent_updates: recentUpdates,
334
+ key_features: keyFeatures,
335
+ known_issues: knownIssues,
336
+ next_steps: []
337
+ };
338
+
339
+ nextState.ai_summary = generateAiSummary(nextState);
340
+ nextState.next_steps = generateNextSteps(nextState);
341
+
342
+ await writeJsonAtomic(contextPaths.stateFile, nextState);
343
+ await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
211
344
 
212
- if (validEvents.length === 0) {
213
- return state;
345
+ if (logger) {
346
+ logger.debug(`Updated AI context with ${interpretedEvents.length} meaningful change(s).`);
214
347
  }
215
348
 
216
- const latestEvent = validEvents[validEvents.length - 1];
217
- const timestamp = latestEvent.timestamp || new Date().toISOString();
218
- const recentUpdates = Array.isArray(state.recent_updates) ? state.recent_updates.slice() : [];
219
- const changelogEntries = Array.isArray(changelog.entries) ? changelog.entries.slice() : [];
349
+ if (typeof settings.syncCallback === 'function') {
350
+ await settings.syncCallback();
351
+ }
220
352
 
221
- for (const event of validEvents) {
222
- const normalizedEvent = {
223
- timestamp: event.timestamp || timestamp,
224
- action: event.action || 'updated',
225
- file: event.file || ''
226
- };
353
+ return nextState;
354
+ }
227
355
 
228
- recentUpdates.push(normalizedEvent);
229
- changelogEntries.push(normalizedEvent);
356
+ function determineUpdateTimestamp(events) {
357
+ if (!Array.isArray(events) || events.length === 0) {
358
+ return new Date().toISOString();
230
359
  }
231
360
 
232
- const nextState = {
233
- project: state.project || createDefaultState(projectRoot).project,
234
- version: state.version || createDefaultState(projectRoot).version,
235
- last_updated: timestamp,
236
- stats: {
237
- files_changed: (state.stats && typeof state.stats.files_changed === 'number'
238
- ? state.stats.files_changed
239
- : 0) + validEvents.length,
240
- last_file: latestEvent.file || ''
241
- },
242
- recent_updates: recentUpdates.slice(-MAX_RECENT_UPDATES),
243
- features: Array.isArray(state.features) ? state.features : [],
244
- next_steps: Array.isArray(state.next_steps) ? state.next_steps : []
361
+ const latestEvent = events[events.length - 1];
362
+ return latestEvent.timestamp || new Date().toISOString();
363
+ }
364
+
365
+ function normalizeProjectPath(filePath) {
366
+ return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
367
+ }
368
+
369
+ function shouldIgnoreProjectFile(filePath) {
370
+ const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
371
+
372
+ if (!normalizedPath) {
373
+ return false;
374
+ }
375
+
376
+ const segments = normalizedPath.split('/');
377
+ const baseName = segments[segments.length - 1];
378
+
379
+ if (
380
+ segments.includes('node_modules') ||
381
+ segments.includes('.git') ||
382
+ segments.includes('.ai-context') ||
383
+ segments.includes('dist') ||
384
+ segments.includes('build')
385
+ ) {
386
+ return true;
387
+ }
388
+
389
+ if (baseName.startsWith('.start')) {
390
+ return true;
391
+ }
392
+
393
+ if (
394
+ baseName.endsWith('.log') ||
395
+ baseName.endsWith('.tmp') ||
396
+ baseName.endsWith('.lock') ||
397
+ baseName === 'package-lock.json' ||
398
+ baseName === 'yarn.lock' ||
399
+ baseName === 'pnpm-lock.yaml' ||
400
+ /^tmp[._-]/i.test(baseName) ||
401
+ /^temp[._-]/i.test(baseName) ||
402
+ /^debug[._-]/i.test(baseName)
403
+ ) {
404
+ return true;
405
+ }
406
+
407
+ return false;
408
+ }
409
+
410
+ function scoreEvent(filePath) {
411
+ const normalizedPath = normalizeProjectPath(filePath);
412
+ const lowerPath = normalizedPath.toLowerCase();
413
+ const baseName = path.basename(normalizedPath).toLowerCase();
414
+ let score = 0;
415
+
416
+ if (shouldIgnoreProjectFile(normalizedPath)) {
417
+ return -5;
418
+ }
419
+
420
+ if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
421
+ score += 3;
422
+ }
423
+
424
+ if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
425
+ score += 2;
426
+ }
427
+
428
+ if (lowerPath === 'package.json') {
429
+ score += 2;
430
+ }
431
+
432
+ if (lowerPath === 'readme.md') {
433
+ score += 1;
434
+ }
435
+
436
+ return score;
437
+ }
438
+
439
+ function isMeaningfulEvent(event) {
440
+ if (!event || !event.file) {
441
+ return false;
442
+ }
443
+
444
+ return scoreEvent(event.file) >= 2;
445
+ }
446
+
447
+ function collapseEventsByFile(events) {
448
+ const collapsedEvents = new Map();
449
+
450
+ for (const event of events) {
451
+ const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
452
+ collapsedEvents.set(
453
+ normalizedFile,
454
+ Object.assign({}, event, {
455
+ file: normalizeProjectPath(event.file)
456
+ })
457
+ );
458
+ }
459
+
460
+ return Array.from(collapsedEvents.values());
461
+ }
462
+
463
+ function interpretChange(event) {
464
+ const filePath = normalizeProjectPath(event.file);
465
+ const area = classifyChangeArea(filePath);
466
+ const subject = describeChangeSubject(filePath, area);
467
+ const type = mapActionToType(event.action);
468
+ const title = `${mapActionToVerb(event.action)} ${subject}`;
469
+
470
+ return {
471
+ timestamp: event.timestamp || new Date().toISOString(),
472
+ file: filePath,
473
+ title,
474
+ type,
475
+ impact: describeImpact(area, subject, event.action)
245
476
  };
477
+ }
478
+
479
+ function classifyChangeArea(filePath) {
480
+ const lowerPath = filePath.toLowerCase();
481
+
482
+ if (lowerPath === 'package.json') {
483
+ return 'dependencies';
484
+ }
485
+
486
+ if (lowerPath === 'readme.md') {
487
+ return 'documentation';
488
+ }
489
+
490
+ if (lowerPath.startsWith('core/')) {
491
+ return 'logic';
492
+ }
493
+
494
+ if (lowerPath.startsWith('server/')) {
495
+ return 'backend';
496
+ }
497
+
498
+ if (lowerPath.startsWith('bin/')) {
499
+ return 'CLI';
500
+ }
501
+
502
+ if (lowerPath.startsWith('templates/')) {
503
+ return 'templates';
504
+ }
505
+
506
+ return 'project';
507
+ }
508
+
509
+ function describeChangeSubject(filePath, area) {
510
+ const lowerPath = filePath.toLowerCase();
511
+ const baseName = path.basename(filePath);
512
+
513
+ if (lowerPath === 'package.json') {
514
+ return 'dependency configuration';
515
+ }
516
+
517
+ if (lowerPath === 'readme.md') {
518
+ return 'documentation';
519
+ }
520
+
521
+ if (lowerPath === 'core/gitsync.js') {
522
+ return 'GitHub sync logic';
523
+ }
524
+
525
+ if (lowerPath === 'core/statemanager.js') {
526
+ return 'state intelligence logic';
527
+ }
528
+
529
+ if (lowerPath === 'core/watcher.js') {
530
+ return 'watcher logic';
531
+ }
532
+
533
+ if (lowerPath === 'core/init.js') {
534
+ return 'initialization flow';
535
+ }
536
+
537
+ if (lowerPath === 'server/server.js') {
538
+ return 'backend server';
539
+ }
540
+
541
+ if (lowerPath === 'server/routes.js') {
542
+ return 'backend routes';
543
+ }
544
+
545
+ if (lowerPath === 'bin/cli.js') {
546
+ return 'CLI workflow';
547
+ }
548
+
549
+ if (area === 'templates') {
550
+ return `${humanizeFileName(baseName)} template`;
551
+ }
552
+
553
+ if (area === 'logic') {
554
+ return `${humanizeFileName(baseName)} logic`;
555
+ }
556
+
557
+ if (area === 'backend') {
558
+ return `${humanizeFileName(baseName)} backend`;
559
+ }
560
+
561
+ if (area === 'CLI') {
562
+ return 'CLI workflow';
563
+ }
564
+
565
+ return humanizeFileName(baseName);
566
+ }
567
+
568
+ function humanizeFileName(fileName) {
569
+ return fileName
570
+ .replace(path.extname(fileName), '')
571
+ .replace(/[-_.]+/g, ' ')
572
+ .replace(/\s+/g, ' ')
573
+ .trim();
574
+ }
575
+
576
+ function mapActionToType(action) {
577
+ if (action === 'add') {
578
+ return 'feature';
579
+ }
580
+
581
+ if (action === 'delete') {
582
+ return 'removal';
583
+ }
584
+
585
+ return 'improvement';
586
+ }
587
+
588
+ function mapActionToVerb(action) {
589
+ if (action === 'add') {
590
+ return 'Added';
591
+ }
592
+
593
+ if (action === 'delete') {
594
+ return 'Removed';
595
+ }
596
+
597
+ return 'Updated';
598
+ }
599
+
600
+ function describeImpact(area, subject, action) {
601
+ if (action === 'delete') {
602
+ return `Removes ${subject.toLowerCase()} from the project workflow.`;
603
+ }
604
+
605
+ if (subject === 'GitHub sync logic') {
606
+ return 'Improves reliability of context syncing.';
607
+ }
608
+
609
+ if (subject === 'state intelligence logic') {
610
+ return 'Improves the quality of AI-readable project state.';
611
+ }
612
+
613
+ if (subject === 'watcher logic') {
614
+ return 'Improves how meaningful project changes are detected.';
615
+ }
616
+
617
+ if (area === 'backend') {
618
+ return 'Improves local AI context delivery.';
619
+ }
620
+
621
+ if (area === 'CLI') {
622
+ return 'Improves command-line workflow clarity and usability.';
623
+ }
624
+
625
+ if (area === 'documentation') {
626
+ return 'Improves onboarding and usage clarity.';
627
+ }
628
+
629
+ if (area === 'dependencies') {
630
+ return 'Updates package behavior and dependency management.';
631
+ }
632
+
633
+ if (area === 'templates') {
634
+ return 'Improves generated AI context defaults.';
635
+ }
636
+
637
+ return 'Improves core project intelligence and automation.';
638
+ }
639
+
640
+ function normalizeStoredUpdates(updates) {
641
+ if (!Array.isArray(updates)) {
642
+ return [];
643
+ }
644
+
645
+ return dedupeRecentUpdates(
646
+ updates
647
+ .map((update) => normalizeStoredUpdate(update))
648
+ .filter(Boolean)
649
+ );
650
+ }
651
+
652
+ function normalizeStoredUpdate(update) {
653
+ if (!update) {
654
+ return null;
655
+ }
656
+
657
+ if (update.title && update.type && update.impact) {
658
+ return {
659
+ title: update.title,
660
+ type: update.type,
661
+ impact: update.impact
662
+ };
663
+ }
664
+
665
+ if (update.file && update.action && isMeaningfulEvent(update)) {
666
+ return toStateUpdate(interpretChange(update));
667
+ }
668
+
669
+ return null;
670
+ }
671
+
672
+ function normalizeStoredHistoryEntries(entries) {
673
+ if (!Array.isArray(entries)) {
674
+ return [];
675
+ }
676
+
677
+ return dedupeHistoryEntries(
678
+ entries
679
+ .map((entry) => normalizeStoredHistoryEntry(entry))
680
+ .filter(Boolean)
681
+ );
682
+ }
683
+
684
+ function normalizeStoredHistoryEntry(entry) {
685
+ if (!entry) {
686
+ return null;
687
+ }
688
+
689
+ if (entry.title && entry.type && entry.impact) {
690
+ return {
691
+ timestamp: entry.timestamp || new Date(0).toISOString(),
692
+ file: normalizeProjectPath(entry.file || ''),
693
+ title: entry.title,
694
+ type: entry.type,
695
+ impact: entry.impact
696
+ };
697
+ }
698
+
699
+ if (entry.file && entry.action && isMeaningfulEvent(entry)) {
700
+ return interpretChange(entry);
701
+ }
702
+
703
+ return null;
704
+ }
246
705
 
247
- const nextChangelog = {
248
- entries: changelogEntries.slice(-MAX_CHANGELOG_ENTRIES)
706
+ function toStateUpdate(update) {
707
+ return {
708
+ title: update.title,
709
+ type: update.type,
710
+ impact: update.impact
249
711
  };
712
+ }
250
713
 
251
- await writeJsonAtomic(contextPaths.stateFile, nextState);
252
- await writeJsonAtomic(contextPaths.changelogFile, nextChangelog);
714
+ function dedupeRecentUpdates(updates) {
715
+ const seenUpdates = new Set();
716
+ const result = [];
253
717
 
254
- if (logger) {
255
- logger.debug(`Updated AI context for ${validEvents.length} change(s).`);
718
+ for (const update of updates) {
719
+ const key = `${update.title}::${update.type}::${update.impact}`;
720
+
721
+ if (seenUpdates.has(key)) {
722
+ continue;
723
+ }
724
+
725
+ seenUpdates.add(key);
726
+ result.push(update);
256
727
  }
257
728
 
258
- if (typeof settings.syncCallback === 'function') {
259
- await settings.syncCallback();
729
+ return result;
730
+ }
731
+
732
+ function dedupeHistoryEntries(entries) {
733
+ const seenEntries = new Set();
734
+ const result = [];
735
+
736
+ for (const entry of entries) {
737
+ const key = `${entry.title}::${entry.type}::${entry.file}`;
738
+
739
+ if (seenEntries.has(key)) {
740
+ continue;
741
+ }
742
+
743
+ seenEntries.add(key);
744
+ result.push(entry);
260
745
  }
261
746
 
262
- return nextState;
747
+ return result;
748
+ }
749
+
750
+ function deriveKeyFeatures(projectRoot) {
751
+ const features = [];
752
+
753
+ if (fs.existsSync(path.join(projectRoot, 'bin', 'cli.js'))) {
754
+ features.push('CLI commands for initializing, linking, starting, and updating AI context');
755
+ }
756
+
757
+ if (fs.existsSync(path.join(projectRoot, 'core', 'watcher.js'))) {
758
+ features.push('Noise-filtered watcher that turns file changes into meaningful project updates');
759
+ }
760
+
761
+ if (
762
+ fs.existsSync(path.join(projectRoot, 'server', 'server.js')) &&
763
+ fs.existsSync(path.join(projectRoot, 'server', 'routes.js'))
764
+ ) {
765
+ features.push('Local Express server for AI-readable context endpoints');
766
+ }
767
+
768
+ if (fs.existsSync(path.join(projectRoot, 'core', 'gitSync.js'))) {
769
+ features.push('Optional GitHub sync for publishing public AI-readable context');
770
+ }
771
+
772
+ if (fs.existsSync(path.join(projectRoot, 'core', 'stateManager.js'))) {
773
+ features.push('Intelligent state engine that summarizes project evolution for AI tools');
774
+ }
775
+
776
+ return features;
777
+ }
778
+
779
+ function deriveKnownIssues(projectRoot, techStack) {
780
+ const knownIssues = [];
781
+
782
+ if (!hasTestIndicators(projectRoot)) {
783
+ knownIssues.push('No automated test suite is detected yet.');
784
+ }
785
+
786
+ if (!techStack.framework) {
787
+ knownIssues.push('No common application framework dependency is currently detected.');
788
+ }
789
+
790
+ return knownIssues;
791
+ }
792
+
793
+ function hasTestIndicators(projectRoot) {
794
+ const testPaths = [
795
+ 'test',
796
+ 'tests',
797
+ '__tests__',
798
+ 'vitest.config.js',
799
+ 'jest.config.js',
800
+ 'jest.config.cjs',
801
+ 'jest.config.mjs'
802
+ ];
803
+
804
+ return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
805
+ }
806
+
807
+ function determineCurrentStage(keyFeatures) {
808
+ const hasCli = keyFeatures.some((feature) => feature.includes('CLI commands'));
809
+ const hasSync = keyFeatures.some((feature) => feature.includes('GitHub sync'));
810
+ const hasServer = keyFeatures.some((feature) => feature.includes('Express server'));
811
+ const hasWatcher = keyFeatures.some((feature) => feature.includes('watcher'));
812
+ const hasIntelligence = keyFeatures.some((feature) => feature.includes('Intelligent state engine'));
813
+
814
+ if (hasCli && hasSync && hasServer && hasWatcher && hasIntelligence) {
815
+ return 'Production-ready';
816
+ }
817
+
818
+ if (hasCli && hasSync) {
819
+ return 'Functional prototype';
820
+ }
821
+
822
+ return 'Early development';
823
+ }
824
+
825
+ function generateAiSummary(state) {
826
+ const language = state.tech_stack.language || 'Project';
827
+ const hasServer = state.key_features.some((feature) => feature.includes('Express server'));
828
+ const hasSync = state.key_features.some((feature) => feature.includes('GitHub sync'));
829
+ const clauses = ['tracks meaningful project evolution', 'generates structured AI-readable context'];
830
+
831
+ if (hasServer) {
832
+ clauses.push('serves project context locally through Express');
833
+ }
834
+
835
+ if (hasSync) {
836
+ clauses.push('can publish public AI-readable context through optional GitHub sync');
837
+ }
838
+
839
+ return `AI-powered ${language} CLI that ${clauses.join(', ')}.`;
840
+ }
841
+
842
+ function generateNextSteps(state) {
843
+ const nextSteps = [];
844
+
845
+ if (state.key_features.length === 0) {
846
+ nextSteps.push('Define core features for the AI context workflow.');
847
+ }
848
+
849
+ if (state.known_issues.includes('No automated test suite is detected yet.')) {
850
+ nextSteps.push('Add automated tests for the state engine, watcher, and GitHub sync flows.');
851
+ }
852
+
853
+ if (state.current_stage === 'Early development') {
854
+ nextSteps.push('Implement the next core workflow milestone and document how AI should use it.');
855
+ }
856
+
857
+ if (state.current_stage === 'Functional prototype') {
858
+ nextSteps.push('Harden the current feature set with tests and release validation.');
859
+ }
860
+
861
+ if (!state.tech_stack.framework) {
862
+ nextSteps.push('Document the intended framework or extend stack detection for this project.');
863
+ }
864
+
865
+ if (state.recent_updates.length === 0) {
866
+ nextSteps.push('Capture the first meaningful project milestone to seed AI context history.');
867
+ }
868
+
869
+ return Array.from(new Set(nextSteps)).slice(0, 4);
263
870
  }
264
871
 
265
872
  function createDebouncedStateUpdater(projectRoot, options) {
@@ -331,9 +938,12 @@ module.exports = {
331
938
  detectProjectMetadata,
332
939
  ensureContextDirectory,
333
940
  getContextPaths,
941
+ interpretChange,
334
942
  loadRuntimeConfig,
335
943
  readJsonFile,
336
944
  renderTemplate,
945
+ scoreEvent,
946
+ shouldIgnoreProjectFile,
337
947
  updateRuntimeConfig,
338
948
  updateProjectState,
339
949
  writeJsonAtomic,
package/core/watcher.js CHANGED
@@ -7,6 +7,8 @@ const { syncContextToGit } = require('./gitSync');
7
7
  const {
8
8
  createDebouncedStateUpdater,
9
9
  loadRuntimeConfig,
10
+ scoreEvent,
11
+ shouldIgnoreProjectFile,
10
12
  updateProjectState
11
13
  } = require('./stateManager');
12
14
 
@@ -18,8 +20,7 @@ async function startWatcher(projectRoot, options) {
18
20
  const settings = Object.assign({ logger: null }, options);
19
21
  const logger = settings.logger;
20
22
  const config = await loadRuntimeConfig(projectRoot);
21
- const syncCallback = async () =>
22
- syncContextToGit(projectRoot, config.gitSync, logger);
23
+ const syncCallback = async () => syncContextToGit(projectRoot, config.gitSync, logger);
23
24
  const debouncedUpdater = createDebouncedStateUpdater(projectRoot, {
24
25
  debounceMs: config.debounceMs,
25
26
  logger,
@@ -27,11 +28,10 @@ async function startWatcher(projectRoot, options) {
27
28
  });
28
29
 
29
30
  const watcher = chokidar.watch(projectRoot, {
30
- ignored: [
31
- /(^|[\\/])\.git([\\/]|$)/,
32
- /(^|[\\/])node_modules([\\/]|$)/,
33
- /(^|[\\/])\.ai-context([\\/]|$)/
34
- ],
31
+ ignored(filePath) {
32
+ const normalizedPath = normalizeFilePath(projectRoot, filePath);
33
+ return shouldIgnoreProjectFile(normalizedPath);
34
+ },
35
35
  ignoreInitial: true,
36
36
  persistent: true
37
37
  });
@@ -39,12 +39,12 @@ async function startWatcher(projectRoot, options) {
39
39
  function handleEvent(action, filePath) {
40
40
  const normalizedPath = normalizeFilePath(projectRoot, filePath);
41
41
 
42
- if (!normalizedPath || normalizedPath.startsWith('.ai-context/')) {
42
+ if (!normalizedPath || shouldIgnoreProjectFile(normalizedPath) || scoreEvent(normalizedPath) < 2) {
43
43
  return;
44
44
  }
45
45
 
46
46
  if (logger) {
47
- logger.info(`${action} ${normalizedPath}`);
47
+ logger.debug(`Queued meaningful ${action} for ${normalizedPath}`);
48
48
  }
49
49
 
50
50
  debouncedUpdater.enqueue({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aibridge-context",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Zero-config CLI and library for generating AI-readable project context, serving it locally, and syncing it with git.",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -2,11 +2,16 @@
2
2
  "project": "",
3
3
  "version": "",
4
4
  "last_updated": "1970-01-01T00:00:00.000Z",
5
- "stats": {
6
- "files_changed": 0,
7
- "last_file": ""
5
+ "ai_summary": "",
6
+ "tech_stack": {
7
+ "language": "",
8
+ "framework": "",
9
+ "runtime": "",
10
+ "package_manager": ""
8
11
  },
12
+ "current_stage": "",
9
13
  "recent_updates": [],
10
- "features": [],
14
+ "key_features": [],
15
+ "known_issues": [],
11
16
  "next_steps": []
12
17
  }