import { readdir, stat, access } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';
import { platform } from 'os';
import { constants } from 'fs';
export function getClaudeProjectsPath(): string {
return join(homedir(), '.claude', 'projects');
}
export function getClaudePlansPath(): string {
return join(homedir(), '.claude', 'plans');
}
export async function findPlanFiles(): Promise<string[]> {
try {
const plansPath = getClaudePlansPath();
const entries = await readdir(plansPath);
return entries.filter((file) => file.endsWith('.md'));
} catch (error) {
console.error('Error finding plan files:', error);
return [];
}
}
export function decodeProjectPath(encodedPath: string): string {
// Claude encodes paths by replacing '/' with '-'
return encodedPath.replace(/-/g, '/');
}
export function encodeProjectPath(path: string): string {
// Encode path for Claude projects directory naming
return path.replace(/\//g, '-');
}
export async function findProjectDirectories(): Promise<string[]> {
try {
const projectsPath = getClaudeProjectsPath();
const entries = await readdir(projectsPath);
const directories = [];
for (const entry of entries) {
const fullPath = join(projectsPath, entry);
const stats = await stat(fullPath);
if (stats.isDirectory()) {
directories.push(entry);
}
}
return directories;
} catch (error) {
console.error('Error finding project directories:', error);
return [];
}
}
export async function findJsonlFiles(projectDir: string): Promise<string[]> {
try {
const projectsPath = getClaudeProjectsPath();
const fullPath = join(projectsPath, projectDir);
const entries = await readdir(fullPath);
return entries.filter((file) => file.endsWith('.jsonl'));
} catch (error) {
console.error(`Error finding JSONL files in ${projectDir}:`, error);
return [];
}
}
export function extractContentFromMessage(message: any): string {
if (typeof message.content === 'string') {
return message.content;
}
if (Array.isArray(message.content)) {
return message.content
.map((item: any) => {
if (item.type === 'text') return item.text;
if (item.type === 'tool_use') return `[Tool: ${item.name}]`;
if (item.type === 'tool_result') return `[Tool Result]`;
return '';
})
.join(' ')
.trim();
}
return '';
}
export function calculateRelevanceScore(message: any, query: string, projectPath?: string): number {
let score = 0;
const content = extractContentFromMessage(message.message || {});
const lowerQuery = query.toLowerCase();
const lowerContent = content.toLowerCase();
// Identify core technical terms - specific tech names that MUST match for relevance
// These are often the most important words in a query (frameworks, tools, protocols)
const coreTechPatterns =
/^(webpack|docker|react|vue|angular|node|npm|yarn|typescript|python|rust|go|java|kubernetes|aws|gcp|azure|postgres|mysql|redis|mongodb|graphql|rest|grpc|oauth|jwt|git|github|gitlab|jenkins|nginx|apache|eslint|prettier|babel|vite|rollup|esbuild|jest|mocha|cypress|playwright|nextjs|nuxt|svelte|tailwind|sass|less|vitest|pnpm|turborepo|prisma|drizzle|sequelize|sqlite|leveldb|indexeddb)$/i;
// Generic terms that should NOT become core terms even if 5+ chars
const genericTerms = new Set([
// Action words
'config',
'configuration',
'setup',
'install',
'build',
'deploy',
'test',
'run',
'start',
'create',
'update',
'fix',
'add',
'remove',
'change',
'optimize',
'optimization',
'improve',
'use',
'using',
'with',
'for',
'the',
'and',
'make',
'write',
'read',
'delete',
'check',
// Testing-related words (appear in many contexts: A/B testing, user testing, etc.)
'testing',
'tests',
'mocks',
'mocking',
'mock',
'stubs',
'stubbing',
'specs',
'coverage',
// Design/architecture terms (appear across many domains)
'design',
'designs',
'designing',
'responsive',
'architecture',
'pattern',
'patterns',
// Performance/optimization terms
'caching',
'cache',
'rendering',
'render',
'bundle',
'bundling',
'performance',
// Process/strategy terms
'strategy',
'strategies',
'approach',
'implementation',
'solution',
'solutions',
'feature',
'features',
'system',
'systems',
'process',
'processing',
'handler',
'handling',
'manager',
'management',
// Common nouns that appear in many contexts
'files',
'file',
'folder',
'directory',
'path',
'code',
'data',
'error',
'errors',
'function',
'functions',
'class',
'classes',
'method',
'methods',
'variable',
'variables',
'component',
'components',
'module',
'modules',
'package',
'packages',
'library',
'libraries',
// Format/display words
'format',
'formatting',
'style',
'styles',
'layout',
'display',
'show',
'hide',
'visible',
'rules',
'rule',
'options',
'option',
'settings',
'setting',
'params',
'parameters',
// Generic technical words
'server',
'client',
'request',
'response',
'async',
'await',
'promise',
'callback',
'import',
'export',
'require',
'include',
'define',
'declare',
'return',
'output',
'input',
// Database/schema generic terms (appear in many contexts)
'database',
'schema',
'schemas',
'models',
'model',
'table',
'tables',
'query',
'queries',
'migration',
'migrations',
'index',
'indexes',
'field',
'fields',
'column',
'columns',
// Deployment/infra generic terms
'deployment',
'container',
'containers',
'service',
'services',
'cluster',
'clusters',
'instance',
'instances',
'environment',
'environments',
'manifest',
'resource',
'resources',
// Common programming terms
'interface',
'interfaces',
'types',
'typing',
'object',
'objects',
'array',
'arrays',
'string',
'strings',
'number',
'numbers',
'boolean',
'value',
'values',
'property',
'properties',
]);
const queryWords = lowerQuery.split(/\s+/).filter((w) => w.length > 2);
// STRICT core terms: Only tech names from coreTechPatterns are "must-match"
// These are specific frameworks/tools that MUST appear for relevance
const strictCoreTerms = queryWords.filter((w) => coreTechPatterns.test(w));
// Supporting terms: Other 5+ char words boost score but don't require match
const supportingTerms = queryWords.filter(
(w) => !coreTechPatterns.test(w) && !genericTerms.has(w) && w.length >= 5
);
// Check if STRICT core terms match (tech names like vue, rust, kubernetes)
let strictCoreMatches = 0;
for (const term of strictCoreTerms) {
if (lowerContent.includes(term)) {
strictCoreMatches++;
score += 10; // High weight for tech name matches
}
}
// If query has strict tech terms but NONE match, reject completely
if (strictCoreTerms.length > 0 && strictCoreMatches === 0) {
return 0; // No relevance if specific tech terms don't match
}
// Supporting terms boost score but don't reject if missing
for (const term of supportingTerms) {
if (lowerContent.includes(term)) {
score += 3; // Moderate boost for supporting term matches
}
}
// Individual word scoring for remaining words
let wordMatchCount = strictCoreMatches;
for (const word of queryWords) {
if (
!strictCoreTerms.includes(word) &&
!supportingTerms.includes(word) &&
lowerContent.includes(word)
) {
wordMatchCount++;
score += 2; // +2 per matching word
}
}
// Bonus for exact phrase match (all words in order)
if (lowerContent.includes(lowerQuery)) {
score += 5; // Bonus for exact phrase, but not required
}
// Bonus for matching majority of query words
if (queryWords.length > 0 && wordMatchCount >= Math.ceil(queryWords.length * 0.6)) {
score += 4; // 60%+ word match bonus
}
// Tool usage bonus
if (message.type === 'tool_use' || message.type === 'tool_result') {
score += 5;
}
// File reference bonus
if (content.includes('src/') || content.includes('.ts') || content.includes('.js')) {
score += 3;
}
// Project path matching bonus
if (projectPath && message.cwd && message.cwd.includes(projectPath)) {
score += 5;
}
return score;
}
export function formatTimestamp(timestamp: string): string {
return new Date(timestamp).toISOString();
}
export function getTimeRangeFilter(timeframe?: string): (timestamp: string) => boolean {
if (!timeframe) return () => true;
const now = new Date();
const cutoff = new Date();
switch (timeframe.toLowerCase()) {
case 'today':
cutoff.setHours(0, 0, 0, 0);
break;
case 'yesterday':
cutoff.setDate(now.getDate() - 1);
cutoff.setHours(0, 0, 0, 0);
break;
case 'week':
case 'last-week':
cutoff.setDate(now.getDate() - 7);
break;
case 'month':
case 'last-month':
cutoff.setMonth(now.getMonth() - 1);
break;
default:
return () => true;
}
return (timestamp: string) => {
const messageDate = new Date(timestamp);
return messageDate >= cutoff;
};
}
export function getClaudeDesktopPath(): string | null {
switch (platform()) {
case 'darwin':
return join(homedir(), 'Library/Application Support/Claude/');
case 'win32':
return join(process.env.APPDATA || '', 'Claude/');
case 'linux':
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'Claude/');
default:
return null;
}
}
export async function detectClaudeDesktop(): Promise<boolean> {
try {
const desktopPath = getClaudeDesktopPath();
if (!desktopPath) return false;
const configPath = join(desktopPath, 'claude_desktop_config.json');
await access(configPath, constants.F_OK);
return true;
} catch {
return false;
}
}
export async function getClaudeDesktopStoragePath(): Promise<string | null> {
const desktopPath = getClaudeDesktopPath();
if (!desktopPath) return null;
const storagePath = join(desktopPath, 'Local Storage');
try {
await access(storagePath, constants.F_OK);
return storagePath;
} catch {
return null;
}
}
export async function getClaudeDesktopIndexedDBPath(): Promise<string | null> {
const desktopPath = getClaudeDesktopPath();
if (!desktopPath) return null;
const indexedDBPath = join(desktopPath, 'IndexedDB');
try {
await access(indexedDBPath, constants.F_OK);
return indexedDBPath;
} catch {
return null;
}
}