import { promises as fs } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { spawn } from 'child_process';
export class SlashCommands {
constructor(memoryTools, configurationTools, factStore) {
this.memoryTools = memoryTools;
this.configurationTools = configurationTools;
this.factStore = factStore;
this.commands = new Map();
this.setupCommands();
}
setupCommands() {
this.commands.set('memconfig', this.handleConfigCommand.bind(this));
this.commands.set('remember', this.handleRememberCommand.bind(this));
this.commands.set('recall', this.handleRecallCommand.bind(this));
this.commands.set('insights', this.handleInsightsCommand.bind(this));
}
async processSlashCommand(prompt) {
if (!prompt.startsWith('/')) {
return null;
}
const parts = prompt.slice(1).split(' ');
const command = parts[0].toLowerCase();
const args = parts.slice(1);
if (!this.commands.has(command)) {
return {
success: false,
message: `Unknown slash command: /${command}`,
availableCommands: Array.from(this.commands.keys()),
};
}
try {
return await this.commands.get(command)(args, prompt);
} catch (error) {
return {
success: false,
message: `Error executing /${command}: ${error.message}`,
error: error.stack,
};
}
}
async handleConfigCommand(args, fullPrompt) {
const subCommand = args[0]?.toLowerCase();
switch (subCommand) {
case 'gui':
case 'open':
case undefined:
return await this.openConfigGUI();
case 'show':
return await this.showCurrentConfig();
case 'reset':
return await this.resetConfig(args.includes('--confirm'));
case 'export':
return await this.exportConfig();
default:
return {
success: false,
message: `Unknown memconfig subcommand: ${subCommand}`,
usage: '/memconfig [gui|show|reset|export]',
examples: [
'/memconfig - Open configuration GUI',
'/memconfig show - Display current configuration',
'/memconfig reset --confirm - Reset to defaults',
'/memconfig export - Export current configuration',
],
};
}
}
async openConfigGUI() {
try {
const guiPath = await this.createConfigGUI();
// Try to open in default browser
const command = process.platform === 'win32' ? 'start' :
process.platform === 'darwin' ? 'open' : 'xdg-open';
spawn(command, [guiPath], { detached: true, stdio: 'ignore' });
return {
success: true,
message: 'Configuration GUI opened in your default browser',
guiPath,
instructions: 'Use the web interface to configure fact types and scoring weights',
};
} catch (error) {
return {
success: false,
message: `Failed to open configuration GUI: ${error.message}`,
};
}
}
async createConfigGUI() {
const configDir = join(homedir(), '.mcp_sequential_thinking', 'config');
const guiPath = join(configDir, 'config-gui.html');
await fs.mkdir(configDir, { recursive: true });
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>meMCP Configuration</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
color: #2d3748;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
text-align: center;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; }
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: 600; }
input, select, textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
background: #667eea;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
.btn:hover { background: #5a67d8; transform: translateY(-1px); }
.btn-secondary { background: #718096; }
.btn-secondary:hover { background: #4a5568; }
.btn-danger { background: #e53e3e; }
.btn-danger:hover { background: #c53030; }
.fact-type {
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.fact-type.custom { border-color: #667eea; }
.slider { width: 100%; }
.weight-display {
display: inline-block;
margin-left: 10px;
font-weight: bold;
color: #667eea;
}
.status {
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
.status.success { background: #f0fff4; color: #22543d; border: 2px solid #68d391; }
.status.error { background: #fed7d7; color: #742a2a; border: 2px solid #fc8181; }
.footer { text-align: center; margin-top: 2rem; color: #718096; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧠 meMCP Configuration</h1>
<p>Configure fact types and scoring weights for your memory system</p>
</div>
<div id="status" class="status" style="display: none;"></div>
<div class="grid">
<div class="card">
<h2>📊 Scoring Weights</h2>
<p>Adjust the relative importance of each scoring dimension (must sum to 1.0):</p>
<div class="form-group">
<label>Novelty (How unique is this insight?)</label>
<input type="range" class="slider" id="novelty" min="0" max="1" step="0.05" value="0.25">
<span class="weight-display" id="novelty-value">0.25</span>
</div>
<div class="form-group">
<label>Generalizability (How broadly applicable?)</label>
<input type="range" class="slider" id="generalizability" min="0" max="1" step="0.05" value="0.25">
<span class="weight-display" id="generalizability-value">0.25</span>
</div>
<div class="form-group">
<label>Specificity (How actionable and detailed?)</label>
<input type="range" class="slider" id="specificity" min="0" max="1" step="0.05" value="0.20">
<span class="weight-display" id="specificity-value">0.20</span>
</div>
<div class="form-group">
<label>Validation (How well-tested or proven?)</label>
<input type="range" class="slider" id="validation" min="0" max="1" step="0.05" value="0.15">
<span class="weight-display" id="validation-value">0.15</span>
</div>
<div class="form-group">
<label>Impact (Potential impact on development)</label>
<input type="range" class="slider" id="impact" min="0" max="1" step="0.05" value="0.15">
<span class="weight-display" id="impact-value">0.15</span>
</div>
<div class="form-group">
<strong>Total: <span id="total-weight">1.00</span></strong>
</div>
<button class="btn" onclick="updateScoringWeights()">Update Scoring Weights</button>
<button class="btn btn-secondary" onclick="loadScoringProfiles()">Load Profile</button>
</div>
<div class="card">
<h2>🏷️ Fact Types</h2>
<div id="fact-types-list"></div>
<h3>Add Custom Fact Type</h3>
<div class="form-group">
<label>Name (snake_case)</label>
<input type="text" id="new-fact-name" placeholder="e.g., security_concern">
</div>
<div class="form-group">
<label>Priority</label>
<select id="new-fact-priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div class="form-group">
<label>Description</label>
<textarea id="new-fact-description" placeholder="Describe this fact type..."></textarea>
</div>
<div class="form-group">
<label>Keywords (comma-separated)</label>
<input type="text" id="new-fact-keywords" placeholder="keyword1, keyword2, keyword3">
</div>
<button class="btn" onclick="addFactType()">Add Fact Type</button>
</div>
</div>
<div class="card">
<h2>⚙️ Configuration Management</h2>
<button class="btn" onclick="exportConfig()">Export Configuration</button>
<button class="btn btn-secondary" onclick="importConfig()">Import Configuration</button>
<button class="btn btn-danger" onclick="resetConfig()">Reset to Defaults</button>
</div>
<div class="footer">
<p>meMCP Configuration Interface - Close this tab when done</p>
</div>
</div>
<script>
let currentConfig = { factTypes: {}, scoringWeights: {} };
// Load current configuration on page load
window.addEventListener('load', async () => {
await loadCurrentConfig();
setupSliders();
});
function setupSliders() {
const sliders = ['novelty', 'generalizability', 'specificity', 'validation', 'impact'];
sliders.forEach(name => {
const slider = document.getElementById(name);
const display = document.getElementById(name + '-value');
slider.addEventListener('input', () => {
display.textContent = parseFloat(slider.value).toFixed(2);
updateTotalWeight();
});
});
}
function updateTotalWeight() {
const sliders = ['novelty', 'generalizability', 'specificity', 'validation', 'impact'];
const total = sliders.reduce((sum, name) => {
return sum + parseFloat(document.getElementById(name).value);
}, 0);
const totalDisplay = document.getElementById('total-weight');
totalDisplay.textContent = total.toFixed(2);
if (Math.abs(total - 1.0) < 0.01) {
totalDisplay.style.color = '#22543d';
} else {
totalDisplay.style.color = '#e53e3e';
}
}
async function loadCurrentConfig() {
try {
// In a real implementation, this would call the MCP server
// For now, we'll use localStorage to simulate configuration
const saved = localStorage.getItem('mcp-config');
if (saved) {
currentConfig = JSON.parse(saved);
updateUIFromConfig();
}
} catch (error) {
showStatus('Failed to load current configuration', 'error');
}
}
function updateUIFromConfig() {
// Update scoring weight sliders
if (currentConfig.scoringWeights) {
Object.entries(currentConfig.scoringWeights).forEach(([key, value]) => {
const slider = document.getElementById(key);
const display = document.getElementById(key + '-value');
if (slider && display) {
slider.value = typeof value === 'object' ? value.weight : value;
display.textContent = parseFloat(slider.value).toFixed(2);
}
});
updateTotalWeight();
}
// Update fact types list
renderFactTypes();
}
function renderFactTypes() {
const container = document.getElementById('fact-types-list');
container.innerHTML = '';
Object.entries(currentConfig.factTypes || {}).forEach(([name, config]) => {
const div = document.createElement('div');
div.className = 'fact-type' + (config._custom ? ' custom' : '');
div.innerHTML = \`
<strong>\${name}</strong>
<span style="color: #718096;">(\${config.priority})</span>
\${config._custom ? '<span style="color: #667eea; font-weight: bold;">CUSTOM</span>' : ''}
<br>
<small>\${config.description}</small>
\${config._custom ? '<button class="btn btn-danger" style="margin-top: 0.5rem; padding: 0.25rem 0.5rem; font-size: 0.8rem;" onclick="removeFactType(\\''+name+'\\')">Remove</button>' : ''}
\`;
container.appendChild(div);
});
}
async function updateScoringWeights() {
const weights = {
novelty: parseFloat(document.getElementById('novelty').value),
generalizability: parseFloat(document.getElementById('generalizability').value),
specificity: parseFloat(document.getElementById('specificity').value),
validation: parseFloat(document.getElementById('validation').value),
impact: parseFloat(document.getElementById('impact').value),
};
const total = Object.values(weights).reduce((sum, val) => sum + val, 0);
if (Math.abs(total - 1.0) > 0.01) {
showStatus('Scoring weights must sum to 1.0 (currently: ' + total.toFixed(2) + ')', 'error');
return;
}
currentConfig.scoringWeights = weights;
localStorage.setItem('mcp-config', JSON.stringify(currentConfig));
showStatus('Scoring weights updated successfully!', 'success');
}
async function addFactType() {
const name = document.getElementById('new-fact-name').value.trim();
const priority = document.getElementById('new-fact-priority').value;
const description = document.getElementById('new-fact-description').value.trim();
const keywords = document.getElementById('new-fact-keywords').value.split(',').map(k => k.trim()).filter(k => k);
if (!name || !description) {
showStatus('Name and description are required', 'error');
return;
}
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
showStatus('Name must be lowercase with underscores (snake_case)', 'error');
return;
}
if (!currentConfig.factTypes) currentConfig.factTypes = {};
currentConfig.factTypes[name] = {
priority,
retention: 'permanent',
description,
keywords,
scoring_multiplier: 1.0,
_custom: true,
_created: new Date().toISOString()
};
localStorage.setItem('mcp-config', JSON.stringify(currentConfig));
renderFactTypes();
// Clear form
document.getElementById('new-fact-name').value = '';
document.getElementById('new-fact-description').value = '';
document.getElementById('new-fact-keywords').value = '';
showStatus('Fact type "' + name + '" added successfully!', 'success');
}
function removeFactType(name) {
if (confirm('Are you sure you want to remove the fact type "' + name + '"?')) {
delete currentConfig.factTypes[name];
localStorage.setItem('mcp-config', JSON.stringify(currentConfig));
renderFactTypes();
showStatus('Fact type "' + name + '" removed', 'success');
}
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = 'status ' + type;
status.style.display = 'block';
setTimeout(() => {
status.style.display = 'none';
}, 5000);
}
function exportConfig() {
const blob = new Blob([JSON.stringify(currentConfig, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mcp-config-' + new Date().toISOString().split('T')[0] + '.json';
a.click();
URL.revokeObjectURL(url);
showStatus('Configuration exported successfully!', 'success');
}
function importConfig() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
currentConfig = { ...currentConfig, ...imported };
localStorage.setItem('mcp-config', JSON.stringify(currentConfig));
updateUIFromConfig();
showStatus('Configuration imported successfully!', 'success');
} catch (error) {
showStatus('Invalid configuration file', 'error');
}
};
reader.readAsText(file);
}
};
input.click();
}
function resetConfig() {
if (confirm('Are you sure you want to reset all configuration to defaults? This cannot be undone.')) {
localStorage.removeItem('mcp-config');
currentConfig = { factTypes: {}, scoringWeights: {} };
updateUIFromConfig();
showStatus('Configuration reset to defaults', 'success');
}
}
function loadScoringProfiles() {
const profiles = {
'Research-Focused': { novelty: 0.35, validation: 0.25, generalizability: 0.2, specificity: 0.15, impact: 0.05 },
'Production-Focused': { validation: 0.3, impact: 0.3, specificity: 0.2, generalizability: 0.15, novelty: 0.05 },
'Learning-Focused': { novelty: 0.3, generalizability: 0.3, specificity: 0.2, validation: 0.1, impact: 0.1 },
'Balanced': { novelty: 0.25, generalizability: 0.25, specificity: 0.2, validation: 0.15, impact: 0.15 }
};
const profile = prompt('Choose a profile:\\n' + Object.keys(profiles).map((p, i) => (i+1) + '. ' + p).join('\\n'));
const profileName = Object.keys(profiles)[parseInt(profile) - 1];
if (profileName && profiles[profileName]) {
Object.entries(profiles[profileName]).forEach(([key, value]) => {
const slider = document.getElementById(key);
const display = document.getElementById(key + '-value');
slider.value = value;
display.textContent = value.toFixed(2);
});
updateTotalWeight();
showStatus('Loaded ' + profileName + ' profile', 'success');
}
}
</script>
</body>
</html>`;
await fs.writeFile(guiPath, htmlContent);
return guiPath;
}
async showCurrentConfig() {
try {
const factTypes = await this.configurationTools.handleConfigTool('config_list_fact_types', {});
return {
success: true,
message: 'Current configuration:',
config: JSON.parse(factTypes.content[0].text),
};
} catch (error) {
return {
success: false,
message: `Failed to load configuration: ${error.message}`,
};
}
}
async resetConfig(confirmed) {
if (!confirmed) {
return {
success: false,
message: 'Configuration reset requires confirmation. Use: /memconfig reset --confirm',
};
}
try {
const result = await this.configurationTools.handleConfigTool('config_reset', { confirm: true });
return {
success: true,
message: 'Configuration reset to defaults successfully',
details: JSON.parse(result.content[0].text),
};
} catch (error) {
return {
success: false,
message: `Failed to reset configuration: ${error.message}`,
};
}
}
async exportConfig() {
try {
const result = await this.configurationTools.handleConfigTool('config_export', {});
return {
success: true,
message: 'Configuration exported',
config: JSON.parse(result.content[0].text),
};
} catch (error) {
return {
success: false,
message: `Failed to export configuration: ${error.message}`,
};
}
}
async handleRememberCommand(args, fullPrompt) {
if (args.length === 0) {
return {
success: false,
message: 'Please provide an insight to remember',
usage: '/remember <insight> [--type <fact_type>] [--domain <domain>]',
examples: [
'/remember Use debouncing for API calls to prevent rate limiting',
'/remember Always validate user input --type security_concern',
'/remember React hooks must be called in order --domain web_development',
],
};
}
// Parse arguments
const content = args.join(' ').replace(/--\w+\s+\w+/g, '').trim();
const typeMatch = fullPrompt.match(/--type\s+(\w+)/);
const domainMatch = fullPrompt.match(/--domain\s+(\w+)/);
const insight = {
content,
type: typeMatch ? typeMatch[1] : 'discovery',
domain: domainMatch ? domainMatch[1] : 'general',
tags: [],
context: {
source: 'slash_command',
timestamp: new Date().toISOString(),
},
};
try {
const result = await this.memoryTools.storeInsight(insight);
return {
success: true,
message: `Insight stored successfully with score ${result.qualityScore}/100`,
insight: result,
};
} catch (error) {
return {
success: false,
message: `Failed to store insight: ${error.message}`,
};
}
}
async handleRecallCommand(args, fullPrompt) {
if (args.length === 0) {
return {
success: false,
message: 'Please provide a search query',
usage: '/recall <query> [--type <fact_type>] [--limit <number>]',
examples: [
'/recall API performance optimization',
'/recall security --type security_concern',
'/recall React patterns --limit 5',
],
};
}
// Parse arguments
const query = args.join(' ').replace(/--\w+\s+\w+/g, '').trim();
const typeMatch = fullPrompt.match(/--type\s+(\w+)/);
const limitMatch = fullPrompt.match(/--limit\s+(\d+)/);
const searchParams = {
query,
limit: limitMatch ? parseInt(limitMatch[1]) : 5,
};
if (typeMatch) {
searchParams.type = typeMatch[1];
}
try {
const queryResult = await this.factStore.queryFacts(searchParams);
const results = queryResult.facts || [];
if (results.length === 0) {
return {
success: true,
message: `No insights found for "${query}"`,
suggestions: 'Try using different keywords or check /insights for recent memories',
};
}
return {
success: true,
message: `Found ${results.length} insights for "${query}":`,
results: results.map(fact => ({
content: fact.content,
type: fact.type,
score: fact.qualityScore,
domain: fact.domain,
tags: fact.tags,
created: fact.timestamp,
})),
};
} catch (error) {
return {
success: false,
message: `Failed to search memories: ${error.message}`,
};
}
}
async handleInsightsCommand(args, fullPrompt) {
const subCommand = args[0]?.toLowerCase();
switch (subCommand) {
case 'recent':
return await this.getRecentInsights(args[1] ? parseInt(args[1]) : 10);
case 'top':
return await this.getTopInsights(args[1] ? parseInt(args[1]) : 5);
case 'stats':
return await this.getInsightStats();
case 'domains':
return await this.getDomainSummary();
default:
return await this.getInsightsDashboard();
}
}
async getRecentInsights(limit = 10) {
try {
const facts = await this.factStore.getRecentFacts(limit);
return {
success: true,
message: `${facts.length} most recent insights:`,
insights: facts.map(fact => ({
content: fact.content.substring(0, 100) + (fact.content.length > 100 ? '...' : ''),
type: fact.type,
score: fact.qualityScore,
created: fact.timestamp,
})),
};
} catch (error) {
return {
success: false,
message: `Failed to get recent insights: ${error.message}`,
};
}
}
async getTopInsights(limit = 5) {
try {
const facts = await this.factStore.getTopScoringFacts(limit);
return {
success: true,
message: `Top ${facts.length} highest-scoring insights:`,
insights: facts.map(fact => ({
content: fact.content.substring(0, 150) + (fact.content.length > 150 ? '...' : ''),
type: fact.type,
score: fact.qualityScore,
domain: fact.domain,
created: fact.timestamp,
})),
};
} catch (error) {
return {
success: false,
message: `Failed to get top insights: ${error.message}`,
};
}
}
async getInsightStats() {
try {
const stats = await this.factStore.getStats();
return {
success: true,
message: 'Memory system statistics:',
stats: {
totalFacts: stats.totalFacts,
averageScore: stats.averageQualityScore,
factsByType: stats.factsByType,
factsByDomain: stats.factsByDomain,
recentActivity: stats.factsAddedLast7Days,
},
};
} catch (error) {
return {
success: false,
message: `Failed to get statistics: ${error.message}`,
};
}
}
async getDomainSummary() {
try {
const facts = await this.factStore.getAllFacts();
const domains = {};
facts.forEach(fact => {
const domain = fact.domain || 'general';
if (!domains[domain]) {
domains[domain] = { count: 0, avgScore: 0, types: new Set() };
}
domains[domain].count++;
domains[domain].avgScore += fact.qualityScore;
domains[domain].types.add(fact.type);
});
Object.values(domains).forEach(domain => {
domain.avgScore = Math.round(domain.avgScore / domain.count);
domain.types = Array.from(domain.types);
});
return {
success: true,
message: 'Knowledge domains summary:',
domains,
};
} catch (error) {
return {
success: false,
message: `Failed to get domain summary: ${error.message}`,
};
}
}
async getInsightsDashboard() {
try {
const [recent, top, stats] = await Promise.all([
this.getRecentInsights(5),
this.getTopInsights(3),
this.getInsightStats(),
]);
return {
success: true,
message: 'Insights Dashboard',
dashboard: {
recent: recent.insights,
top: top.insights,
stats: stats.stats,
},
availableCommands: [
'/insights recent [limit] - Show recent insights',
'/insights top [limit] - Show highest-scoring insights',
'/insights stats - Show system statistics',
'/insights domains - Show knowledge domains',
],
};
} catch (error) {
return {
success: false,
message: `Failed to create dashboard: ${error.message}`,
};
}
}
getAvailableCommands() {
return [
{
command: '/memconfig',
description: 'Open configuration GUI or manage settings',
usage: '/memconfig [gui|show|reset|export]',
},
{
command: '/remember',
description: 'Store a new insight in memory',
usage: '/remember <insight> [--type <fact_type>] [--domain <domain>]',
},
{
command: '/recall',
description: 'Search for insights in memory',
usage: '/recall <query> [--type <fact_type>] [--limit <number>]',
},
{
command: '/insights',
description: 'View insights dashboard and statistics',
usage: '/insights [recent|top|stats|domains]',
},
];
}
}