aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-neural-vfs.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-04 06:01:04 +0200
committerAgando <roduccomm@gmail.com>2026-06-04 06:01:04 +0200
commita5023e61be61f2f2287342039addab6bb28ad2fd (patch)
tree9b1b3c166869cd6fee91bffe1f4134513005b8b1 /modules/core/module-neural-vfs.js
parent301f75ab03302a05b3c35950a3b0ab49f9f69ed6 (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.js271
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