namnam-skills 1.0.1 → 1.0.2

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.
@@ -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
+ }