UNPKG

@calmhive/calmhive-cli

Version:

🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry

241 lines (203 loc) 6.4 kB
const fs = require('fs'); const path = require('path'); const os = require('os'); /** * Calmhive Rule Injector - Reliable CLAUDE.md Rule Injection * * This module provides reliable CLAUDE.md rule injection at the Calmhive level, * ensuring every conversation gets rules regardless of tool usage or conversation type. * * Unlike hooks (which only fire before tool use), this approach guarantees * rule injection for ALL Calmhive commands: chat, run, afk, voice, etc. * * @example * const ruleInjector = require('./rule-injector'); * const task = ruleInjector.injectRules('analyze the codebase'); * // Returns: 'analyze the codebase\n\n<system-reminder>...CLAUDE.md content...</system-reminder>' */ class RuleInjector { /** * Creates a new RuleInjector instance * * Initializes paths for Claude configuration directory, CLAUDE.md file, * and user settings. Automatically loads user preferences on creation. */ constructor() { this.claudeDir = path.join(os.homedir(), '.claude'); this.claudeMdPath = path.join(this.claudeDir, 'CLAUDE.md'); this.enabled = false; // Default disabled to reduce log clutter this.settingsPath = path.join(this.claudeDir, 'calmhive-settings.json'); // Load user settings this.loadSettings(); } /** * Load user settings for rule injection */ loadSettings() { try { if (fs.existsSync(this.settingsPath)) { const settings = JSON.parse(fs.readFileSync(this.settingsPath, 'utf8')); this.enabled = settings.ruleInjection === true; // Default to false unless explicitly enabled } } catch (error) { // If settings file is corrupt, default to disabled this.enabled = false; } } /** * Save user settings */ saveSettings() { try { if (!fs.existsSync(this.claudeDir)) { fs.mkdirSync(this.claudeDir, { recursive: true }); } const settings = { ruleInjection: this.enabled, lastModified: new Date().toISOString() }; fs.writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2)); } catch (error) { // Fail silently - don't break command execution for settings issues } } /** * Enable rule injection */ enable() { this.enabled = true; this.saveSettings(); } /** * Disable rule injection */ disable() { this.enabled = false; this.saveSettings(); } /** * Check if rule injection is enabled */ isEnabled() { return this.enabled; } /** * Extract rules from actual ~/.claude/CLAUDE.md file */ extractRulesFromCLAUDEmd() { try { if (!fs.existsSync(this.claudeMdPath)) { return null; } // Read the actual CLAUDE.md content and return it in full const content = fs.readFileSync(this.claudeMdPath, 'utf8'); if (content.trim().length === 0) { return null; } // Return the full CLAUDE.md content as rules return `CLAUDE.md RULES:\n\n${content}`; } catch (error) { return null; } } /** * Inject rules into a user message/prompt * @param {string} userMessage - The user's message or prompt * @param {Object} options - Options for injection * @returns {string} - Message with rules injected (or original if disabled/no rules) */ injectRules(userMessage, options = {}) { // Skip injection if disabled if (!this.enabled) { return userMessage; } // Skip injection if rules already present (prevent double injection) if (userMessage.includes('CLAUDE.md RULES:')) { return userMessage; } // Extract rules from CLAUDE.md const rules = this.extractRulesFromCLAUDEmd(); if (!rules) { return userMessage; } // Different injection patterns based on command type const commandType = options.commandType || 'chat'; switch (commandType) { case 'chat': // For interactive chat, prepend rules to user message return `${rules}\n\nUSER: ${userMessage}`; case 'run': // For run command, insert rules before task execution return `${rules}\n\nTASK: ${userMessage}`; case 'afk': // For AFk, inject rules at start of each iteration return `${rules}\n\nAFK TASK: ${userMessage}`; case 'voice': // For voice, inject rules before voice command return `${rules}\n\nVOICE COMMAND: ${userMessage}`; default: // Generic injection pattern return `${rules}\n\n${userMessage}`; } } /** * Inject rules for multi-turn conversations * @param {Array} messages - Array of conversation messages * @param {Object} options - Options for injection * @returns {Array} - Messages with rules injected in appropriate places */ injectRulesIntoConversation(messages, options = {}) { if (!this.enabled || !Array.isArray(messages)) { return messages; } const rules = this.extractRulesFromCLAUDEmd(); if (!rules) { return messages; } // Find the last user message and inject rules if not already present const updatedMessages = [...messages]; for (let i = updatedMessages.length - 1; i >= 0; i--) { if (updatedMessages[i].role === 'user') { const content = updatedMessages[i].content; // Only inject if rules not already present if (!content.includes('CLAUDE.md RULES:')) { updatedMessages[i] = { ...updatedMessages[i], content: `${rules}\n\nUSER: ${content}` }; } break; } } return updatedMessages; } /** * Get status information for debugging */ getStatus() { return { enabled: this.enabled, claudeMdExists: fs.existsSync(this.claudeMdPath), settingsPath: this.settingsPath, rulesExtracted: !!this.extractRulesFromCLAUDEmd() }; } /** * Get user-friendly status message */ getStatusMessage() { const status = this.getStatus(); if (!status.enabled) { return '❌ Rule injection DISABLED by user'; } if (!status.claudeMdExists) { return '⚠️ No CLAUDE.md file found - rules cannot be injected'; } if (!status.rulesExtracted) { return '⚠️ No extractable rules found in CLAUDE.md'; } return '✅ Rule injection ENABLED and ready'; } } // Export singleton instance module.exports = new RuleInjector();