@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
946 lines (809 loc) • 32 kB
JavaScript
/**
* Calmhive Chat Interceptor
*
* This module intercepts Claude API calls at the network level to inject
* CLAUDE.md rules into every message in interactive chat sessions.
*
* Based on the claude-trace approach by Simon Willison
*
* Environment Variables:
* - CALMHIVE_DEBUG or CALMHIVE_INTERCEPTOR_DEBUG: Enable debug logging
* - CALMHIVE_INJECTION_MODE: Control injection behavior
* - 'always': Inject on every request (legacy behavior)
* - 'smart': Skip automated/tool contexts (default)
* - 'interactive-only': Only inject for fresh user messages
* - 'never': Disable injection completely
* - CALMHIVE_MAX_INJECTIONS: Max injections per session (default: 1)
* - CALMHIVE_INJECTION_COOLDOWN: Cooldown between injections in ms (default: 300000 = 5 min)
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');
// Store original fetch
const originalFetch = global.fetch;
// Load CLAUDE.md content
const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
let claudeMdContent = null;
try {
if (fs.existsSync(claudeMdPath)) {
claudeMdContent = fs.readFileSync(claudeMdPath, 'utf8');
console.error('[Calmhive Interceptor] Loaded CLAUDE.md for injection');
} else {
console.error('[Calmhive Interceptor] Warning: CLAUDE.md not found');
}
} catch (error) {
console.error('[Calmhive Interceptor] Error loading CLAUDE.md:', error.message);
}
// Check if rule injection is enabled
const settingsPath = path.join(os.homedir(), '.claude', 'calmhive-settings.json');
let ruleInjectionEnabled = true;
const debugMode = process.env.CALMHIVE_DEBUG === '1' || process.env.CALMHIVE_INTERCEPTOR_DEBUG === '1';
// Environment variable controls for injection behavior
const injectionMode = process.env.CALMHIVE_INJECTION_MODE || 'smart'; // 'always', 'smart', 'interactive-only', 'never'
const maxInjectionsPerSession = parseInt(process.env.CALMHIVE_MAX_INJECTIONS || '1', 10);
const injectionCooldownMs = parseInt(process.env.CALMHIVE_INJECTION_COOLDOWN || '300000', 10); // 5 minutes default
// Conversation tracking for smart injection
let processedMessages = new Map(); // Track messages we've already injected into with timestamps
let recentMessages = []; // Track recent messages to detect typing patterns
let conversationSessions = new Map(); // Track conversation sessions
let totalInjections = 0;
let lastCleanupTime = Date.now();
const CLEANUP_INTERVAL = 60 * 60 * 1000; // Clean up every hour
const MESSAGE_TTL = 60 * 60 * 1000; // Keep messages for 1 hour
try {
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
ruleInjectionEnabled = settings.ruleInjection !== false;
}
} catch (error) {
// Default to enabled if settings can't be read
ruleInjectionEnabled = true;
}
if (debugMode) {
console.error('[Calmhive Debug] Debug mode enabled');
console.error('[Calmhive Debug] Rule injection enabled:', ruleInjectionEnabled);
console.error('[Calmhive Debug] CLAUDE.md content length:', claudeMdContent ? claudeMdContent.length : 0);
}
/**
* Check if a URL is a Claude API endpoint
*/
function isClaudeAPI(url) {
if (typeof url === 'string') {
return url.includes('anthropic.com') || url.includes('claude.ai');
}
if (url instanceof URL) {
return url.hostname.includes('anthropic.com') || url.hostname.includes('claude.ai');
}
return false;
}
/**
* Generate a cryptographic hash for user message content
*/
function hashUserMessage(content) {
if (!content) {return null;}
// Use SHA-256 for better uniqueness
const hash = crypto.createHash('sha256');
hash.update(content.toString());
return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
}
/**
* Clean up old entries from tracking maps
*/
function cleanupOldEntries() {
const now = Date.now();
if (now - lastCleanupTime < CLEANUP_INTERVAL) {
return; // Not time for cleanup yet
}
lastCleanupTime = now;
let cleaned = 0;
// Clean up old processed messages
for (const [hash, timestamp] of processedMessages.entries()) {
if (now - timestamp > MESSAGE_TTL) {
processedMessages.delete(hash);
cleaned++;
}
}
// Clean up old conversation sessions
for (const [sessionId, data] of conversationSessions.entries()) {
if (now - data.lastSeen > MESSAGE_TTL) {
conversationSessions.delete(sessionId);
cleaned++;
}
}
if (debugMode && cleaned > 0) {
console.error(`[Calmhive Debug] Cleaned up ${cleaned} old entries`);
}
}
/**
* Check if current message is likely typing continuation of recent message
*
* This function implements sophisticated typing detection to prevent duplicate
* rule injection during real-time typing. It analyzes recent message patterns
* to identify:
* - Text expansion (user continuing to type)
* - Text deletion (user backspacing)
* - Similar messages (user editing/revising)
*
* Uses a combination of string similarity algorithms and temporal analysis.
*/
function isTypingContinuation(currentContent) {
if (!currentContent || recentMessages.length === 0) {return false;}
const now = Date.now();
const currentText = currentContent.toString().trim();
// Check recent messages (within last 5 seconds)
// This window captures typical typing speed variations
const recentTyping = recentMessages.filter(msg => now - msg.timestamp < 5000);
for (const recent of recentTyping) {
const recentText = recent.content.toString().trim();
// PATTERN 1: Text Expansion Detection
// Current message is longer and starts with the recent message
// Example: "hello" → "hello world" (user continuing to type)
if (currentText.length > recentText.length &&
currentText.startsWith(recentText)) {
if (debugMode) {
console.error(`[Calmhive Debug] Detected typing continuation: "${recentText}" → "${currentText}"`);
}
return true;
}
// PATTERN 2: Text Deletion Detection
// Recent message is longer and starts with current message
// Example: "hello world" → "hello" (user backspacing)
if (recentText.length > currentText.length &&
recentText.startsWith(currentText)) {
if (debugMode) {
console.error(`[Calmhive Debug] Detected typing backspace: "${recentText}" → "${currentText}"`);
}
return true;
}
// PATTERN 3: High Similarity Detection
// Messages are very similar but not exact prefixes (editing/revising)
// Uses custom similarity algorithm optimized for typing patterns
const similarity = calculateSimilarity(currentText, recentText);
if (similarity > 0.8 && Math.abs(currentText.length - recentText.length) < 20) {
if (debugMode) {
console.error(`[Calmhive Debug] Detected similar message (${(similarity * 100).toFixed(1)}% similar)`);
}
return true;
}
}
return false;
}
/**
* Calculate similarity between two strings (0-1)
*
* Custom similarity algorithm optimized for typing detection.
* More efficient than full Levenshtein distance for our use case.
*
* Algorithm:
* 1. Fast path: exact match returns 1.0
* 2. Fast path: substring relationship (common in typing)
* 3. Character-order matching (preserves typing sequence)
*
* Time complexity: O(n) where n is length of longer string
* Space complexity: O(1)
*/
function calculateSimilarity(str1, str2) {
if (str1 === str2) {return 1;} // Fast path: exact match
if (!str1 || !str2) {return 0;} // Fast path: empty strings
const longer = str1.length > str2.length ? str1 : str2;
const shorter = str1.length > str2.length ? str2 : str1;
// Fast path: substring relationship
// Common when user is expanding or contracting text
if (longer.includes(shorter)) {
return shorter.length / longer.length;
}
// Character-order matching algorithm
// Counts characters that appear in the same relative order
// This preserves typing sequence better than bag-of-words approaches
let commonChars = 0;
let shortIndex = 0;
for (let i = 0; i < longer.length && shortIndex < shorter.length; i++) {
if (longer[i] === shorter[shortIndex]) {
commonChars++;
shortIndex++;
}
}
// Normalize by the length of the longer string
// This prevents short strings from having inflated similarity scores
return commonChars / Math.max(str1.length, str2.length);
}
/**
* Track a message for typing pattern detection
*/
function trackMessage(content) {
const now = Date.now();
recentMessages.push({
content: content,
timestamp: now
});
// Keep only last 10 messages and recent messages (last 10 seconds)
recentMessages = recentMessages
.filter(msg => now - msg.timestamp < 10000)
.slice(-10);
}
/**
* Generate conversation session ID from message history
*
* Creates a stable identifier for conversation sessions to prevent
* duplicate rule injection across multi-turn conversations.
*
* Session ID Strategy:
* 1. Single message: Hash the original user message
* 2. Multi-turn: Extract and hash the original first message
* 3. Already injected: Extract original content from injection
*
* This ensures the same conversation gets the same session ID
* regardless of how many times it's been processed.
*/
function getConversationSessionId(bodyData) {
if (!bodyData.messages || bodyData.messages.length === 0) {
return null;
}
// CASE 1: Single message conversations (new conversations)
if (bodyData.messages.length === 1) {
const firstMessage = bodyData.messages[0];
if (firstMessage.role === 'user') {
// Extract text content from various message formats
// Claude API supports both string and structured content
const content = typeof firstMessage.content === 'string'
? firstMessage.content
: (Array.isArray(firstMessage.content) ? firstMessage.content.map(c => {
if (typeof c === 'string') {return c;}
if (c.type === 'text' && c.text) {return c.text;}
return '';
}).join(' ') : '');
// Don't track if already injected (to avoid duplicate tracking)
// This prevents circular session creation
if (content.includes('CLAUDE.md RULES:')) {
return null;
}
return hashUserMessage(content);
}
}
// CASE 2: Multi-turn conversations (continuing conversations)
// Find the first user message to use as session anchor
const firstMessage = bodyData.messages.find(m => m.role === 'user');
if (!firstMessage) {return null;}
// Extract content using the same logic as single messages
const firstContent = typeof firstMessage.content === 'string'
? firstMessage.content
: (Array.isArray(firstMessage.content) ? firstMessage.content.map(c => {
if (typeof c === 'string') {return c;}
if (c.type === 'text' && c.text) {return c.text;}
return '';
}).join(' ') : '');
if (debugMode) {
console.error('[Calmhive Debug] First message content length:', firstContent.length);
console.error('[Calmhive Debug] First message includes CLAUDE.md:', firstContent.includes('CLAUDE.md RULES:'));
if (firstContent.includes('CLAUDE.md RULES:')) {
console.error('[Calmhive Debug] Looking for original message...');
}
}
// CASE 3: Previously injected conversations
// Extract the original message from our injection wrapper
if (firstContent.includes('CLAUDE.md RULES:')) {
// Use regex to extract the original user message
// Our injection format includes "Original message:" marker
const originalMatch = firstContent.match(/Original message:\n(.+)$/s);
if (originalMatch) {
const sessionId = hashUserMessage(originalMatch[1]);
if (debugMode) {
console.error('[Calmhive Debug] Found original message, session ID:', sessionId);
}
return sessionId;
} else if (debugMode) {
console.error('[Calmhive Debug] Could not extract original message from injected content');
}
}
// Don't track sessions for non-injected first messages
// This prevents creating sessions for conversations we haven't processed
return null;
}
/**
* Check if this conversation session has already been injected
*/
function isConversationAlreadyInjected(bodyData) {
const sessionId = getConversationSessionId(bodyData);
if (!sessionId) {
if (debugMode) {
console.error('[Calmhive Debug] No session ID found, not a tracked conversation');
}
return false;
}
const now = Date.now();
if (debugMode) {
console.error(`[Calmhive Debug] Checking session: ${sessionId}`);
console.error('[Calmhive Debug] Tracked sessions:', Array.from(conversationSessions.keys()));
}
// Check if we've injected into this conversation recently
if (conversationSessions.has(sessionId)) {
const sessionData = conversationSessions.get(sessionId);
// Check injection count limit
if (sessionData.injectionCount >= maxInjectionsPerSession) {
if (debugMode) {
console.error(`[Calmhive Debug] Session injection limit reached (${sessionData.injectionCount}/${maxInjectionsPerSession}): ${sessionId.slice(0, 20)}...`);
}
return true;
}
// Check cooldown period
if (now - sessionData.lastSeen < injectionCooldownMs) {
if (debugMode) {
const cooldownRemaining = Math.round((injectionCooldownMs - (now - sessionData.lastSeen)) / 1000);
console.error(`[Calmhive Debug] Session in cooldown period (${cooldownRemaining}s remaining): ${sessionId.slice(0, 20)}...`);
}
return true;
}
}
if (debugMode) {
console.error('[Calmhive Debug] Session not found in tracked sessions');
}
return false;
}
/**
* Mark conversation session as injected
*/
function markConversationInjected(bodyData) {
const sessionId = getConversationSessionId(bodyData);
if (sessionId) {
if (debugMode) {
console.error(`[Calmhive Debug] Marking session as injected: ${sessionId}`);
}
const existing = conversationSessions.get(sessionId) || { injectionCount: 0 };
conversationSessions.set(sessionId, {
timestamp: existing.timestamp || Date.now(),
lastSeen: Date.now(),
messageCount: bodyData.messages.length,
injectionCount: existing.injectionCount + 1
});
} else if (debugMode) {
console.error('[Calmhive Debug] Cannot mark session - no session ID found');
}
}
/**
* Analyze request type to determine if injection should occur
*/
function analyzeRequestType(bodyData) {
const analysis = {
isUserMessage: false,
isToolCall: false,
isStreaming: false,
messageCount: bodyData.messages ? bodyData.messages.length : 0,
hasTools: !!bodyData.tools,
hasRecentToolUse: false,
requestType: 'unknown'
};
if (!bodyData.messages || !Array.isArray(bodyData.messages)) {
analysis.requestType = 'non-message';
return analysis;
}
// Check for tool definitions in request
if (bodyData.tools && Array.isArray(bodyData.tools) && bodyData.tools.length > 0) {
analysis.isToolCall = true;
analysis.requestType = 'tool-enabled';
}
// Check for streaming
if (bodyData.stream === true) {
analysis.isStreaming = true;
}
// Analyze message history for tool use patterns
const assistantMessages = bodyData.messages.filter(m => m.role === 'assistant');
analysis.hasRecentToolUse = assistantMessages.some(m => {
if (Array.isArray(m.content)) {
return m.content.some(c => c.type === 'tool_use');
}
if (typeof m.content === 'string') {
return m.content.includes('tool_use') || m.content.includes('<function_calls>');
}
return false;
});
// Get the last user message
const lastUserMessage = bodyData.messages.filter(m => m.role === 'user').pop();
if (lastUserMessage) {
// Determine if this looks like a fresh user message vs tool execution context
const userContent = typeof lastUserMessage.content === 'string'
? lastUserMessage.content
: (Array.isArray(lastUserMessage.content) ? lastUserMessage.content.map(c => c.text || '').join(' ') : '');
// Fresh user message indicators: short, no tool references, direct human language
const isLikelyFreshUserMessage =
!analysis.hasRecentToolUse &&
analysis.messageCount <= 3 && // Short conversation
!userContent.includes('CLAUDE.md RULES:') && // Not already injected
userContent.length > 0 && userContent.length < 500; // Reasonable length
if (isLikelyFreshUserMessage) {
analysis.isUserMessage = true;
analysis.requestType = 'fresh-user-message';
} else if (analysis.hasRecentToolUse || analysis.messageCount > 10) {
analysis.requestType = 'tool-execution-context';
} else {
analysis.requestType = 'continued-conversation';
}
}
if (debugMode) {
console.error('[Calmhive Debug] Request analysis:', JSON.stringify(analysis, null, 2));
}
return analysis;
}
/**
* Check if this request should receive injection based on request analysis
*/
function shouldInjectForRequest(bodyData) {
// Check injection mode environment variable
if (injectionMode === 'never') {
if (debugMode) {
console.error('[Calmhive Debug] Injection disabled by CALMHIVE_INJECTION_MODE=never');
}
return false;
}
// First check if ANY user message in the conversation already has CLAUDE.md injected
const hasInjectedMessage = bodyData.messages.some(msg => {
if (msg.role !== 'user') {return false;}
const content = typeof msg.content === 'string'
? msg.content
: (Array.isArray(msg.content) ? msg.content.map(c => {
if (typeof c === 'string') {return c;}
if (c.type === 'text' && c.text) {return c.text;}
return '';
}).join(' ') : '');
return content.includes('CLAUDE.md RULES:');
});
if (hasInjectedMessage) {
if (debugMode) {
console.error('[Calmhive Debug] Conversation already has CLAUDE.md injected in a previous message');
}
return false;
}
// Get the user message content for deduplication
const lastUserMessage = bodyData.messages.filter(m => m.role === 'user').pop();
if (!lastUserMessage) {
if (debugMode) {
console.error('[Calmhive Debug] No user message found');
}
return false;
}
// Check if this is a tool result message (actual tool execution)
const isToolResult = Array.isArray(lastUserMessage.content) &&
lastUserMessage.content.some(c => c.type === 'tool_result');
if (isToolResult) {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection - tool result message');
}
return false;
}
// Enhanced detection for automated vs interactive requests
const requestAnalysis = analyzeRequestType(bodyData);
// Apply injection mode filtering
if (injectionMode === 'interactive-only') {
// Only inject for fresh user messages, not tool contexts
if (requestAnalysis.requestType !== 'fresh-user-message') {
if (debugMode) {
console.error(`[Calmhive Debug] Skipping injection - interactive-only mode, request type: ${requestAnalysis.requestType}`);
}
return false;
}
}
// Skip injection for tool execution contexts (in smart mode)
if (injectionMode === 'smart' && requestAnalysis.requestType === 'tool-execution-context') {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection - automated tool execution context');
}
return false;
}
// Skip injection if this appears to be an automated continuation (in smart mode)
// (many messages, recent tool use, no fresh human input)
if (injectionMode === 'smart' && requestAnalysis.hasRecentToolUse && requestAnalysis.messageCount > 5) {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection - automated continuation with recent tool use');
}
return false;
}
const userContent = typeof lastUserMessage.content === 'string'
? lastUserMessage.content
: (Array.isArray(lastUserMessage.content) ? lastUserMessage.content.map(c => {
if (typeof c === 'string') {return c;}
if (c.type === 'text' && c.text) {return c.text;}
return '';
}).join(' ') : '');
// Skip if message already contains CLAUDE.md (re-injection prevention)
if (userContent.includes('CLAUDE.md RULES:')) {
if (debugMode) {
console.error('[Calmhive Debug] Message already contains CLAUDE.md rules');
}
return false;
}
// Check if this conversation session has already been injected (CRITICAL FIX for conversation spam)
if (isConversationAlreadyInjected(bodyData)) {
if (debugMode) {
console.error('[Calmhive Debug] Conversation already injected, skipping');
}
trackMessage(userContent); // Still track for typing detection
return false;
}
// Check if this is typing continuation (CRITICAL FIX for typing spam)
if (isTypingContinuation(userContent)) {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection - typing continuation detected');
}
trackMessage(userContent); // Still track for future typing detection
return false;
}
// Create a simple hash for deduplication
const messageHash = hashUserMessage(userContent);
// Skip if we've already processed this exact message
if (processedMessages.has(messageHash)) {
if (debugMode) {
console.error(`[Calmhive Debug] Already processed this message: ${messageHash.slice(0, 20)}...`);
}
return false;
}
// Mark this message as processed and inject
processedMessages.set(messageHash, Date.now());
trackMessage(userContent); // Track for typing pattern detection
markConversationInjected(bodyData); // Mark conversation session as injected
if (debugMode) {
console.error('[Calmhive Debug] Injection decision: INJECT (new user message)');
}
return true;
}
/**
* Inject CLAUDE.md into a message
*/
function injectRulesIntoMessage(message) {
// Skip if already injected
if (message.includes('CLAUDE.md RULES:')) {
return message;
}
// Prepend rules to message
return `CLAUDE.md RULES:\n\n${claudeMdContent}\n\n---\n\nOriginal message:\n${message}`;
}
/**
* Modify request body to inject rules
*/
function modifyRequestBody(body) {
// Run cleanup periodically
cleanupOldEntries();
if (!body || !claudeMdContent || !ruleInjectionEnabled) {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection - body:', !!body, 'claudeMd:', !!claudeMdContent, 'enabled:', ruleInjectionEnabled);
}
return body;
}
try {
let bodyData;
const originalBody = body;
// Parse body based on type
if (typeof body === 'string') {
bodyData = JSON.parse(body);
} else if (body instanceof Uint8Array) {
bodyData = JSON.parse(new TextDecoder().decode(body));
} else {
bodyData = body;
}
if (debugMode) {
console.error('[Calmhive Debug] === REQUEST ANALYSIS ===');
console.error('[Calmhive Debug] Request has tools:', !!bodyData.tools);
console.error('[Calmhive Debug] Stream setting:', bodyData.stream);
console.error('[Calmhive Debug] Messages count:', bodyData.messages ? bodyData.messages.length : 0);
console.error('[Calmhive Debug] Model:', bodyData.model);
if (bodyData.messages && bodyData.messages.length > 0) {
const lastMessage = bodyData.messages[bodyData.messages.length - 1];
console.error('[Calmhive Debug] Last message role:', lastMessage.role);
// Check for tool use in recent assistant messages
const assistantMessages = bodyData.messages.filter(m => m.role === 'assistant');
const hasToolUse = assistantMessages.some(m =>
(Array.isArray(m.content) && m.content.some(c => c.type === 'tool_use')) ||
(typeof m.content === 'string' && m.content.includes('tool_use'))
);
console.error('[Calmhive Debug] Has recent tool use:', hasToolUse);
}
console.error('[Calmhive Debug] Full request body:', JSON.stringify(bodyData, null, 2));
console.error('[Calmhive Debug] ========================');
}
// Check if this is a message request
if (bodyData.messages && Array.isArray(bodyData.messages)) {
let modified = false;
// Use request analysis to determine if injection should occur
if (!shouldInjectForRequest(bodyData)) {
if (debugMode) {
console.error('[Calmhive Debug] Skipping injection based on request analysis');
}
} else {
// Find the last user message to inject into
for (let i = bodyData.messages.length - 1; i >= 0; i--) {
if (bodyData.messages[i].role === 'user') {
const content = bodyData.messages[i].content;
// Handle different content formats
if (typeof content === 'string') {
const newContent = injectRulesIntoMessage(content);
if (newContent !== content) {
bodyData.messages[i].content = newContent;
modified = true;
totalInjections++;
if (debugMode) {
console.error('[Calmhive Debug] Injected into string content');
console.error('[Calmhive Debug] Original length:', content.length);
console.error('[Calmhive Debug] New length:', newContent.length);
}
}
} else if (Array.isArray(content)) {
// Handle multi-part messages
for (let j = 0; j < content.length; j++) {
if (content[j].type === 'text' && content[j].text) {
const newText = injectRulesIntoMessage(content[j].text);
if (newText !== content[j].text) {
content[j].text = newText;
modified = true;
totalInjections++;
if (debugMode) {
console.error('[Calmhive Debug] Injected into multipart content');
}
}
break; // Only inject into first text part
}
}
}
break; // Only modify the last user message
}
}
}
if (modified) {
// Only log when actual injection occurs
console.error(`[Calmhive Interceptor] Rules injected (session #${totalInjections})`);
// Return modified body in same format as input
const modifiedBody = typeof originalBody === 'string' ? JSON.stringify(bodyData) :
originalBody instanceof Uint8Array ? new TextEncoder().encode(JSON.stringify(bodyData)) :
bodyData;
return modifiedBody;
}
}
if (debugMode) {
console.error('[Calmhive Debug] No modification needed - not a message request or already injected');
}
} catch (error) {
console.error('[Calmhive Interceptor] Error modifying request:', error);
if (debugMode) {
console.error('[Calmhive Debug] Request modification failed:', error.stack);
}
}
return body;
}
// Monkey-patch global fetch
if (originalFetch) {
global.fetch = async function(url, options = {}) {
// Check if this is a Claude API request
if (isClaudeAPI(url)) {
// Clone options to avoid modifying the original
const modifiedOptions = { ...options };
// Modify the request body if present
if (modifiedOptions.body) {
const modifiedBody = modifyRequestBody(modifiedOptions.body);
if (modifiedBody !== modifiedOptions.body) {
modifiedOptions.body = modifiedBody;
// Logging moved inside modifyRequestBody to only log actual injections
}
}
// Call original fetch with modified options
return originalFetch.call(this, url, modifiedOptions);
}
// Call original fetch for non-Claude requests
return originalFetch.call(this, url, options);
};
// Store reference to original on our function for testing
global.fetch._originalFetch = originalFetch;
console.error('[Calmhive Interceptor] Network interception active');
}
// Also patch Node's HTTP/HTTPS modules for comprehensive coverage
const http = require('http');
const https = require('https');
function patchModule(module) {
const originalRequest = module.request;
module.request = function(options, callback) {
// Check if this is a Claude API request
const isClaudeReq = (options.hostname &&
(options.hostname.includes('anthropic.com') ||
options.hostname.includes('claude.ai')));
if (!isClaudeReq) {
return originalRequest.apply(this, arguments);
}
// Create intercepted request
const req = originalRequest.call(this, options, callback);
const originalWrite = req.write;
const originalEnd = req.end;
const chunks = [];
req.write = function(chunk, encoding, callback) {
// Buffer chunks instead of sending them
if (chunk) {
chunks.push({ chunk, encoding });
}
// Call callback if provided, but don't actually write yet
if (callback) {callback();}
return true;
};
req.end = function(chunk, encoding, callback) {
// Add final chunk if present
if (chunk) {
chunks.push({ chunk, encoding });
}
// Reconstruct the full body
let requestBody = '';
chunks.forEach(({ chunk, encoding }) => {
if (Buffer.isBuffer(chunk)) {
requestBody += chunk.toString(encoding || 'utf8');
} else {
requestBody += chunk.toString();
}
});
// Modify the body if needed
let finalBody = requestBody;
if (requestBody && claudeMdContent && ruleInjectionEnabled) {
const modifiedBody = modifyRequestBody(requestBody);
if (modifiedBody !== requestBody) {
finalBody = modifiedBody;
// Logging is handled inside modifyRequestBody to ensure consistency
if (process.env.CALMHIVE_DEBUG) {
console.error('[Calmhive Debug] Original length:', requestBody.length);
console.error('[Calmhive Debug] Modified length:', finalBody.length);
}
}
}
// Now write all the data at once
if (finalBody) {
originalWrite.call(this, finalBody);
}
return originalEnd.call(this, callback);
};
return req;
};
}
// Patch both HTTP and HTTPS
patchModule(http);
patchModule(https);
// Try to patch axios if it's available
try {
// First check if axios exists in Claude's dependencies
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function(id) {
const module = originalRequire.apply(this, arguments);
// If this is axios being loaded, patch it
if (id === 'axios' || id.includes('axios')) {
patchAxios(module);
}
return module;
};
// Also try to patch axios if it's already loaded
try {
const axios = require('axios');
patchAxios(axios);
} catch (e) {
// Axios not directly available
}
} catch (error) {
// Module patching not available
}
function patchAxios(axios) {
if (!axios || !axios.interceptors || axios.__calmhive_patched__) {
return;
}
axios.__calmhive_patched__ = true;
// Add request interceptor
axios.interceptors.request.use(
(config) => {
if (isClaudeAPI(config.url) && claudeMdContent && ruleInjectionEnabled) {
// Modify request data
if (config.data) {
const modifiedData = modifyRequestBody(config.data);
if (modifiedData !== config.data) {
config.data = modifiedData;
// Logging is handled inside modifyRequestBody to ensure consistency
}
}
}
return config;
},
(error) => Promise.reject(error)
);
console.error('[Calmhive Interceptor] Patched axios');
}
console.error('[Calmhive Interceptor] Ready for rule injection');