UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

322 lines (321 loc) 9.28 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; class ClaudeAutoDetect { projectRoot; constructor(projectRoot = process.cwd()) { this.projectRoot = projectRoot; } /** * Main detection logic */ detect() { const result = { shouldUseWorktree: false, reasons: [], confidence: "low", suggestions: [] }; const checks = [ this.checkUncommittedChanges(), this.checkActiveInstances(), this.checkBranchProtection(), this.checkFileConflicts(), this.checkResourceUsage(), this.checkTaskComplexity() ]; let score = 0; for (const check of checks) { if (check.detected) { score += check.weight; result.reasons.push(check.reason); if (check.suggestion) { result.suggestions.push(check.suggestion); } } } if (score >= 7) { result.shouldUseWorktree = true; result.confidence = "high"; } else if (score >= 4) { result.shouldUseWorktree = true; result.confidence = "medium"; } else if (score >= 2) { result.shouldUseWorktree = false; result.confidence = "medium"; result.suggestions.push( "Consider using --worktree if making significant changes" ); } return result; } /** * Check for uncommitted changes */ checkUncommittedChanges() { try { const status = execSync("git status --porcelain", { cwd: this.projectRoot, encoding: "utf8" }); if (status.trim().length > 0) { const lines = status.trim().split("\n").length; return { detected: true, weight: lines > 10 ? 3 : 2, reason: `${lines} uncommitted changes in working directory`, suggestion: "Commit or stash changes before proceeding, or use worktree" }; } } catch { } return { detected: false, weight: 0, reason: "" }; } /** * Check for other active Claude instances */ checkActiveInstances() { const lockDir = path.join(this.projectRoot, ".claude-worktree-locks"); if (fs.existsSync(lockDir)) { try { const locks = fs.readdirSync(lockDir).filter((f) => f.endsWith(".lock")); const activeLocks = locks.filter((lockFile) => { const lockPath = path.join(lockDir, lockFile); const stats = fs.statSync(lockPath); const ageHours = (Date.now() - stats.mtimeMs) / (1e3 * 60 * 60); return ageHours < 24; }); if (activeLocks.length > 0) { return { detected: true, weight: activeLocks.length >= 2 ? 4 : 3, reason: `${activeLocks.length} other Claude instance(s) active`, suggestion: "Use worktree to avoid conflicts with other instances" }; } } catch { } } return { detected: false, weight: 0, reason: "" }; } /** * Check if on protected branch */ checkBranchProtection() { try { const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: this.projectRoot, encoding: "utf8" }).trim(); const protectedBranches = [ "main", "master", "production", "staging", "develop" ]; if (protectedBranches.includes(branch)) { return { detected: true, weight: 4, reason: `Working on protected branch: ${branch}`, suggestion: "Create a feature branch or use worktree" }; } } catch { } return { detected: false, weight: 0, reason: "" }; } /** * Check for potential file conflicts */ checkFileConflicts() { const ideFiles = [ ".vscode/settings.json", ".idea/workspace.xml", ".sublime-workspace" ]; let openIDEs = 0; for (const ideFile of ideFiles) { const filePath = path.join(this.projectRoot, ideFile); if (fs.existsSync(filePath)) { try { const stats = fs.statSync(filePath); const ageMinutes = (Date.now() - stats.mtimeMs) / (1e3 * 60); if (ageMinutes < 5) { openIDEs++; } } catch { } } } if (openIDEs > 1) { return { detected: true, weight: 2, reason: "Multiple IDEs/editors detected", suggestion: "Use worktree to avoid file lock conflicts" }; } return { detected: false, weight: 0, reason: "" }; } /** * Check system resource usage */ checkResourceUsage() { try { const worktrees = execSync("git worktree list", { cwd: this.projectRoot, encoding: "utf8" }).trim().split("\n").length; if (worktrees > 3) { return { detected: true, weight: 1, reason: `${worktrees} worktrees already exist`, suggestion: "Consider cleaning up old worktrees" }; } } catch { } return { detected: false, weight: 0, reason: "" }; } /** * Detect task complexity from user input or context */ checkTaskComplexity() { try { const recentCommit = execSync("git log -1 --pretty=%B", { cwd: this.projectRoot, encoding: "utf8" }).toLowerCase(); const complexIndicators = [ "refactor", "breaking change", "major", "experiment", "prototype", "redesign", "migration" ]; for (const indicator of complexIndicators) { if (recentCommit.includes(indicator)) { return { detected: true, weight: 2, reason: "Recent complex changes detected", suggestion: "Use worktree for experimental or major changes" }; } } } catch { } return { detected: false, weight: 0, reason: "" }; } /** * Get current environment status */ getStatus() { const status = { git: false, worktrees: 0, instances: 0, branch: "unknown", uncommittedChanges: 0 }; try { execSync("git rev-parse --git-dir", { cwd: this.projectRoot, stdio: "ignore" }); status.git = true; status.branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: this.projectRoot, encoding: "utf8" }).trim(); const worktreeList = execSync("git worktree list", { cwd: this.projectRoot, encoding: "utf8" }); status.worktrees = worktreeList.trim().split("\n").length; const changes = execSync("git status --porcelain", { cwd: this.projectRoot, encoding: "utf8" }); status.uncommittedChanges = changes.trim() ? changes.trim().split("\n").length : 0; } catch { } const lockDir = path.join(this.projectRoot, ".claude-worktree-locks"); if (fs.existsSync(lockDir)) { try { const locks = fs.readdirSync(lockDir).filter((f) => f.endsWith(".lock")); status.instances = locks.filter((lockFile) => { const lockPath = path.join(lockDir, lockFile); const stats = fs.statSync(lockPath); const ageHours = (Date.now() - stats.mtimeMs) / (1e3 * 60 * 60); return ageHours < 24; }).length; } catch { } } return status; } /** * Smart recommendation based on context */ recommend(taskDescription) { const detection = this.detect(); const status = this.getStatus(); let taskType = "general"; if (taskDescription) { const lower = taskDescription.toLowerCase(); if (lower.includes("ui") || lower.includes("frontend") || lower.includes("component")) { taskType = "frontend"; } else if (lower.includes("api") || lower.includes("backend") || lower.includes("database")) { taskType = "backend"; } else if (lower.includes("test") || lower.includes("debug") || lower.includes("fix")) { taskType = "debugging"; } else if (lower.includes("refactor") || lower.includes("clean")) { taskType = "refactoring"; } } let mode = "normal"; const flags = []; let reason = ""; if (detection.shouldUseWorktree) { mode = "worktree"; flags.push("--worktree"); reason = detection.reasons[0] || "Isolation recommended"; } switch (taskType) { case "frontend": if (mode === "worktree") { mode = "chrome"; flags.push("--chrome"); reason += "; Chrome automation for UI work"; } break; case "backend": if (mode === "worktree") { mode = "sandbox"; flags.push("--sandbox"); reason += "; Sandboxed for API development"; } break; case "refactoring": if (mode === "normal" && status.uncommittedChanges === 0) { reason = "Clean working directory, safe to proceed"; } break; } return { mode, reason, flags }; } } export { ClaudeAutoDetect }; //# sourceMappingURL=auto-detect.js.map