entroly-wasm 0.9.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.
package/js/repo_map.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Repo File Map — pure-JS port of entroly/repo_map.py
3
+ * Builds grouped inventory with ownership roles.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const SKIP_DIRS = new Set(['.git', '.venv', '__pycache__', '.pytest_cache', '.ruff_cache', 'target', 'node_modules', '.tmp']);
12
+
13
+ const PY_ROLES = {
14
+ 'server.py': ['primary Python MCP server and product shell', 'python-runtime'],
15
+ 'cli.py': ['primary CLI and operator surface', 'python-runtime'],
16
+ 'proxy.py': ['HTTP proxy prompt compiler runtime', 'python-runtime'],
17
+ 'dashboard.py': ['developer-facing runtime dashboard', 'python-runtime'],
18
+ 'vault.py': ['vault persistence and artifact schema', 'python-cogops'],
19
+ 'epistemic_router.py': ['ingress routing policy engine', 'python-cogops'],
20
+ 'belief_compiler.py': ['Truth to Belief compiler', 'python-cogops'],
21
+ 'verification_engine.py': ['belief verification and confidence engine', 'python-cogops'],
22
+ 'change_pipeline.py': ['change-driven PR and review pipeline', 'python-cogops'],
23
+ 'flow_orchestrator.py': ['canonical flow executor', 'python-cogops'],
24
+ 'skill_engine.py': ['dynamic skill synthesis and lifecycle', 'python-cogops'],
25
+ 'auto_index.py': ['workspace discovery and raw ingest indexing', 'python-support'],
26
+ 'autotune.py': ['parameter tuning and feedback journal', 'python-support'],
27
+ 'multimodal.py': ['image, diff, diagram, and voice ingestion', 'python-support'],
28
+ };
29
+
30
+ const RUST_ROLES = {
31
+ 'lib.rs': ['core Rust engine and PyO3 export surface', 'rust-runtime'],
32
+ 'cogops.rs': ['Rust epistemic engine and CogOps data plane', 'rust-cogops'],
33
+ 'knapsack.rs': ['budgeted context selection optimizer', 'rust-core'],
34
+ 'entropy.rs': ['information density scoring', 'rust-core'],
35
+ 'dedup.rs': ['SimHash deduplication', 'rust-core'],
36
+ 'prism.rs': ['reinforcement and spectral optimizer', 'rust-learning'],
37
+ 'sast.rs': ['static security analysis engine', 'rust-verification'],
38
+ 'health.rs': ['codebase health analysis', 'rust-verification'],
39
+ 'cache.rs': ['EGSC cache and retrieval economics', 'rust-core'],
40
+ };
41
+
42
+ const WASM_ROLES = {
43
+ 'server.js': ['Node MCP server over WASM engine', 'wasm-runtime'],
44
+ 'cli.js': ['Node CLI over WASM engine', 'wasm-runtime'],
45
+ 'vault.js': ['pure-JS vault persistence', 'wasm-cogops'],
46
+ 'cogops.js': ['pure-JS epistemic engine', 'wasm-cogops'],
47
+ 'skills.js': ['pure-JS skill lifecycle', 'wasm-cogops'],
48
+ 'workspace.js': ['Node workspace change listener', 'wasm-cogops'],
49
+ 'config.js': ['Node configuration wrapper', 'wasm-support'],
50
+ 'auto_index.js': ['Node workspace indexing wrapper', 'wasm-support'],
51
+ 'checkpoint.js': ['Node checkpoint wrapper', 'wasm-support'],
52
+ 'autotune.js': ['Node autotune wrapper', 'wasm-support'],
53
+ };
54
+
55
+ function buildRepoMap(rootDir) {
56
+ const root = path.resolve(rootDir);
57
+ const grouped = { root: [], python: [], 'rust-core': [], wasm: [], tests: [], other: [] };
58
+ const walk = (dir) => {
59
+ let entries;
60
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
61
+ for (const e of entries) {
62
+ if (SKIP_DIRS.has(e.name)) continue;
63
+ const full = path.join(dir, e.name);
64
+ if (e.isDirectory()) { walk(full); continue; }
65
+ const rel = path.relative(root, full).replace(/\\/g, '/');
66
+ const parts = rel.split('/');
67
+ const name = path.basename(rel);
68
+ let repo = 'other', role = 'support file', category = 'support';
69
+ if (parts.length === 1) {
70
+ repo = 'root';
71
+ role = 'top-level support artifact';
72
+ category = 'root-support';
73
+ } else if (parts[0] === 'entroly' && !parts[1]?.startsWith('entroly')) {
74
+ repo = 'python';
75
+ const r = PY_ROLES[name];
76
+ if (r) { role = r[0]; category = r[1]; }
77
+ else { role = 'Python package support file'; category = 'python-support'; }
78
+ } else if (parts[0] === 'entroly-core') {
79
+ repo = 'rust-core';
80
+ const r = RUST_ROLES[name];
81
+ if (r) { role = r[0]; category = r[1]; }
82
+ else { role = parts[1] === 'src' ? 'Rust core module' : 'Rust core metadata'; category = 'rust-core'; }
83
+ } else if (parts[0] === 'entroly-wasm') {
84
+ repo = 'wasm';
85
+ const r = WASM_ROLES[name];
86
+ if (r) { role = r[0]; category = r[1]; }
87
+ else { role = 'WASM package support file'; category = 'wasm-support'; }
88
+ } else if (parts[0] === 'tests') {
89
+ repo = 'tests'; role = 'integration test'; category = 'python-test';
90
+ } else if (['docs', 'examples', 'bench'].includes(parts[0])) {
91
+ repo = 'other'; role = 'documentation, example, or benchmark asset'; category = 'support';
92
+ }
93
+ grouped[repo].push({ repo, path: rel, role, category });
94
+ }
95
+ };
96
+ walk(root);
97
+ return grouped;
98
+ }
99
+
100
+ function renderRepoMapMarkdown(grouped) {
101
+ const lines = ['# Entroly Repo File Map', '', 'Canonical ownership map across the Python product shell, Rust core, and WASM/JS surface.', ''];
102
+ for (const section of ['root', 'python', 'rust-core', 'wasm', 'tests', 'other']) {
103
+ const entries = grouped[section] || [];
104
+ if (!entries.length) continue;
105
+ lines.push(`## ${section}`, '', '| Path | Role | Category |', '|---|---|---|');
106
+ for (const e of entries) lines.push(`| \`${e.path}\` | ${e.role} | \`${e.category}\` |`);
107
+ lines.push('');
108
+ }
109
+ return lines.join('\n') + '\n';
110
+ }
111
+
112
+ module.exports = { buildRepoMap, renderRepoMapMarkdown };
package/js/server.js ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ // Entroly MCP Server — JS port of server.py
3
+ // Pure-wasm MCP server — no Python dependency.
4
+ //
5
+ // Architecture: MCP Client → JSON-RPC (stdio) → Node.js → Wasm Engine → Results
6
+
7
+ const { WasmEntrolyEngine } = require('../pkg/entroly_wasm');
8
+ const { EntrolyConfig } = require('./config');
9
+ const { CheckpointManager, persistIndex, loadIndex } = require('./checkpoint');
10
+ const { autoIndex, startIncrementalWatcher } = require('./auto_index');
11
+ const { startAutotuneDaemon, FeedbackJournal, TaskProfileOptimizer } = require('./autotune');
12
+ const { VaultManager } = require('./vault');
13
+ const { EpistemicRouter, BeliefCompiler, VerificationEngine, ChangePipeline, FlowOrchestrator, compileDocs, exportTrainingData } = require('./cogops');
14
+ const { WorkspaceChangeListener } = require('./workspace');
15
+ const { SkillEngine } = require('./skills');
16
+ const { buildRepoMap, renderRepoMapMarkdown } = require('./repo_map');
17
+ const { ingestDiff, ingestDiagram, ingestVoice } = require('./multimodal');
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+
21
+ // ── MCP Protocol Implementation (stdio JSON-RPC 2.0) ──
22
+
23
+ class EntrolyMCPServer {
24
+ constructor(config) {
25
+ this.config = config || new EntrolyConfig();
26
+ this.engine = new WasmEntrolyEngine();
27
+ this.turnCounter = 0;
28
+
29
+ // Feedback journal — persists episodes for cross-session autotune
30
+ this.feedbackJournal = new FeedbackJournal(this.config.checkpointDir);
31
+ // Task-conditioned weight profiles (novel: per-task-type optimization)
32
+ this.taskProfiles = new TaskProfileOptimizer(this.feedbackJournal);
33
+ this.taskProfiles.optimizeAll(); // warm from existing journal
34
+ // Track last optimization context for feedback attribution
35
+ this._lastOptCtx = null;
36
+
37
+ // Checkpoint manager
38
+ this.checkpointMgr = new CheckpointManager(
39
+ this.config.checkpointDir, this.config.autoCheckpointInterval
40
+ );
41
+
42
+ // Index path for persistent repo-level indexing
43
+ this.indexPath = path.join(this.config.checkpointDir, 'index.json.gz');
44
+
45
+ // CogOps layer — epistemic engine, vault, compiler, verifier, etc.
46
+ const vaultBase = process.env.ENTROLY_VAULT || path.join(process.env.ENTROLY_DIR || path.join(process.cwd(), '.entroly'), 'vault');
47
+ this.vault = new VaultManager(vaultBase);
48
+ this.epistemicRouter = new EpistemicRouter(this.vault, { missThreshold: 3, freshnessHours: 24, minConfidence: 0.6 });
49
+ this.beliefCompiler = new BeliefCompiler(this.vault);
50
+ this.verifier = new VerificationEngine(this.vault, { freshnessHours: 24, minConfidence: 0.5 });
51
+ this.changePipe = new ChangePipeline(this.vault, this.verifier);
52
+ this.skillEngine = new SkillEngine(this.vault);
53
+ this.sourceDir = process.env.ENTROLY_SOURCE || process.cwd();
54
+ this.flowOrchestrator = new FlowOrchestrator(this.vault, this.epistemicRouter, this.beliefCompiler, this.verifier, this.changePipe, this.sourceDir);
55
+ this.workspaceListener = new WorkspaceChangeListener(this.vault, this.beliefCompiler, this.verifier, this.changePipe, this.sourceDir);
56
+
57
+ // Try loading persistent index
58
+ if (loadIndex(this.engine, this.indexPath)) {
59
+ const n = this.engine.fragment_count();
60
+ this._log(`Loaded persistent index: ${n} fragments`);
61
+ }
62
+
63
+ this._buffer = '';
64
+ this._initialized = false;
65
+ }
66
+
67
+ _log(msg) { process.stderr.write(`${new Date().toISOString()} [entroly] ${msg}\n`); }
68
+
69
+ // ── MCP Tool Definitions (36 tools — full parity with pip) ──
70
+ get tools() {
71
+ return [
72
+ // ── Core Engine Tools (12) ──
73
+ { name: 'remember_fragment', description: 'Store a context fragment with automatic dedup and entropy scoring.', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Text content to store' }, source: { type: 'string', description: 'Origin label (e.g. file:utils.py)', default: '' }, token_count: { type: 'integer', description: 'Token count (auto if 0)', default: 0 }, is_pinned: { type: 'boolean', description: 'Always include in optimized context', default: false } }, required: ['content'] } },
74
+ { name: 'optimize_context', description: 'Select the optimal context subset for a token budget. Uses IOS + PRISM RL + Channel Coding.', inputSchema: { type: 'object', properties: { token_budget: { type: 'integer', description: 'Max tokens (default 128K)', default: 128000 }, query: { type: 'string', description: 'Current task/query for semantic scoring', default: '' } } } },
75
+ { name: 'recall_relevant', description: 'Semantic recall of most relevant stored fragments.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, top_k: { type: 'integer', description: 'Number of results', default: 5 } }, required: ['query'] } },
76
+ { name: 'record_outcome', description: 'Record whether selected fragments led to success/failure (RL feedback).', inputSchema: { type: 'object', properties: { fragment_ids: { type: 'string', description: 'Comma-separated fragment IDs' }, success: { type: 'boolean', description: 'True if output was good', default: true } }, required: ['fragment_ids'] } },
77
+ { name: 'explain_context', description: 'Explain why each fragment was included/excluded in last optimization.', inputSchema: { type: 'object', properties: {} } },
78
+ { name: 'get_stats', description: 'Get comprehensive session statistics.', inputSchema: { type: 'object', properties: {} } },
79
+ { name: 'checkpoint_state', description: 'Save current state for crash recovery.', inputSchema: { type: 'object', properties: { task_description: { type: 'string', default: '' } } } },
80
+ { name: 'resume_state', description: 'Resume from the latest checkpoint.', inputSchema: { type: 'object', properties: {} } },
81
+ { name: 'analyze_codebase_health', description: 'Analyze codebase health (grade A-F).', inputSchema: { type: 'object', properties: {} } },
82
+ { name: 'security_report', description: 'Session-wide security audit across all ingested fragments.', inputSchema: { type: 'object', properties: {} } },
83
+ { name: 'entroly_dashboard', description: 'Show live value metrics: money saved, performance, bloat prevention, quality.', inputSchema: { type: 'object', properties: {} } },
84
+ { name: 'scan_for_vulnerabilities', description: 'Scan code for security vulnerabilities (SAST).', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Source code to scan' }, source: { type: 'string', description: 'File path', default: 'unknown' } }, required: ['content'] } },
85
+ // ── CogOps Epistemic Tools (9) ──
86
+ { name: 'epistemic_route', description: 'Route a query through the CogOps Epistemic Ingress Controller. Inspects 4 signals (intent, belief coverage, freshness, risk) and selects one of 5 canonical flows.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The user query or event description' }, is_event: { type: 'boolean', description: 'True if this is a change-driven event', default: false }, event_type: { type: 'string', description: 'Type of event (pr, commit, release, incident, scheduled)', default: '' } }, required: ['query'] } },
87
+ { name: 'vault_status', description: 'Show the current state of the CogOps Knowledge Vault: total beliefs, verification status, confidence distribution, and routing statistics.', inputSchema: { type: 'object', properties: {} } },
88
+ { name: 'vault_write_belief', description: 'Write a belief artifact to the CogOps Knowledge Vault with machine-auditable frontmatter.', inputSchema: { type: 'object', properties: { entity: { type: 'string', description: 'System entity this belief is about (e.g. auth::token_rotation)' }, title: { type: 'string', description: 'Human-readable title' }, body: { type: 'string', description: 'Belief content (markdown)' }, confidence: { type: 'number', description: 'Machine-assigned confidence 0.0-1.0', default: 0.7 }, status: { type: 'string', description: 'observed|inferred|verified|stale|hypothesis', default: 'inferred' }, sources: { type: 'string', description: 'Comma-separated source paths', default: '' }, derived_from: { type: 'string', description: 'Comma-separated component names', default: '' } }, required: ['entity', 'title', 'body'] } },
89
+ { name: 'vault_query', description: 'Query the CogOps Knowledge Vault for existing beliefs. Supports lookup by entity name or listing all.', inputSchema: { type: 'object', properties: { entity: { type: 'string', description: 'Entity name to look up (fuzzy match)', default: '' }, list_all: { type: 'boolean', description: 'If true, return all beliefs with frontmatter summary', default: false } } } },
90
+ { name: 'vault_write_action', description: 'Write a task output or report to the CogOps Knowledge Vault (actions/).', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the output' }, content: { type: 'string', description: 'Full markdown content' }, action_type: { type: 'string', description: 'Type tag (report, pr_brief, answer, diagram, context_pack)', default: 'report' } }, required: ['title', 'content'] } },
91
+ { name: 'vault_search', description: 'Full-text search across all belief artifacts in the vault. Uses keyword matching with entity-name boosting.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language search query' }, top_k: { type: 'integer', description: 'Maximum results', default: 5 } }, required: ['query'] } },
92
+ { name: 'compile_beliefs', description: 'Compile source code into belief artifacts (Truth → Belief pipeline). Scans directory for source files, extracts entities, resolves dependencies, writes beliefs.', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Path to scan. Defaults to project root.', default: '' }, max_files: { type: 'integer', description: 'Maximum files to process', default: 200 } } } },
93
+ { name: 'verify_beliefs', description: 'Run a full verification pass on all beliefs. Checks staleness, contradictions, confidence divergence, low confidence scores.', inputSchema: { type: 'object', properties: {} } },
94
+ // ── CogOps Analysis & Change Tools (7) ──
95
+ { name: 'blast_radius', description: 'Analyze the blast radius of file changes on existing beliefs. Returns affected beliefs, risk level, and recommendations.', inputSchema: { type: 'object', properties: { changed_files: { type: 'string', description: 'Comma-separated list of changed file paths' } }, required: ['changed_files'] } },
96
+ { name: 'coverage_gaps', description: 'Find source files with no corresponding belief in the vault. Identifies blind spots before running compile_beliefs.', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Path to scan. Defaults to project root.', default: '' } } } },
97
+ { name: 'process_change', description: 'Process a code change through the Change-Driven pipeline (Flow ④). Diff → ChangeSet → Review → Blast Radius → Vault.', inputSchema: { type: 'object', properties: { diff_text: { type: 'string', description: 'Raw unified diff text' }, commit_message: { type: 'string', description: 'Optional commit message', default: '' }, pr_title: { type: 'string', description: 'Optional PR title', default: '' } }, required: ['diff_text'] } },
98
+ { name: 'execute_flow', description: 'Execute a full canonical epistemic flow end-to-end. Routes query through the Ingress Controller then chains the appropriate pipeline.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The user query or event description' }, diff_text: { type: 'string', description: 'Raw diff for change-driven flows', default: '' }, is_event: { type: 'boolean', description: 'True if change-driven event', default: false }, event_type: { type: 'string', description: 'Type of event', default: '' } }, required: ['query'] } },
99
+ { name: 'refresh_beliefs', description: 'Mark beliefs as stale after file changes so the next verify pass will flag them for re-compilation.', inputSchema: { type: 'object', properties: { changed_files: { type: 'string', description: 'Comma-separated list of changed file paths' } }, required: ['changed_files'] } },
100
+ { name: 'compile_docs', description: 'Compile markdown documentation files into belief artifacts with confidence 0.80 (human-authored > machine-inferred).', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Project root to scan. Defaults to project root.', default: '' }, max_files: { type: 'integer', description: 'Maximum doc files to process', default: 50 } } } },
101
+ { name: 'export_training_data', description: 'Export vault beliefs as JSONL training data for LLM finetuning. Filters out stale and low-confidence beliefs.', inputSchema: { type: 'object', properties: { output_path: { type: 'string', description: 'Path to write JSONL file', default: 'training_data.jsonl' }, format: { type: 'string', description: 'Output format (jsonl)', default: 'jsonl' } } } },
102
+ // ── Ingestion Tools (3) ──
103
+ { name: 'ingest_diff', description: 'Ingest a code diff/patch into context memory. Converts unified diff into structured change summary with intent classification.', inputSchema: { type: 'object', properties: { diff_text: { type: 'string', description: 'Raw unified diff text' }, source: { type: 'string', description: 'Identifier (e.g. pr_42_auth_refactor.diff)' }, commit_message: { type: 'string', description: 'Optional commit message', default: '' } }, required: ['diff_text', 'source'] } },
104
+ { name: 'ingest_diagram', description: 'Ingest an image/diagram into context memory (requires PIL/OCR — not available in WASM runtime, use pip install entroly).', inputSchema: { type: 'object', properties: { image_path: { type: 'string', description: 'Path to image file' } }, required: ['image_path'] } },
105
+ { name: 'ingest_voice', description: 'Ingest audio into context memory (requires whisper — not available in WASM runtime, use pip install entroly).', inputSchema: { type: 'object', properties: { audio_path: { type: 'string', description: 'Path to audio file' } }, required: ['audio_path'] } },
106
+ // ── Workspace Tools (2) ──
107
+ { name: 'sync_workspace_changes', description: 'Synchronize workspace file changes into the belief and verification layers. Detects new/modified/deleted files, marks beliefs stale, recompiles.', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Project directory', default: '' }, force: { type: 'boolean', description: 'Force re-scan all files', default: false }, max_files: { type: 'integer', description: 'Maximum files to process', default: 100 } } } },
108
+ { name: 'start_workspace_listener', description: 'Start a background workspace listener that continuously feeds repo changes into CogOps belief CI.', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Project directory', default: '' }, interval_s: { type: 'integer', description: 'Scan interval in seconds', default: 120 }, force_initial: { type: 'boolean', description: 'Force initial full scan', default: false }, max_files: { type: 'integer', description: 'Max files per scan', default: 100 } } } },
109
+ // ── Skills Tools (2) ──
110
+ { name: 'create_skill', description: 'Create a new skill from a capability gap (Evolution layer). Generates SKILL.md, tool.py, metrics.json, and test cases.', inputSchema: { type: 'object', properties: { entity_key: { type: 'string', description: 'Entity this skill handles (e.g. protobuf_analysis)' }, failing_queries: { type: 'string', description: 'Pipe-separated list of failing queries' }, intent: { type: 'string', description: 'Intent class for this skill', default: '' } }, required: ['entity_key', 'failing_queries'] } },
111
+ { name: 'manage_skills', description: 'Manage CogOps skill lifecycle (Evolution layer). Actions: list, benchmark, promote.', inputSchema: { type: 'object', properties: { action: { type: 'string', description: 'list | benchmark | promote', default: 'list' }, skill_id: { type: 'string', description: 'Required for benchmark/promote', default: '' } } } },
112
+ // ── Analysis Tools (2) ──
113
+ { name: 'repo_file_map', description: 'Return the canonical Entroly file map across Python, Rust core, and WASM repos with ownership roles.', inputSchema: { type: 'object', properties: { format: { type: 'string', description: 'Output format: markdown or json', default: 'markdown' } } } },
114
+ { name: 'prefetch_related', description: 'Predict which files the LLM will need next based on co-access patterns and dependency graph.', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Current file being worked on' }, source_content: { type: 'string', description: 'Content of current file for import analysis', default: '' }, language: { type: 'string', description: 'Programming language', default: '' } }, required: ['file_path'] } },
115
+ ];
116
+ }
117
+
118
+ // ── Tool Handlers (36 tools) ──
119
+ handleTool(name, args) {
120
+ switch (name) {
121
+ // ── Core Engine Tools ──
122
+ case 'remember_fragment':
123
+ return this.engine.ingest(args.content, args.source || '', args.token_count || 0, args.is_pinned || false);
124
+
125
+ case 'optimize_context': {
126
+ this.turnCounter++;
127
+ this.engine.advance_turn();
128
+ const budget = args.token_budget || this.config.defaultTokenBudget;
129
+ const query = args.query || '';
130
+ const profile = this.taskProfiles.applyToEngine(this.engine, query);
131
+ const result = this.engine.optimize(budget, query);
132
+ result._taskProfile = { taskType: profile.taskType, confidence: profile.confidence };
133
+ const state = this.engine.export_state();
134
+ this._lastOptCtx = { weights: { w_r: state.w_recency, w_f: state.w_frequency, w_s: state.w_semantic, w_e: state.w_entropy }, selectedSources: (result.selected || []).map(s => s.source).filter(Boolean), selectedCount: result.selected_count || 0, tokenBudget: budget, query, turn: this.turnCounter };
135
+ if (this.checkpointMgr.shouldAutoCheckpoint()) { try { persistIndex(this.engine, this.indexPath); this.checkpointMgr.save({ engine_state: state, turn: this.turnCounter }); } catch {} }
136
+ return result;
137
+ }
138
+
139
+ case 'recall_relevant':
140
+ return this.engine.recall(args.query, args.top_k || 5);
141
+
142
+ case 'record_outcome': {
143
+ const ids = (args.fragment_ids || '').split(',').map(s => s.trim()).filter(Boolean);
144
+ const idsJson = JSON.stringify(ids);
145
+ const success = args.success !== false;
146
+ if (success) this.engine.record_success(idsJson); else this.engine.record_failure(idsJson);
147
+ if (this._lastOptCtx) { this.feedbackJournal.log({ ...this._lastOptCtx, reward: success ? 1.0 : -1.0 }); }
148
+ return { status: 'recorded', fragment_ids: ids, outcome: success ? 'success' : 'failure' };
149
+ }
150
+
151
+ case 'explain_context':
152
+ return this.engine.explain_selection();
153
+
154
+ case 'get_stats': {
155
+ const stats = this.engine.stats();
156
+ stats.checkpoint = this.checkpointMgr.stats();
157
+ return stats;
158
+ }
159
+
160
+ case 'checkpoint_state': {
161
+ try { persistIndex(this.engine, this.indexPath); const p = this.checkpointMgr.save({ engine_state: this.engine.export_state(), turn: this.turnCounter, task: args.task_description || '' }); return { status: 'checkpoint_saved', path: p }; }
162
+ catch (e) { return { status: 'error', error: e.message }; }
163
+ }
164
+
165
+ case 'resume_state': {
166
+ const ckpt = this.checkpointMgr.loadLatest();
167
+ if (!ckpt) return { status: 'no_checkpoint_found' };
168
+ if (ckpt.engine_state) this.engine.import_state(JSON.stringify(ckpt.engine_state));
169
+ return { status: 'resumed', checkpoint_id: ckpt.checkpoint_id, turn: ckpt.turn || 0 };
170
+ }
171
+
172
+ case 'analyze_codebase_health':
173
+ return this.engine.analyze_health();
174
+
175
+ case 'security_report':
176
+ return this.engine.security_report();
177
+
178
+ case 'scan_for_vulnerabilities':
179
+ return this.engine.scan_fragment(args.source || 'unknown');
180
+
181
+ case 'entroly_dashboard': {
182
+ const stats = this.engine.stats();
183
+ const explanation = this.engine.explain_selection();
184
+ return { stats, explanation, turn: this.turnCounter };
185
+ }
186
+
187
+ // ── CogOps Epistemic Tools ──
188
+ case 'epistemic_route':
189
+ return this.epistemicRouter.route(args.query, args.is_event || false, args.event_type || '');
190
+
191
+ case 'vault_status': {
192
+ const init = this.vault.ensureStructure();
193
+ const coverage = this.vault.coverageIndex();
194
+ const routing = this.epistemicRouter.stats();
195
+ return { vault: init, coverage, routing };
196
+ }
197
+
198
+ case 'vault_write_belief': {
199
+ const { randomUUID } = require('crypto');
200
+ const artifact = {
201
+ claim_id: randomUUID(),
202
+ entity: args.entity,
203
+ title: args.title,
204
+ body: args.body,
205
+ confidence: args.confidence ?? 0.7,
206
+ status: args.status || 'inferred',
207
+ sources: args.sources ? args.sources.split(',').map(s => s.trim()).filter(Boolean) : [],
208
+ last_checked: new Date().toISOString(),
209
+ derived_from: args.derived_from ? args.derived_from.split(',').map(s => s.trim()).filter(Boolean) : [],
210
+ };
211
+ const result = this.vault.writeBelief(artifact);
212
+ result.artifact = { claim_id: artifact.claim_id, entity: artifact.entity, status: artifact.status, confidence: artifact.confidence };
213
+ return result;
214
+ }
215
+
216
+ case 'vault_query': {
217
+ if (args.list_all) { const beliefs = this.vault.listBeliefs(); return { beliefs, total: beliefs.length }; }
218
+ if (args.entity) { const r = this.vault.readBelief(args.entity); return r || { status: 'not_found', entity: args.entity }; }
219
+ return this.vault.coverageIndex();
220
+ }
221
+
222
+ case 'vault_write_action':
223
+ return this.vault.writeAction(args.title, args.content, args.action_type || 'report');
224
+
225
+ case 'vault_search': {
226
+ const beliefs = this.vault.listBeliefs();
227
+ const q = (args.query || '').toLowerCase();
228
+ const topK = args.top_k || 5;
229
+ const matches = [];
230
+ for (const b of beliefs) {
231
+ const full = this.vault.readBelief(b.entity);
232
+ if (full && (b.entity.toLowerCase().includes(q) || (full.body || '').toLowerCase().includes(q))) {
233
+ matches.push({ entity: b.entity, confidence: b.confidence, status: b.status, score: b.entity.toLowerCase().includes(q) ? 3.0 : 1.0 });
234
+ }
235
+ }
236
+ matches.sort((a, b) => b.score - a.score);
237
+ return { query: args.query, results: matches.slice(0, topK), total: matches.length, engine: 'javascript' };
238
+ }
239
+
240
+ case 'compile_beliefs':
241
+ return this.beliefCompiler.compileDirectory(args.directory || this.sourceDir, args.max_files || 200);
242
+
243
+ case 'verify_beliefs':
244
+ return this.verifier.fullVerificationPass();
245
+
246
+ // ── CogOps Analysis & Change Tools ──
247
+ case 'blast_radius': {
248
+ const files = (args.changed_files || '').split(',').map(s => s.trim()).filter(Boolean);
249
+ return this.verifier.blastRadius(files);
250
+ }
251
+
252
+ case 'coverage_gaps':
253
+ return this.verifier.coverageGaps(args.directory || this.sourceDir);
254
+
255
+ case 'process_change':
256
+ return this.changePipe.processDiff(args.diff_text, args.commit_message || '', args.pr_title || '');
257
+
258
+ case 'execute_flow':
259
+ return this.flowOrchestrator.execute(args.query, args.diff_text || '', args.is_event || false, args.event_type || '');
260
+
261
+ case 'refresh_beliefs': {
262
+ const files = (args.changed_files || '').split(',').map(s => s.trim()).filter(Boolean);
263
+ return this.changePipe.refreshDocs(files);
264
+ }
265
+
266
+ case 'compile_docs':
267
+ return compileDocs(this.vault, args.directory || this.sourceDir, args.max_files || 50);
268
+
269
+ case 'export_training_data':
270
+ return exportTrainingData(this.vault, args.output_path || 'training_data.jsonl');
271
+
272
+ // ── Ingestion Tools ──
273
+ case 'ingest_diff': {
274
+ const modal = ingestDiff(args.diff_text, args.source, args.commit_message || '');
275
+ const data = this.engine.ingest(modal.text, args.source, modal.token_estimate, false);
276
+ return { ...data, modal_source_type: 'diff', intent: modal.metadata.intent, files_changed: modal.metadata.files_changed, added_lines: modal.metadata.added_lines, removed_lines: modal.metadata.removed_lines, symbols_changed: modal.metadata.symbols_changed };
277
+ }
278
+
279
+ case 'ingest_diagram':
280
+ return ingestDiagram();
281
+
282
+ case 'ingest_voice':
283
+ return ingestVoice();
284
+
285
+ // ── Workspace Tools ──
286
+ case 'sync_workspace_changes': {
287
+ const listener = args.directory ? new WorkspaceChangeListener(this.vault, this.beliefCompiler, this.verifier, this.changePipe, args.directory) : this.workspaceListener;
288
+ return listener.scanOnce(args.force || false, args.max_files || 100);
289
+ }
290
+
291
+ case 'start_workspace_listener': {
292
+ const listener = args.directory ? new WorkspaceChangeListener(this.vault, this.beliefCompiler, this.verifier, this.changePipe, args.directory) : this.workspaceListener;
293
+ return listener.start(args.interval_s || 120, args.max_files || 100, args.force_initial || false);
294
+ }
295
+
296
+ // ── Skills Tools ──
297
+ case 'create_skill': {
298
+ const queries = (args.failing_queries || '').split('|').map(s => s.trim()).filter(Boolean);
299
+ return this.skillEngine.createSkill(args.entity_key, queries, args.intent || '');
300
+ }
301
+
302
+ case 'manage_skills': {
303
+ const action = args.action || 'list';
304
+ if (action === 'list') { const skills = this.skillEngine.listSkills(); return { skills, total: skills.length, engine: 'javascript' }; }
305
+ if (!args.skill_id) return { error: `skill_id required for '${action}'` };
306
+ if (action === 'benchmark') return this.skillEngine.benchmarkSkill(args.skill_id);
307
+ if (action === 'promote') return this.skillEngine.promoteOrPrune(args.skill_id);
308
+ return { error: `Unknown action '${action}'. Use: list, benchmark, promote` };
309
+ }
310
+
311
+ // ── Analysis Tools ──
312
+ case 'repo_file_map': {
313
+ const rootDir = path.resolve(this.sourceDir, '..');
314
+ const grouped = buildRepoMap(rootDir);
315
+ if ((args.format || '').toLowerCase() === 'json') return grouped;
316
+ return renderRepoMapMarkdown(grouped);
317
+ }
318
+
319
+ case 'prefetch_related': {
320
+ // Use dep_graph_stats + import analysis for prediction
321
+ const depStats = this.engine.dep_graph_stats();
322
+ const imports = [];
323
+ const content = args.source_content || '';
324
+ const importRe = /(?:import|require|from|use|include)\s+['"]?([^\s'";\)]+)/g;
325
+ let m;
326
+ while ((m = importRe.exec(content)) !== null) imports.push(m[1]);
327
+ return { file: args.file_path, language: args.language || 'unknown', predicted_files: imports.slice(0, 10), dep_graph: depStats, engine: 'javascript' };
328
+ }
329
+
330
+ default:
331
+ throw new Error(`Unknown tool: ${name}`);
332
+ }
333
+ }
334
+
335
+ // ── JSON-RPC stdio transport ──
336
+ async run() {
337
+ this._log(`Starting Entroly MCP server v${this.config.serverVersion} (Wasm engine)`);
338
+
339
+ // Auto-index on startup
340
+ try {
341
+ const result = autoIndex(this.engine);
342
+ if (result.status === 'indexed') {
343
+ this._log(`Auto-indexed ${result.files_indexed} files (${result.total_tokens.toLocaleString()} tokens) in ${result.duration_s}s`);
344
+ }
345
+ startIncrementalWatcher(this.engine);
346
+ } catch (e) {
347
+ this._log(`Auto-index failed (non-fatal): ${e.message}`);
348
+ }
349
+
350
+ // Start autotune daemon — reads tuning_config.json, hot-reloads weights
351
+ try {
352
+ const tid = startAutotuneDaemon(this.engine);
353
+ if (tid) this._log('Autotune daemon started (hot-reload every 30s)');
354
+ } catch (e) {
355
+ this._log(`Autotune: failed to start daemon: ${e.message}`);
356
+ }
357
+
358
+ // Graceful shutdown
359
+ process.on('SIGTERM', () => {
360
+ this._log('Shutdown — persisting state...');
361
+ try { persistIndex(this.engine, this.indexPath); } catch {}
362
+ process.exit(0);
363
+ });
364
+
365
+ // Read JSONRPC from stdin
366
+ process.stdin.setEncoding('utf-8');
367
+ process.stdin.on('data', (chunk) => {
368
+ this._buffer += chunk;
369
+ this._processBuffer();
370
+ });
371
+ process.stdin.on('end', () => {
372
+ this._log('stdin closed — shutting down');
373
+ try { persistIndex(this.engine, this.indexPath); } catch {}
374
+ });
375
+ }
376
+
377
+ _processBuffer() {
378
+ while (true) {
379
+ const headerEnd = this._buffer.indexOf('\r\n\r\n');
380
+
381
+ if (headerEnd !== -1) {
382
+ const header = this._buffer.slice(0, headerEnd);
383
+ const match = header.match(/Content-Length:\s*(\d+)/i);
384
+ if (match) {
385
+ const contentLength = parseInt(match[1], 10);
386
+ const bodyStart = headerEnd + 4;
387
+ if (this._buffer.length < bodyStart + contentLength) return;
388
+ const body = this._buffer.slice(bodyStart, bodyStart + contentLength);
389
+ this._buffer = this._buffer.slice(bodyStart + contentLength);
390
+ try {
391
+ this._handleMessage(JSON.parse(body));
392
+ } catch (e) {
393
+ this._log(`JSON parse error (LSP frame): ${e.message}`);
394
+ }
395
+ continue;
396
+ }
397
+ }
398
+
399
+ const nlIdx = this._buffer.indexOf('\n');
400
+ if (nlIdx === -1) return;
401
+ const line = this._buffer.slice(0, nlIdx).trim();
402
+ this._buffer = this._buffer.slice(nlIdx + 1);
403
+ if (!line) continue;
404
+ try {
405
+ this._handleMessage(JSON.parse(line));
406
+ } catch (e) {
407
+ this._log(`JSON parse error (NDJSON): ${e.message}`);
408
+ }
409
+ }
410
+ }
411
+
412
+ _handleMessage(msg) {
413
+ if (msg.method === 'initialize') {
414
+ this._initialized = true;
415
+ this._respond(msg.id, {
416
+ protocolVersion: '2024-11-05',
417
+ capabilities: { tools: { listChanged: false } },
418
+ serverInfo: { name: this.config.serverName, version: this.config.serverVersion },
419
+ });
420
+ } else if (msg.method === 'notifications/initialized') {
421
+ // No response needed for notifications
422
+ } else if (msg.method === 'tools/list') {
423
+ this._respond(msg.id, { tools: this.tools });
424
+ } else if (msg.method === 'tools/call') {
425
+ const { name, arguments: toolArgs } = msg.params;
426
+ try {
427
+ const result = this.handleTool(name, toolArgs || {});
428
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
429
+ this._respond(msg.id, {
430
+ content: [{ type: 'text', text }], isError: false,
431
+ });
432
+ } catch (e) {
433
+ this._respond(msg.id, {
434
+ content: [{ type: 'text', text: JSON.stringify({ error: e.message }) }], isError: true,
435
+ });
436
+ }
437
+ } else if (msg.method === 'ping') {
438
+ this._respond(msg.id, {});
439
+ } else if (msg.id !== undefined) {
440
+ // Unknown method with ID — respond with error
441
+ this._respondError(msg.id, -32601, `Method not found: ${msg.method}`);
442
+ }
443
+ }
444
+
445
+ _respond(id, result) {
446
+ if (id === undefined) return;
447
+ const response = JSON.stringify({ jsonrpc: '2.0', id, result });
448
+ const header = `Content-Length: ${Buffer.byteLength(response)}\r\n\r\n`;
449
+ process.stdout.write(header + response);
450
+ }
451
+
452
+ _respondError(id, code, message) {
453
+ const response = JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
454
+ const header = `Content-Length: ${Buffer.byteLength(response)}\r\n\r\n`;
455
+ process.stdout.write(header + response);
456
+ }
457
+ }
458
+
459
+ // ── Entry Point ──
460
+ if (require.main === module) {
461
+ const server = new EntrolyMCPServer();
462
+ server.run();
463
+ }
464
+
465
+ module.exports = { EntrolyMCPServer };