namnam-skills 1.0.1 → 1.0.3
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.
- package/package.json +2 -2
- package/src/cli.js +487 -8
- package/src/conversation.js +467 -0
- package/src/indexer.js +151 -0
- package/src/templates/{core/namnam.md → namnam.md} +118 -5
- package/src/watcher.js +356 -0
package/src/conversation.js
CHANGED
|
@@ -16,6 +16,11 @@ const META_FILE = 'meta.json';
|
|
|
16
16
|
const CONTEXT_FILE = 'context.md';
|
|
17
17
|
const FULL_LOG_FILE = 'full.md';
|
|
18
18
|
|
|
19
|
+
// Auto-memory files
|
|
20
|
+
const AUTO_MEMORY_DIR = 'auto-memories';
|
|
21
|
+
const AUTO_MEMORY_INDEX = 'auto-index.json';
|
|
22
|
+
const AUTO_MEMORY_CURRENT = 'current-session.json';
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Validate conversation options
|
|
21
26
|
*/
|
|
@@ -445,3 +450,465 @@ export async function resolveConversationRefs(text, cwd = process.cwd()) {
|
|
|
445
450
|
combined: contexts.join('\n\n')
|
|
446
451
|
};
|
|
447
452
|
}
|
|
453
|
+
|
|
454
|
+
// ========================================
|
|
455
|
+
// AUTO-MEMORY SYSTEM
|
|
456
|
+
// ========================================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get auto-memory directory path
|
|
460
|
+
*/
|
|
461
|
+
export function getAutoMemoryDir(cwd = process.cwd()) {
|
|
462
|
+
return path.join(cwd, '.claude', AUTO_MEMORY_DIR);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get auto-memory index path
|
|
467
|
+
*/
|
|
468
|
+
export function getAutoMemoryIndexPath(cwd = process.cwd()) {
|
|
469
|
+
return path.join(getAutoMemoryDir(cwd), AUTO_MEMORY_INDEX);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get current session path
|
|
474
|
+
*/
|
|
475
|
+
export function getCurrentSessionPath(cwd = process.cwd()) {
|
|
476
|
+
return path.join(getAutoMemoryDir(cwd), AUTO_MEMORY_CURRENT);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Initialize auto-memory system
|
|
481
|
+
*/
|
|
482
|
+
export async function initAutoMemory(cwd = process.cwd()) {
|
|
483
|
+
const memoryDir = getAutoMemoryDir(cwd);
|
|
484
|
+
const indexPath = getAutoMemoryIndexPath(cwd);
|
|
485
|
+
|
|
486
|
+
await fs.ensureDir(memoryDir);
|
|
487
|
+
|
|
488
|
+
if (!(await fs.pathExists(indexPath))) {
|
|
489
|
+
await fs.writeJson(indexPath, {
|
|
490
|
+
version: '1.0.0',
|
|
491
|
+
memories: [],
|
|
492
|
+
patterns: [],
|
|
493
|
+
decisions: [],
|
|
494
|
+
lastUpdated: new Date().toISOString()
|
|
495
|
+
}, { spaces: 2 });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return memoryDir;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Load auto-memory index
|
|
503
|
+
*/
|
|
504
|
+
export async function loadAutoMemoryIndex(cwd = process.cwd()) {
|
|
505
|
+
const indexPath = getAutoMemoryIndexPath(cwd);
|
|
506
|
+
|
|
507
|
+
if (!(await fs.pathExists(indexPath))) {
|
|
508
|
+
return { version: '1.0.0', memories: [], patterns: [], decisions: [], lastUpdated: null };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return await fs.readJson(indexPath);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Save auto-memory index
|
|
516
|
+
*/
|
|
517
|
+
export async function saveAutoMemoryIndex(index, cwd = process.cwd()) {
|
|
518
|
+
const indexPath = getAutoMemoryIndexPath(cwd);
|
|
519
|
+
index.lastUpdated = new Date().toISOString();
|
|
520
|
+
await fs.writeJson(indexPath, index, { spaces: 2 });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Validation constants for auto-memory
|
|
524
|
+
const VALID_MEMORY_TYPES = ['decision', 'pattern', 'context', 'learning'];
|
|
525
|
+
const VALID_IMPORTANCE_LEVELS = ['low', 'normal', 'high', 'critical'];
|
|
526
|
+
const VALID_SOURCES = ['auto', 'user', 'agent', 'session'];
|
|
527
|
+
const MAX_CONTENT_SIZE = 10000; // 10KB max per memory
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Validate auto-memory input
|
|
531
|
+
*/
|
|
532
|
+
function validateMemoryInput(memory) {
|
|
533
|
+
const errors = [];
|
|
534
|
+
|
|
535
|
+
// Content is required
|
|
536
|
+
if (!memory.content || typeof memory.content !== 'string') {
|
|
537
|
+
errors.push('content is required and must be a string');
|
|
538
|
+
} else if (memory.content.length > MAX_CONTENT_SIZE) {
|
|
539
|
+
errors.push(`content exceeds maximum size of ${MAX_CONTENT_SIZE} characters`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Validate type
|
|
543
|
+
if (memory.type && !VALID_MEMORY_TYPES.includes(memory.type)) {
|
|
544
|
+
errors.push(`type must be one of: ${VALID_MEMORY_TYPES.join(', ')}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Validate importance
|
|
548
|
+
if (memory.importance && !VALID_IMPORTANCE_LEVELS.includes(memory.importance)) {
|
|
549
|
+
errors.push(`importance must be one of: ${VALID_IMPORTANCE_LEVELS.join(', ')}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Validate source
|
|
553
|
+
if (memory.source && !VALID_SOURCES.includes(memory.source)) {
|
|
554
|
+
errors.push(`source must be one of: ${VALID_SOURCES.join(', ')}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Validate tags
|
|
558
|
+
if (memory.tags && !Array.isArray(memory.tags)) {
|
|
559
|
+
errors.push('tags must be an array');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Validate relatedFiles
|
|
563
|
+
if (memory.relatedFiles && !Array.isArray(memory.relatedFiles)) {
|
|
564
|
+
errors.push('relatedFiles must be an array');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (errors.length > 0) {
|
|
568
|
+
throw new Error(`Invalid memory input: ${errors.join('; ')}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Auto-save a memory (decision, pattern, or important context)
|
|
574
|
+
* This is called automatically during AI interactions
|
|
575
|
+
*/
|
|
576
|
+
export async function autoSaveMemory(memory, cwd = process.cwd()) {
|
|
577
|
+
// Validate input
|
|
578
|
+
validateMemoryInput(memory);
|
|
579
|
+
|
|
580
|
+
await initAutoMemory(cwd);
|
|
581
|
+
|
|
582
|
+
const index = await loadAutoMemoryIndex(cwd);
|
|
583
|
+
const id = `mem_${Date.now().toString(36)}_${crypto.randomBytes(2).toString('hex')}`;
|
|
584
|
+
|
|
585
|
+
const memoryEntry = {
|
|
586
|
+
id,
|
|
587
|
+
type: memory.type || 'context', // 'decision', 'pattern', 'context', 'learning'
|
|
588
|
+
content: memory.content,
|
|
589
|
+
summary: memory.summary || null,
|
|
590
|
+
source: memory.source || 'auto', // 'auto', 'user', 'agent'
|
|
591
|
+
tags: memory.tags || [],
|
|
592
|
+
importance: memory.importance || 'normal', // 'low', 'normal', 'high', 'critical'
|
|
593
|
+
relatedFiles: memory.relatedFiles || [],
|
|
594
|
+
createdAt: new Date().toISOString(),
|
|
595
|
+
expiresAt: memory.expiresAt || null // null = never expires
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Add to appropriate array based on type
|
|
599
|
+
if (memory.type === 'decision') {
|
|
600
|
+
index.decisions.push(memoryEntry);
|
|
601
|
+
} else if (memory.type === 'pattern') {
|
|
602
|
+
index.patterns.push(memoryEntry);
|
|
603
|
+
} else {
|
|
604
|
+
index.memories.push(memoryEntry);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Keep only last 100 memories per type (cleanup old ones)
|
|
608
|
+
if (index.memories.length > 100) {
|
|
609
|
+
index.memories = index.memories.slice(-100);
|
|
610
|
+
}
|
|
611
|
+
if (index.decisions.length > 50) {
|
|
612
|
+
index.decisions = index.decisions.slice(-50);
|
|
613
|
+
}
|
|
614
|
+
if (index.patterns.length > 50) {
|
|
615
|
+
index.patterns = index.patterns.slice(-50);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
await saveAutoMemoryIndex(index, cwd);
|
|
619
|
+
|
|
620
|
+
return memoryEntry;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Get relevant memories for a query/context
|
|
625
|
+
* Uses keyword matching and recency
|
|
626
|
+
*/
|
|
627
|
+
export async function getRelevantMemories(options = {}, cwd = process.cwd()) {
|
|
628
|
+
const {
|
|
629
|
+
query = null,
|
|
630
|
+
files = [],
|
|
631
|
+
types = ['decision', 'pattern', 'context', 'learning'],
|
|
632
|
+
limit = 10,
|
|
633
|
+
minImportance = 'low'
|
|
634
|
+
} = options;
|
|
635
|
+
|
|
636
|
+
const index = await loadAutoMemoryIndex(cwd);
|
|
637
|
+
|
|
638
|
+
// Combine all memory types
|
|
639
|
+
let allMemories = [
|
|
640
|
+
...index.memories.map(m => ({ ...m, _type: 'memory' })),
|
|
641
|
+
...index.decisions.map(m => ({ ...m, _type: 'decision' })),
|
|
642
|
+
...index.patterns.map(m => ({ ...m, _type: 'pattern' }))
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
// Filter by type
|
|
646
|
+
allMemories = allMemories.filter(m => types.includes(m.type));
|
|
647
|
+
|
|
648
|
+
// Filter by importance
|
|
649
|
+
const importanceOrder = ['low', 'normal', 'high', 'critical'];
|
|
650
|
+
const minIdx = importanceOrder.indexOf(minImportance);
|
|
651
|
+
allMemories = allMemories.filter(m => {
|
|
652
|
+
const memIdx = importanceOrder.indexOf(m.importance);
|
|
653
|
+
return memIdx >= minIdx;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Filter expired
|
|
657
|
+
const now = new Date();
|
|
658
|
+
allMemories = allMemories.filter(m => {
|
|
659
|
+
if (!m.expiresAt) return true;
|
|
660
|
+
return new Date(m.expiresAt) > now;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Score by relevance
|
|
664
|
+
allMemories = allMemories.map(m => {
|
|
665
|
+
let score = 0;
|
|
666
|
+
|
|
667
|
+
// Recency score (newer = higher)
|
|
668
|
+
const age = now - new Date(m.createdAt);
|
|
669
|
+
const hoursSinceCreated = age / (1000 * 60 * 60);
|
|
670
|
+
score += Math.max(0, 10 - hoursSinceCreated / 24); // Decays over 10 days
|
|
671
|
+
|
|
672
|
+
// Importance score
|
|
673
|
+
score += importanceOrder.indexOf(m.importance) * 2;
|
|
674
|
+
|
|
675
|
+
// Query match score
|
|
676
|
+
if (query) {
|
|
677
|
+
const queryLower = query.toLowerCase();
|
|
678
|
+
const content = (m.content + ' ' + (m.summary || '')).toLowerCase();
|
|
679
|
+
if (content.includes(queryLower)) {
|
|
680
|
+
score += 5;
|
|
681
|
+
}
|
|
682
|
+
// Check for word matches
|
|
683
|
+
const queryWords = queryLower.split(/\s+/);
|
|
684
|
+
for (const word of queryWords) {
|
|
685
|
+
if (word.length > 2 && content.includes(word)) {
|
|
686
|
+
score += 1;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// File match score
|
|
692
|
+
if (files.length > 0 && m.relatedFiles.length > 0) {
|
|
693
|
+
for (const file of files) {
|
|
694
|
+
if (m.relatedFiles.some(rf => rf.includes(file) || file.includes(rf))) {
|
|
695
|
+
score += 3;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Tag match score
|
|
701
|
+
if (query && m.tags.length > 0) {
|
|
702
|
+
for (const tag of m.tags) {
|
|
703
|
+
if (query.toLowerCase().includes(tag.toLowerCase())) {
|
|
704
|
+
score += 2;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return { ...m, _score: score };
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Sort by score and limit
|
|
713
|
+
allMemories.sort((a, b) => b._score - a._score);
|
|
714
|
+
allMemories = allMemories.slice(0, limit);
|
|
715
|
+
|
|
716
|
+
return allMemories;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Generate auto-memory context for AI consumption
|
|
721
|
+
*/
|
|
722
|
+
export async function generateAutoMemoryContext(options = {}, cwd = process.cwd()) {
|
|
723
|
+
const memories = await getRelevantMemories(options, cwd);
|
|
724
|
+
|
|
725
|
+
if (memories.length === 0) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let context = '<auto-memories>\n';
|
|
730
|
+
|
|
731
|
+
// Group by type
|
|
732
|
+
const decisions = memories.filter(m => m.type === 'decision');
|
|
733
|
+
const patterns = memories.filter(m => m.type === 'pattern');
|
|
734
|
+
const others = memories.filter(m => !['decision', 'pattern'].includes(m.type));
|
|
735
|
+
|
|
736
|
+
if (decisions.length > 0) {
|
|
737
|
+
context += '## Prior Decisions\n';
|
|
738
|
+
for (const d of decisions) {
|
|
739
|
+
context += `- **${d.summary || 'Decision'}**: ${d.content}\n`;
|
|
740
|
+
}
|
|
741
|
+
context += '\n';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (patterns.length > 0) {
|
|
745
|
+
context += '## Learned Patterns\n';
|
|
746
|
+
for (const p of patterns) {
|
|
747
|
+
context += `- ${p.content}\n`;
|
|
748
|
+
}
|
|
749
|
+
context += '\n';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (others.length > 0) {
|
|
753
|
+
context += '## Relevant Context\n';
|
|
754
|
+
for (const m of others) {
|
|
755
|
+
context += `- ${m.content}\n`;
|
|
756
|
+
}
|
|
757
|
+
context += '\n';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
context += '</auto-memories>';
|
|
761
|
+
|
|
762
|
+
return context;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Start a new session (for tracking current work)
|
|
767
|
+
*/
|
|
768
|
+
export async function startSession(options = {}, cwd = process.cwd()) {
|
|
769
|
+
await initAutoMemory(cwd);
|
|
770
|
+
|
|
771
|
+
const session = {
|
|
772
|
+
id: `session_${Date.now().toString(36)}`,
|
|
773
|
+
startedAt: new Date().toISOString(),
|
|
774
|
+
task: options.task || null,
|
|
775
|
+
filesModified: [],
|
|
776
|
+
decisions: [],
|
|
777
|
+
memories: [],
|
|
778
|
+
status: 'active'
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const sessionPath = getCurrentSessionPath(cwd);
|
|
782
|
+
await fs.writeJson(sessionPath, session, { spaces: 2 });
|
|
783
|
+
|
|
784
|
+
return session;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Get current session
|
|
789
|
+
*/
|
|
790
|
+
export async function getCurrentSession(cwd = process.cwd()) {
|
|
791
|
+
const sessionPath = getCurrentSessionPath(cwd);
|
|
792
|
+
|
|
793
|
+
if (!(await fs.pathExists(sessionPath))) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return await fs.readJson(sessionPath);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Update current session
|
|
802
|
+
*/
|
|
803
|
+
export async function updateSession(updates, cwd = process.cwd()) {
|
|
804
|
+
let session = await getCurrentSession(cwd);
|
|
805
|
+
|
|
806
|
+
if (!session) {
|
|
807
|
+
session = await startSession({}, cwd);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Merge updates
|
|
811
|
+
if (updates.task) session.task = updates.task;
|
|
812
|
+
if (updates.fileModified) {
|
|
813
|
+
if (!session.filesModified.includes(updates.fileModified)) {
|
|
814
|
+
session.filesModified.push(updates.fileModified);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (updates.decision) {
|
|
818
|
+
session.decisions.push({
|
|
819
|
+
content: updates.decision,
|
|
820
|
+
timestamp: new Date().toISOString()
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
if (updates.memory) {
|
|
824
|
+
session.memories.push({
|
|
825
|
+
content: updates.memory,
|
|
826
|
+
timestamp: new Date().toISOString()
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
session.lastUpdatedAt = new Date().toISOString();
|
|
831
|
+
|
|
832
|
+
const sessionPath = getCurrentSessionPath(cwd);
|
|
833
|
+
await fs.writeJson(sessionPath, session, { spaces: 2 });
|
|
834
|
+
|
|
835
|
+
return session;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* End session and save memories
|
|
840
|
+
*/
|
|
841
|
+
export async function endSession(options = {}, cwd = process.cwd()) {
|
|
842
|
+
const session = await getCurrentSession(cwd);
|
|
843
|
+
|
|
844
|
+
if (!session) {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Save important decisions as permanent memories
|
|
849
|
+
for (const decision of session.decisions) {
|
|
850
|
+
await autoSaveMemory({
|
|
851
|
+
type: 'decision',
|
|
852
|
+
content: decision.content,
|
|
853
|
+
relatedFiles: session.filesModified,
|
|
854
|
+
importance: 'high',
|
|
855
|
+
source: 'session'
|
|
856
|
+
}, cwd);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Save session memories
|
|
860
|
+
for (const memory of session.memories) {
|
|
861
|
+
await autoSaveMemory({
|
|
862
|
+
type: 'context',
|
|
863
|
+
content: memory.content,
|
|
864
|
+
relatedFiles: session.filesModified,
|
|
865
|
+
importance: 'normal',
|
|
866
|
+
source: 'session'
|
|
867
|
+
}, cwd);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Mark session as complete
|
|
871
|
+
session.status = 'completed';
|
|
872
|
+
session.endedAt = new Date().toISOString();
|
|
873
|
+
session.summary = options.summary || null;
|
|
874
|
+
|
|
875
|
+
// Archive session
|
|
876
|
+
const archivePath = path.join(getAutoMemoryDir(cwd), `sessions/${session.id}.json`);
|
|
877
|
+
await fs.ensureDir(path.dirname(archivePath));
|
|
878
|
+
await fs.writeJson(archivePath, session, { spaces: 2 });
|
|
879
|
+
|
|
880
|
+
// Clear current session
|
|
881
|
+
const sessionPath = getCurrentSessionPath(cwd);
|
|
882
|
+
await fs.remove(sessionPath);
|
|
883
|
+
|
|
884
|
+
return session;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Quick memory save (for inline use)
|
|
889
|
+
* Usage: await remember("User prefers tabs over spaces", { type: 'pattern' })
|
|
890
|
+
*/
|
|
891
|
+
export async function remember(content, options = {}, cwd = process.cwd()) {
|
|
892
|
+
return await autoSaveMemory({
|
|
893
|
+
content,
|
|
894
|
+
type: options.type || 'context',
|
|
895
|
+
summary: options.summary,
|
|
896
|
+
importance: options.importance || 'normal',
|
|
897
|
+
tags: options.tags || [],
|
|
898
|
+
relatedFiles: options.files || [],
|
|
899
|
+
source: options.source || 'user'
|
|
900
|
+
}, cwd);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Recall memories (for inline use)
|
|
905
|
+
* Usage: const memories = await recall("authentication")
|
|
906
|
+
*/
|
|
907
|
+
export async function recall(query, options = {}, cwd = process.cwd()) {
|
|
908
|
+
return await getRelevantMemories({
|
|
909
|
+
query,
|
|
910
|
+
limit: options.limit || 5,
|
|
911
|
+
types: options.types,
|
|
912
|
+
minImportance: options.minImportance
|
|
913
|
+
}, cwd);
|
|
914
|
+
}
|
package/src/indexer.js
CHANGED
|
@@ -791,3 +791,154 @@ export async function getIndexStats(cwd = process.cwd()) {
|
|
|
791
791
|
indexAge: Date.now() - new Date(meta.updatedAt).getTime()
|
|
792
792
|
};
|
|
793
793
|
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Incremental index update - only process changed files
|
|
797
|
+
*/
|
|
798
|
+
export async function updateIndexIncremental(changedFiles, cwd = process.cwd(), options = {}) {
|
|
799
|
+
const { onProgress = () => {} } = options;
|
|
800
|
+
|
|
801
|
+
const indexDir = getIndexDir(cwd);
|
|
802
|
+
const summariesDir = path.join(indexDir, SUMMARIES_DIR);
|
|
803
|
+
|
|
804
|
+
// Load existing index data
|
|
805
|
+
const filesPath = path.join(indexDir, FILES_FILE);
|
|
806
|
+
const symbolsPath = path.join(indexDir, SYMBOLS_FILE);
|
|
807
|
+
const importsPath = path.join(indexDir, IMPORTS_FILE);
|
|
808
|
+
|
|
809
|
+
if (!(await fs.pathExists(filesPath))) {
|
|
810
|
+
// No existing index, do full build
|
|
811
|
+
return await buildIndex(cwd, options);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let filesIndex = await fs.readJson(filesPath);
|
|
815
|
+
let allSymbols = await fs.readJson(symbolsPath);
|
|
816
|
+
let allImports = await fs.readJson(importsPath);
|
|
817
|
+
|
|
818
|
+
onProgress({ phase: 'incremental', message: `Processing ${changedFiles.length} changed files...` });
|
|
819
|
+
|
|
820
|
+
let processed = 0;
|
|
821
|
+
|
|
822
|
+
for (const change of changedFiles) {
|
|
823
|
+
const { path: filePath, fullPath, type } = change;
|
|
824
|
+
|
|
825
|
+
if (type === 'delete' || type === 'rename') {
|
|
826
|
+
// Remove from index
|
|
827
|
+
filesIndex = filesIndex.filter(f => f.path !== filePath);
|
|
828
|
+
delete allSymbols[filePath];
|
|
829
|
+
delete allImports[filePath];
|
|
830
|
+
|
|
831
|
+
// Remove summary
|
|
832
|
+
const summaryPath = path.join(summariesDir, filePath.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
|
|
833
|
+
if (await fs.pathExists(summaryPath)) {
|
|
834
|
+
await fs.remove(summaryPath);
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
// Add or update file
|
|
838
|
+
try {
|
|
839
|
+
const resolvedPath = fullPath || path.join(cwd, filePath);
|
|
840
|
+
|
|
841
|
+
if (!(await fs.pathExists(resolvedPath))) {
|
|
842
|
+
// File was deleted
|
|
843
|
+
filesIndex = filesIndex.filter(f => f.path !== filePath);
|
|
844
|
+
delete allSymbols[filePath];
|
|
845
|
+
delete allImports[filePath];
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
850
|
+
const stats = await fs.stat(resolvedPath);
|
|
851
|
+
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
852
|
+
|
|
853
|
+
// Extract symbols and imports
|
|
854
|
+
const symbols = extractSymbols(content, filePath);
|
|
855
|
+
const imports = extractImports(content, filePath);
|
|
856
|
+
|
|
857
|
+
// Update file entry
|
|
858
|
+
const existingIdx = filesIndex.findIndex(f => f.path === filePath);
|
|
859
|
+
const fileEntry = {
|
|
860
|
+
path: filePath,
|
|
861
|
+
size: stats.size,
|
|
862
|
+
hash,
|
|
863
|
+
mtime: stats.mtime.toISOString(),
|
|
864
|
+
lines: content.split('\n').length,
|
|
865
|
+
hasSymbols: symbols.functions.length > 0 || symbols.classes.length > 0
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
if (existingIdx >= 0) {
|
|
869
|
+
filesIndex[existingIdx] = fileEntry;
|
|
870
|
+
} else {
|
|
871
|
+
filesIndex.push(fileEntry);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Update symbols and imports
|
|
875
|
+
if (symbols.functions.length > 0 || symbols.classes.length > 0 || symbols.types.length > 0) {
|
|
876
|
+
allSymbols[filePath] = symbols;
|
|
877
|
+
} else {
|
|
878
|
+
delete allSymbols[filePath];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (imports.length > 0) {
|
|
882
|
+
allImports[filePath] = imports;
|
|
883
|
+
} else {
|
|
884
|
+
delete allImports[filePath];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Update summary
|
|
888
|
+
const summary = generateFileSummary(filePath, content, symbols, imports);
|
|
889
|
+
const summaryPath = path.join(summariesDir, filePath.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
|
|
890
|
+
await fs.writeFile(summaryPath, summary);
|
|
891
|
+
|
|
892
|
+
} catch (err) {
|
|
893
|
+
// Skip files that can't be read
|
|
894
|
+
console.error(`Skipping ${filePath}: ${err.message}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
processed++;
|
|
899
|
+
onProgress({
|
|
900
|
+
phase: 'incremental',
|
|
901
|
+
message: `Processed ${processed}/${changedFiles.length}`,
|
|
902
|
+
current: processed,
|
|
903
|
+
total: changedFiles.length
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Re-detect patterns
|
|
908
|
+
onProgress({ phase: 'analyzing', message: 'Analyzing patterns...' });
|
|
909
|
+
const patterns = detectPatterns(filesIndex);
|
|
910
|
+
|
|
911
|
+
// Update meta
|
|
912
|
+
const meta = {
|
|
913
|
+
version: '1.0.0',
|
|
914
|
+
createdAt: (await getIndexMeta(cwd))?.createdAt || new Date().toISOString(),
|
|
915
|
+
updatedAt: new Date().toISOString(),
|
|
916
|
+
stats: {
|
|
917
|
+
totalFiles: filesIndex.length,
|
|
918
|
+
totalLines: filesIndex.reduce((sum, f) => sum + f.lines, 0),
|
|
919
|
+
filesWithSymbols: Object.keys(allSymbols).length,
|
|
920
|
+
totalFunctions: Object.values(allSymbols).reduce((sum, s) => sum + s.functions.length, 0),
|
|
921
|
+
totalClasses: Object.values(allSymbols).reduce((sum, s) => sum + s.classes.length, 0)
|
|
922
|
+
},
|
|
923
|
+
patterns,
|
|
924
|
+
lastIncrementalUpdate: {
|
|
925
|
+
timestamp: new Date().toISOString(),
|
|
926
|
+
filesProcessed: changedFiles.length
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// Save updated index
|
|
931
|
+
onProgress({ phase: 'saving', message: 'Saving index...' });
|
|
932
|
+
await fs.writeJson(path.join(indexDir, META_FILE), meta, { spaces: 2 });
|
|
933
|
+
await fs.writeJson(filesPath, filesIndex, { spaces: 2 });
|
|
934
|
+
await fs.writeJson(symbolsPath, allSymbols, { spaces: 2 });
|
|
935
|
+
await fs.writeJson(importsPath, allImports, { spaces: 2 });
|
|
936
|
+
await fs.writeJson(path.join(indexDir, PATTERNS_FILE), patterns, { spaces: 2 });
|
|
937
|
+
|
|
938
|
+
onProgress({
|
|
939
|
+
phase: 'complete',
|
|
940
|
+
message: `Incremental update complete: ${changedFiles.length} files processed`
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
return meta;
|
|
944
|
+
}
|