🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

claude-code-spy

Package Overview
Dependencies
Maintainers
1
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

claude-code-spy - npm Package Compare versions

Comparing version
2.5.0
to
2.5.1
+155
lib/core.js
const fs = require('fs');
const path = require('path');
const colors = require('./colors');
const { formatTimestamp, formatProjectPath, truncateText } = require('./utils');
// Filter entries based on config criteria
function filterEntries(entries, config) {
let filtered = entries;
// Filter by project
if (config.project) {
filtered = filtered.filter(e =>
e.project && e.project.toLowerCase().includes(config.project.toLowerCase())
);
}
// Filter by search term
if (config.search) {
filtered = filtered.filter(e =>
e.display && e.display.toLowerCase().includes(config.search.toLowerCase())
);
}
// Filter by date range
if (config.after) {
filtered = filtered.filter(e => new Date(e.timestamp) > config.after);
}
if (config.before) {
filtered = filtered.filter(e => new Date(e.timestamp) < config.before);
}
// Sort order
if (!config.reverse) {
filtered = filtered.reverse(); // Show newest first by default
}
// Limit
if (config.limit) {
filtered = filtered.slice(0, config.limit);
}
return filtered;
}
// Find conversation for an entry
async function findConversationForEntry(entry, { readConversation, findSessionFile }) {
const sessionInfo = await findSessionFile(entry.project, entry.timestamp);
if (!sessionInfo) return null;
const conversation = await readConversation(sessionInfo.filePath);
// Find the user message that matches our entry
const entryTime = new Date(entry.timestamp);
let closestMatch = null;
let minDiff = Infinity;
for (let i = 0; i < conversation.length; i++) {
const conv = conversation[i];
if (conv.type === 'user' && conv.message?.content) {
const convTime = new Date(conv.timestamp);
const diff = Math.abs(entryTime - convTime);
const userContent = typeof conv.message.content === 'string'
? conv.message.content
: conv.message.content[0]?.text || '';
const entryText = entry.display.substring(0, 100);
const convText = userContent.substring(0, 100);
if (diff < minDiff && (diff < 60000 || convText.includes(entryText.substring(0, 50)) || entryText.includes(convText.substring(0, 50)))) {
minDiff = diff;
closestMatch = i;
}
}
}
if (closestMatch !== null) {
const result = [conversation[closestMatch]];
if (closestMatch + 1 < conversation.length && conversation[closestMatch + 1].type === 'assistant') {
result.push(conversation[closestMatch + 1]);
}
return result;
}
return null;
}
// Display a single entry
async function displayEntry(entry, index, total, config, helpers) {
const time = formatTimestamp(entry.timestamp);
const project = formatProjectPath(entry.project);
if (!config.outputOnly) {
console.log(`\n${colors.bright}${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
console.log(`${colors.bright}Entry #${total - index}${colors.reset} ${colors.gray}(${time.relative})${colors.reset}`);
console.log(`${colors.yellow}Time:${colors.reset} ${time.formatted}`);
console.log(`${colors.magenta}Project:${colors.reset} ${project}`);
console.log(`${colors.green}Input:${colors.reset}`);
const lines = entry.display.split('\n');
lines.forEach(line => {
console.log(` ${colors.white}${line}${colors.reset}`);
});
}
// If full conversation requested, try to get Claude's response
if (config.showFull || config.outputOnly) {
const conversation = await findConversationForEntry(entry, helpers);
if (conversation && conversation.length > 1) {
const assistant = conversation[1];
if (!config.outputOnly) {
console.log(`\n${colors.blue}Claude's Response:${colors.reset}`);
} else {
console.log(`\n${colors.bright}${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
console.log(`${colors.gray}Response to: "${truncateText(entry.display, 80)}"${colors.reset}`);
}
const response = helpers.formatAssistantMessage(assistant.message);
const responseLines = response.split('\n');
const maxLines = 50;
const displayLines = responseLines.slice(0, maxLines);
displayLines.forEach(line => {
console.log(` ${colors.gray}${line}${colors.reset}`);
});
if (responseLines.length > maxLines) {
console.log(` ${colors.dim}... (${responseLines.length - maxLines} more lines)${colors.reset}`);
}
if (assistant.message?.usage) {
console.log(` ${colors.dim}[Tokens: ${assistant.message.usage.input_tokens} in, ${assistant.message.usage.output_tokens} out]${colors.reset}`);
}
} else if (config.showFull) {
console.log(`\n${colors.gray}(Claude's response not found or not available)${colors.reset}`);
}
}
// If there's pasted content, show it
if (!config.outputOnly && entry.pastedContents && Object.keys(entry.pastedContents).length > 0) {
console.log(`${colors.blue}Attachments:${colors.reset}`);
Object.entries(entry.pastedContents).forEach(([key, value]) => {
console.log(` ${colors.gray}• ${key}: ${truncateText(value.toString())}${colors.reset}`);
});
}
}
module.exports = {
filterEntries,
findConversationForEntry,
displayEntry
};
const readline = require('readline');
const { execFileSync } = require('child_process');
const colors = require('./colors');
// Setup keyboard input handling for watch mode
function setupKeyboardInput(options) {
const {
onExit,
onAnalyzer,
onSecurity,
onDependencies,
onBash,
onFiles,
onHelp,
onTime,
readConversation,
sessionFile,
sessionStartTime,
watchStartIndex
} = options;
if (!process.stdin.isTTY) {
return;
}
readline.emitKeypressEvents(process.stdin);
if (process.stdin.setRawMode) {
process.stdin.setRawMode(true);
}
process.stdin.on('keypress', async (str, key) => {
// Exit
if (key && (key.name === 'q' || (key.ctrl && key.name === 'c'))) {
onExit();
return;
}
// Security analysis (s key)
if (key && key.name === 's') {
const conversation = await readConversation(sessionFile);
await onSecurity(conversation, watchStartIndex);
return;
}
// Archer analysis (a key)
if (key && key.name === 'a') {
const conversation = await readConversation(sessionFile);
await onAnalyzer(conversation, watchStartIndex);
return;
}
// Dependencies (d key)
if (key && key.name === 'd') {
const conversation = await readConversation(sessionFile);
await onDependencies(conversation, sessionStartTime, watchStartIndex);
return;
}
// Bash history (b key)
if (key && key.name === 'b') {
const conversation = await readConversation(sessionFile);
await onBash(conversation, watchStartIndex);
return;
}
// File views (f key - cycles through tracker, summary, sizes)
if (key && key.name === 'f') {
const conversation = await readConversation(sessionFile);
await onFiles(conversation, watchStartIndex);
return;
}
// Help (h key)
if (key && key.name === 'h') {
await onHelp();
return;
}
// Time statistics (t key)
if (key && key.name === 't') {
const conversation = await readConversation(sessionFile);
await onTime(conversation, watchStartIndex, sessionStartTime);
return;
}
});
// Handle Ctrl+C globally
process.on('SIGINT', () => {
onExit();
});
}
// Clean up keyboard input
function cleanupKeyboardInput() {
if (process.stdin.setRawMode) {
process.stdin.setRawMode(false);
}
}
// Get keyboard shortcuts help text
function getKeyboardShortcuts() {
return `${colors.bright}${colors.cyan}Keyboard Shortcuts:${colors.reset}
${colors.yellow}a${colors.reset} - Run Archer (AI conversation analysis)
${colors.yellow}s${colors.reset} - Run Security Analysis
${colors.yellow}d${colors.reset} - Show dependency graph & recent commits
${colors.yellow}b${colors.reset} - Show bash command history
${colors.yellow}f${colors.reset} - Cycle through file views (tracker → summary → sizes)
${colors.yellow}t${colors.reset} - Show time statistics
${colors.yellow}h${colors.reset} - Show this help
${colors.yellow}q${colors.reset} - Quit`;
}
module.exports = {
setupKeyboardInput,
cleanupKeyboardInput,
getKeyboardShortcuts
};
/**
* Models Module - OpenAI API and Analysis Functions
*
* This module handles all LLM interactions and analysis operations.
* Functions: Archer analysis, Security analysis, Dependency analysis
*
* Note: Analysis functions (runArcherAnalysisInline, runSecurityAnalysisInline)
* are currently exported from main CLI file but will be extracted here
* as part of ongoing refactoring. This module provides the interface
* and helper functions for AI-powered analysis.
*/
const colors = require('./colors');
const { appendToMarkdownLog } = require('./logger');
/**
* Extract interactions from conversation for analysis
* @param {Array} conversation - Conversation entries from session
* @param {number} startIndex - Start index for filtering
* @param {number} limit - Max interactions to return
* @returns {Array} Array of interaction objects
*/
function extractInteractions(conversation, startIndex = 0, limit = 10) {
const interactions = [];
let currentInteraction = null;
for (let i = startIndex; i < conversation.length; i++) {
const entry = conversation[i];
if (entry.type === 'user') {
const content = entry.message?.content || '';
// Skip tool results
if (Array.isArray(content) && content.length > 0 && content[0].type === 'tool_result') {
continue;
}
if (currentInteraction) {
interactions.push(currentInteraction);
}
currentInteraction = {
user: typeof content === 'string' ? content : content[0]?.text || '',
assistant: '',
tools: []
};
} else if (entry.type === 'assistant' && currentInteraction) {
const content = entry.message?.content || [];
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'text') {
currentInteraction.assistant += item.text + '\n';
} else if (item.type === 'tool_use') {
currentInteraction.tools.push({
name: item.name,
input: item.input
});
}
});
} else if (typeof content === 'string') {
currentInteraction.assistant = content;
}
}
}
if (currentInteraction) {
interactions.push(currentInteraction);
}
return interactions.slice(-limit);
}
/**
* Format interactions for LLM input
* @param {Array} interactions - Interaction objects
* @returns {string} Formatted conversation text
*/
function formatConversationForLLM(interactions) {
return interactions.map((interaction, idx) => {
let text = `\n## Interaction ${idx + 1}\n\n`;
text += `**User Input:**\n${interaction.user}\n\n`;
if (interaction.tools.length > 0) {
text += `**Tools Used:**\n`;
interaction.tools.forEach(tool => {
text += `- ${tool.name}: ${JSON.stringify(tool.input, null, 2)}\n`;
});
text += `\n`;
}
text += `**Claude's Response:**\n${interaction.assistant}\n`;
return text;
}).join('\n---\n');
}
/**
* Display analysis results with proper formatting
* @param {string} analysis - Analysis text from LLM
* @param {object} options - Display options
*/
function displayAnalysis(analysis, options = {}) {
const {
title = '🏹 ANALYSIS',
titleColor = colors.orange,
highlightTerms = [],
logFile = null
} = options;
console.log(`${colors.bright}${titleColor}${title}${colors.reset}\n`);
if (logFile) appendToMarkdownLog(logFile, `## ${title}\n`);
const paragraphs = analysis.split('\n\n');
paragraphs.forEach((para, idx) => {
const lines = para.split('\n');
lines.forEach(line => {
if (line.trim()) {
let displayLine = line;
let lineColor = titleColor;
// Apply highlight colors for specific terms
highlightTerms.forEach(({ term, color }) => {
if (line.includes(term)) {
lineColor = color;
}
});
console.log(`${lineColor}${displayLine}${colors.reset}`);
if (logFile) appendToMarkdownLog(logFile, line);
}
});
if (idx < paragraphs.length - 1) {
console.log('');
if (logFile) appendToMarkdownLog(logFile, '');
}
});
}
/**
* Call OpenAI API with retry and error handling
* @param {object} options - API options
* @returns {Promise<string>} Analysis result
*/
async function callOpenAIAPI(options) {
const {
systemPrompt,
userMessage,
model = 'gpt-4o-mini',
apiKey = process.env.OPENAI_API_KEY,
timeout = 30000
} = options;
if (!apiKey) {
throw new Error('OPENAI_API_KEY environment variable not set');
}
const controller = new AbortController();
const timeoutHandle = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
signal: controller.signal,
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage }
],
temperature: 0.3,
max_tokens: 1200
})
});
clearTimeout(timeoutHandle);
if (!response.ok) {
const error = new Error(`OpenAI API Error: ${response.status}`);
error.status = response.status;
throw error;
}
const data = await response.json();
return {
analysis: data.choices[0].message.content,
tokens: data.usage.total_tokens,
model
};
} catch (err) {
clearTimeout(timeoutHandle);
if (err.name === 'AbortError') {
throw new Error('API request timeout');
}
throw err;
}
}
module.exports = {
extractInteractions,
formatConversationForLLM,
displayAnalysis,
callOpenAIAPI
};
/**
* Watch Mode Module - Real-time conversation monitoring
*
* This module handles watch mode functionality including:
* - Polling for new messages
* - Idle-time tracking for auto-summaries
* - Token counting for analysis triggers
* - Auto-triggering Archer analysis
*/
const colors = require('./colors');
const { appendToMarkdownLog } = require('./logger');
/**
* Calculate tool usage statistics from conversation
* @param {Array} conversation - Conversation entries
* @returns {object} {totalTools, toolList, toolTypes}
*/
function getToolStats(conversation) {
const toolCounts = {};
const toolTypes = new Set();
for (const entry of conversation) {
if (entry.type === 'assistant') {
const content = entry.message?.content || [];
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'tool_use') {
const toolName = item.name || 'unknown';
toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
toolTypes.add(toolName);
}
});
}
}
}
const totalTools = Object.values(toolCounts).reduce((a, b) => a + b, 0);
const toolList = Object.entries(toolCounts)
.map(([name, count]) => `${name}(${count})`)
.join(', ');
return { totalTools, toolList, toolTypes: Array.from(toolTypes) };
}
/**
* Generate initial watch mode stats display
* @param {Array} conversation - Initial conversation
* @param {number} startIndex - Starting index for new content
* @returns {string} Formatted stats display
*/
function generateStatsDisplay(conversation, startIndex = 0) {
const recentMessages = conversation.slice(startIndex);
const userMessages = recentMessages.filter(e => e.type === 'user');
const assistantMessages = recentMessages.filter(e => e.type === 'assistant');
// Count tokens (rough estimate)
let totalTokens = 0;
assistantMessages.forEach(entry => {
const content = entry.message?.content || [];
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'text') {
totalTokens += Math.ceil(item.text.length / 4);
}
});
}
});
const { totalTools, toolList } = getToolStats(recentMessages);
return `${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${colors.bright}${colors.cyan}Watch Mode Active${colors.reset}
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${colors.dim}Messages:${colors.reset} ${userMessages.length} user, ${assistantMessages.length} assistant
${colors.dim}Tokens:${colors.reset} ~${totalTokens}
${colors.dim}Tools used:${colors.reset} ${totalTools > 0 ? toolList : 'none'}
${colors.dim}Press 'h' for help, 'q' to quit${colors.reset}`;
}
/**
* Count total tokens in conversation (rough estimate: 4 chars per token)
* @param {Array} conversation - Conversation entries
* @returns {number} Estimated total tokens
*/
function countConversationTokens(conversation) {
let totalTokens = 0;
for (const entry of conversation) {
if (entry.type === 'assistant') {
const content = entry.message?.content || [];
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'text') {
totalTokens += Math.ceil(item.text.length / 4);
}
});
}
}
}
return totalTokens;
}
/**
* Handle polling loop for watch mode
* @param {object} options - Polling options
*/
async function startPolling(options) {
const {
sessionFile,
readConversation,
displayMessage,
runArcherAnalysis,
logFile,
onExit
} = options;
let lastMessageCount = 0;
let lastMessageTime = Date.now();
let analysisPending = false;
let lastAnalysisTokenCount = 0;
let isExiting = false;
const apiKey = process.env.OPENAI_API_KEY;
// Initialize tracking
try {
const initialConversation = await readConversation(sessionFile);
lastMessageCount = initialConversation.length;
lastAnalysisTokenCount = countConversationTokens(initialConversation);
} catch (e) {
// File might not exist yet
}
// Poll for changes every 500ms
const pollInterval = setInterval(async () => {
if (isExiting) {
clearInterval(pollInterval);
return;
}
try {
const currentConversation = await readConversation(sessionFile);
if (currentConversation.length > lastMessageCount) {
// New messages arrived
const newMessages = currentConversation.slice(lastMessageCount);
console.log(`${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
console.log(`${colors.green}New message(s) received!${colors.reset}\n`);
appendToMarkdownLog(logFile, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
appendToMarkdownLog(logFile, 'New message(s) received!');
appendToMarkdownLog(logFile, '');
for (const entry of newMessages) {
displayMessage(entry, false);
// Log entry to markdown
if (entry.type === 'user') {
appendToMarkdownLog(logFile, `**User:** ${entry.input || ''}`);
} else if (entry.type === 'assistant') {
const content = entry.message?.content || [];
let text = '';
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'text') {
text += item.text || '';
}
});
}
appendToMarkdownLog(logFile, `**Assistant:** ${text.substring(0, 500)}${text.length > 500 ? '...' : ''}`);
}
appendToMarkdownLog(logFile, '');
}
lastMessageCount = currentConversation.length;
lastMessageTime = Date.now();
analysisPending = false;
} else {
// Check for idle timeout (15 seconds) with minimum token count
const idleTime = Date.now() - lastMessageTime;
const totalTokens = countConversationTokens(currentConversation);
const newTokens = totalTokens - lastAnalysisTokenCount;
const idleSeconds = Math.floor(idleTime / 1000);
// Auto-trigger analysis after 15 seconds idle and 1000+ new tokens
if (idleSeconds >= 15 && newTokens >= 1000 && !analysisPending) {
if (!apiKey) {
console.log(`\n${colors.yellow}⚠ Auto-summary needs OPENAI_API_KEY${colors.reset}\n`);
appendToMarkdownLog(logFile, 'Auto-summary skipped: OPENAI_API_KEY not set');
} else {
analysisPending = true;
lastAnalysisTokenCount = totalTokens;
console.log(`\n${colors.dim}Idle for ${idleSeconds}s and ${newTokens} tokens...${colors.reset}\n`);
console.log(`${colors.orange}🏹 Archer is contemplating...${colors.reset}\n`);
appendToMarkdownLog(logFile, `Idle for ${idleSeconds}s and ${newTokens} tokens`);
appendToMarkdownLog(logFile, 'Archer is contemplating...');
try {
await runArcherAnalysis();
} catch (err) {
console.error(`${colors.red}Auto-summary error: ${err.message}${colors.reset}`);
appendToMarkdownLog(logFile, `Auto-summary error: ${err.message}`);
}
console.log(`${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
console.log(`${colors.dim}Resuming watch...${colors.reset}\n`);
appendToMarkdownLog(logFile, '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
appendToMarkdownLog(logFile, 'Resuming watch...');
analysisPending = false;
}
}
}
} catch (e) {
// File might be in the middle of being written, ignore
}
}, 500);
// Return cleanup function
return () => {
isExiting = true;
clearInterval(pollInterval);
};
}
module.exports = {
getToolStats,
generateStatsDisplay,
countConversationTokens,
startPolling
};
+5
-1

@@ -6,3 +6,7 @@ // Main library exports - ties together all modules

logger: require('./logger'),
config: require('./config')
config: require('./config'),
core: require('./core'),
input: require('./input'),
models: require('./models'),
watch: require('./watch')
};
+1
-1
{
"name": "claude-code-spy",
"version": "2.5.0",
"version": "2.5.1",
"description": "Spy on your Claude Code conversations - Real-time monitoring with AI-powered summaries and full tool visibility",

@@ -5,0 +5,0 @@ "main": "claude-history-cli.js",