diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-04 06:01:04 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-04 06:01:04 +0200 |
| commit | a5023e61be61f2f2287342039addab6bb28ad2fd (patch) | |
| tree | 9b1b3c166869cd6fee91bffe1f4134513005b8b1 /modules/core/module-neural-vfs.js | |
| parent | 301f75ab03302a05b3c35950a3b0ab49f9f69ed6 (diff) | |
auto: 2026-06-04_06:00 changes
Diffstat (limited to 'modules/core/module-neural-vfs.js')
| -rw-r--r-- | modules/core/module-neural-vfs.js | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js new file mode 100644 index 0000000..e9ffafb --- /dev/null +++ b/modules/core/module-neural-vfs.js @@ -0,0 +1,271 @@ +class NeuralVFSModule { + constructor() { + this.commands = {}; + this.terminal = null; + this.index = {}; + this.indexPath = '/.semantic_index/map.json'; + } + + getHelpInfo() { + return { + 'nvfs-index': 'nvfs-index — Scan VFS and build semantic concept index', + 'nvfs-status': 'nvfs-status — Show semantic index stats and last scan time', + 'nvfs-resolve': 'nvfs-resolve <concept> — Find files related to a concept', + 'nvfs-tree': 'nvfs-tree [concept] — Show concept-to-file mapping as a tree' + }; + } + + registerCommands() { + this.commands['nvfs-index'] = (args) => this.buildIndex(); + this.commands['nvfs-status'] = (args) => this.showStatus(); + this.commands['nvfs-resolve'] = (args) => this.resolve(args); + this.commands['nvfs-tree'] = (args) => this.tree(args); + } + + // ── Core Index Builder ── + + buildIndex() { + const fs = this.terminal.fs; + if (!fs) return this.terminal.print('VFS not available', 'error'); + + this.terminal.print('🔍 Scanning VFS for semantic concepts...', 'info'); + + const start = Date.now(); + const newIndex = {}; + let fileCount = 0; + let conceptCount = 0; + + // Walk VFS recursively — skip system dirs + this._walk('/', fs, newIndex, (path, node) => { + if (node.type !== 'file') return; + if (this._isSystemFile(path)) return; + + fileCount++; + const concepts = this._extractConcepts(path, node.content || ''); + for (const concept of concepts) { + if (!newIndex[concept]) { + newIndex[concept] = []; + conceptCount++; + } + // Avoid dupes + if (!newIndex[concept].find(e => e.path === path)) { + newIndex[concept].push({ path, summary: this._summarize(node.content) }); + } + } + }); + + // Also index skills + this._indexSkills(fs, newIndex); + + this.index = newIndex; + this.lastScan = new Date().toISOString(); + this.fileCount = fileCount; + this.conceptCount = conceptCount; + + // Save to VFS + this._saveIndex(fs); + // Clear VFS concept cache so concept:// URIs reflect the new index + if (fs._clearConceptCache) fs._clearConceptCache(); + + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + this.terminal.print(`✅ Indexed ${fileCount} files → ${conceptCount} concepts in ${elapsed}s`, 'success'); + this.terminal.print(` Index saved to ${this.indexPath}`, 'info'); + } + + // ── Resolve ── + + resolve(args) { + if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning'); + + const concept = args.join(' ').toLowerCase(); + const matches = this.index[concept] || []; + + // Fuzzy fallback — find partial matches + const fuzzy = Object.keys(this.index).filter(k => + k.includes(concept) || concept.includes(k) + ).filter(k => k !== concept); + + if (matches.length === 0 && fuzzy.length === 0) { + return this.terminal.print(`No files found for concept "${concept}"`, 'warning'); + } + + if (matches.length > 0) { + this.terminal.print(`📂 "${concept}" → ${matches.length} file(s):`, 'info'); + for (const m of matches.slice(0, 20)) { + this.terminal.print(` ${m.path}`, 'success'); + if (m.summary) this.terminal.print(` ${m.summary}`, '') + } + if (matches.length > 20) { + this.terminal.print(` ... and ${matches.length - 20} more`, 'info'); + } + } + + if (fuzzy.length > 0) { + this.terminal.print(`\n🔀 Similar concepts: ${fuzzy.slice(0, 10).join(', ')}`, 'info'); + } + } + + // ── Status ── + + showStatus() { + const count = Object.keys(this.index).length; + if (count === 0) { + // Try loading from VFS + const fs = this.terminal.fs; + const saved = fs.fsReadJSON(this.indexPath); + if (saved) { + this.index = saved; + this.lastScan = '(loaded from disk)'; + this.terminal.print(`📖 Loaded index from ${this.indexPath}`, 'info'); + } else { + return this.terminal.print('No semantic index found. Run nvfs-index first.', 'warning'); + } + } + + const totalFiles = new Set(); + for (const entries of Object.values(this.index)) { + for (const e of entries) totalFiles.add(e.path); + } + + this.terminal.print(`🧠 Neural VFS Index`, 'info'); + this.terminal.print(` Concepts: ${Object.keys(this.index).length}`, ''); + this.terminal.print(` Files indexed: ${totalFiles.size}`, ''); + this.terminal.print(` Last scan: ${this.lastScan || 'never'}`, ''); + } + + // ── Tree View ── + + tree(args) { + const concepts = Object.keys(this.index); + if (concepts.length === 0) { + return this.terminal.print('No index. Run nvfs-index first.', 'warning'); + } + + const filter = args[0] ? args[0].toLowerCase() : null; + const filtered = filter ? concepts.filter(c => c.includes(filter)) : concepts; + + this.terminal.print(`🌳 Concept Tree${filter ? ` (filter: "${filter}")` : ''}:`, 'info'); + for (const concept of filtered.slice(0, 15)) { + this.terminal.print(` 📁 ${concept}`, 'success'); + for (const entry of this.index[concept].slice(0, 5)) { + this.terminal.print(` └─ ${entry.path}`, ''); + } + if (this.index[concept].length > 5) { + this.terminal.print(` └─ ... (+${this.index[concept].length - 5} more)`, ''); + } + } + if (filtered.length > 15) { + this.terminal.print(` ... and ${filtered.length - 15} more concepts`, 'info'); + } + } + + // ── Private Helpers ── + + _walk(path, fs, index, cb) { + const entries = fs.readDir(path); + if (!entries) return; + for (const entry of entries) { + const fullPath = path === '/' ? '/' + entry.name : path + '/' + entry.name; + if (entry.type === 'file') { + cb(fullPath, { type: 'file', content: fs.readFile(fullPath) || '' }); + } else if (entry.type === 'dir') { + this._walk(fullPath, fs, index, cb); + } + } + } + + _isSystemFile(path) { + const sysPrefixes = ['/.trash', '/.semantic_index', '/_snapshots', '/_sessions']; + return sysPrefixes.some(p => path.startsWith(p) || path.includes('/node_modules')); + } + + _extractConcepts(path, content) { + const concepts = new Set(); + const name = path.split('/').pop() || ''; + const nameLower = name.toLowerCase(); + + // Extract from filename (split on camelCase, kebab, dots, underscores) + const words = nameLower + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[-_.]/g, ' ') + .replace(/\.(md|js|html|json|css|txt|yaml|yml|toml|env|sh)$/i, '') + .split(/\s+/) + .filter(w => w.length > 2 && !/^\d+$/.test(w)); + + for (const word of words) { + concepts.add(word); + } + + // Extract file extension as concept + const ext = path.split('.').pop(); + if (ext && ['md', 'js', 'html', 'json', 'css'].includes(ext)) { + concepts.add(ext + '-file'); + } + + // Extract path-level concepts + const parts = path.split('/').filter(p => p.length > 0); + for (const part of parts) { + if (['modules', 'skills', 'core', 'ext', 'backups', 'chat', 'downloads'].includes(part)) { + concepts.add(part); + } + } + + // Extract #tags from content (markdown frontmatter or inline) + if (content) { + const tagMatches = content.match(/tags:\s*\[([^\]]+)\]/i) || content.match(/#[a-zA-Z]\w+/g); + if (tagMatches) { + const tags = tagMatches[0].replace(/tags:\s*\[/i, '').replace(/\]/, '').split(',').map(t => t.trim().replace(/^#/, '').replace(/"/g, '')); + for (const tag of tags) { + if (tag.length > 2) concepts.add(tag.toLowerCase()); + } + } + + // Extract first H1/H2 as concept + const headerMatch = content.match(/^#\s+(.+)/m) || content.match(/^##\s+(.+)/m); + if (headerMatch) { + const header = headerMatch[1].toLowerCase().replace(/[^a-z0-9\s]/g, '').trim(); + if (header.length > 3 && header.length < 40) { + concepts.add(header); + } + } + } + + return [...concepts]; + } + + _summarize(content) { + if (!content || content.length < 10) return ''; + const cleaned = content.replace(/---[\s\S]*?---/, '').trim(); + const firstLine = cleaned.split('\n').find(l => l.trim().length > 20); + if (!firstLine) return ''; + return firstLine.trim().slice(0, 100) + (firstLine.length > 100 ? '...' : ''); + } + + _indexSkills(fs, index) { + const skills = fs.readDir('/skills'); + if (!skills) return; + for (const skill of skills) { + if (skill.type !== 'file' || !skill.name.endsWith('.md')) continue; + // Already indexed by _walk — but we add a "skill" tag + const base = skill.name.replace('.md', ''); + if (!index['skill']) index['skill'] = []; + index['skill'].push({ path: '/skills/' + skill.name, summary: 'Skill: ' + base }); + } + } + + _saveIndex(fs) { + try { + if (!fs.readDir('/.semantic_index')) { + fs.mkdirRecursive('/.semantic_index'); + } + fs.fsWriteJSON(this.indexPath, this.index); + } catch (e) { + console.error('NeuralVFS save error:', e); + } + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.NeuralVFSModule = NeuralVFSModule; +}
\ No newline at end of file |