cleanup_agent_artifacts
Remove temporary files and markers created by AI coding agents from project directories using scan, clean, or archive operations.
Instructions
Detect, classify, and clean up artifacts generated by AI coding agents (e.g., TODO.md, PLAN.md, agent markers, temporary files). Supports scan, clean, and archive operations with configurable patterns.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the project directory to scan | |
| operation | Yes | Operation: scan (detect only), clean (remove), or archive (move to .agent-archive/) | |
| dryRun | No | Show what would be changed without making changes | |
| interactive | No | Prompt for confirmation (not supported in MCP, treated as dryRun) | |
| autoDeleteThreshold | No | Confidence threshold for automatic deletion (0-1) | |
| includeGitIgnored | No | Include artifacts that are already in .gitignore | |
| customPatterns | No | Custom patterns to detect in addition to defaults |
Implementation Reference
- Zod schema for input validation (inputSchema) and TypeScript type definitions for input and output.const inputSchema = z.object({ path: z.string().describe("Path to the project directory to scan"), operation: z .enum(["scan", "clean", "archive"]) .describe( "Operation to perform: scan (detect only), clean (remove), or archive (move to .agent-archive/)", ), dryRun: z .boolean() .optional() .default(false) .describe("Show what would be changed without making changes"), interactive: z .boolean() .optional() .default(false) .describe( "Prompt for confirmation before each action (not supported in MCP, treated as dryRun)", ), autoDeleteThreshold: z .number() .min(0) .max(1) .optional() .default(0.9) .describe("Confidence threshold for automatic deletion (0-1)"), includeGitIgnored: z .boolean() .optional() .default(false) .describe("Include artifacts that are already in .gitignore"), customPatterns: z .object({ files: z.array(z.string()).optional(), directories: z.array(z.string()).optional(), inlineMarkers: z.array(z.string()).optional(), }) .optional() .describe("Custom patterns to detect in addition to defaults"), }); export type CleanupAgentArtifactsInput = z.infer<typeof inputSchema>; export interface CleanupAgentArtifactsOutput { operation: string; result: ArtifactScanResult; actionsPerformed?: { deleted: string[]; archived: string[]; skipped: string[]; }; }
- Core handler function that executes the tool: validates input, scans directory for agent artifacts, performs scan/clean/archive operations based on parameters, handles dry-run, generates recommendations and next steps, and returns MCP-formatted response.export async function cleanupAgentArtifacts( args: unknown, ): Promise<{ content: any[]; isError?: boolean }> { const startTime = Date.now(); try { const input = inputSchema.parse(args); // Validate path exists try { const stats = await fs.stat(input.path); if (!stats.isDirectory()) { return formatMCPResponse({ success: false, error: { code: "INVALID_PATH", message: `Path is not a directory: ${input.path}`, resolution: "Provide a valid directory path", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }); } } catch (error) { return formatMCPResponse({ success: false, error: { code: "PATH_NOT_FOUND", message: `Path does not exist: ${input.path}`, resolution: "Provide a valid directory path", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }); } // Build configuration const config: Partial<ArtifactCleanupConfig> = { autoDeleteThreshold: input.autoDeleteThreshold, preserveGitIgnored: !input.includeGitIgnored, }; // Merge custom patterns if provided if (input.customPatterns) { config.patterns = { files: [ ...DEFAULT_CONFIG.patterns.files, ...(input.customPatterns.files || []), ], directories: [ ...DEFAULT_CONFIG.patterns.directories, ...(input.customPatterns.directories || []), ], inlineMarkers: [ ...DEFAULT_CONFIG.patterns.inlineMarkers, ...(input.customPatterns.inlineMarkers || []), ], blockPatterns: DEFAULT_CONFIG.patterns.blockPatterns, }; } // Create detector and scan const detector = new ArtifactDetector(input.path, config); const scanResult = await detector.scan(); // Handle operations let actionsPerformed: | { deleted: string[]; archived: string[]; skipped: string[] } | undefined; // Interactive mode is treated as dry-run in MCP context const isDryRun = input.dryRun || input.interactive; if (input.operation === "clean" && !isDryRun) { actionsPerformed = await performCleanup( input.path, scanResult.artifacts, input.autoDeleteThreshold, ); } else if (input.operation === "archive" && !isDryRun) { actionsPerformed = await performArchive( input.path, scanResult.artifacts, input.autoDeleteThreshold, ); } // Build output const output: CleanupAgentArtifactsOutput = { operation: isDryRun ? `${input.operation} (dry-run)` : input.operation, result: scanResult, actionsPerformed, }; // Build recommendations const recommendations = []; if (input.operation === "scan" && scanResult.artifacts.length > 0) { const deleteCount = scanResult.summary.byRecommendation["delete"] || 0; const archiveCount = scanResult.summary.byRecommendation["archive"] || 0; const reviewCount = scanResult.summary.byRecommendation["review"] || 0; if (deleteCount > 0) { recommendations.push({ type: "info" as const, title: "High-confidence artifacts found", description: `Found ${deleteCount} artifacts recommended for deletion. Run with operation='clean' to remove them.`, action: "cleanup_agent_artifacts with operation='clean'", }); } if (archiveCount > 0) { recommendations.push({ type: "info" as const, title: "Archivable artifacts found", description: `Found ${archiveCount} artifacts recommended for archiving. Run with operation='archive' to preserve them.`, action: "cleanup_agent_artifacts with operation='archive'", }); } if (reviewCount > 0) { recommendations.push({ type: "warning" as const, title: "Manual review recommended", description: `Found ${reviewCount} artifacts that require manual review before action.`, }); } } if (isDryRun && input.operation !== "scan") { recommendations.push({ type: "info" as const, title: "Dry-run mode", description: "No changes were made. Remove dryRun=true to apply changes.", }); } // Build next steps const nextSteps = []; if (input.operation === "scan" && scanResult.artifacts.length > 0) { nextSteps.push({ action: "Review detected artifacts", description: "Examine the artifacts list to understand what was detected", priority: "high" as const, }); nextSteps.push({ action: "Run cleanup with dry-run", toolRequired: "cleanup_agent_artifacts", description: "Test cleanup operation before making changes", priority: "medium" as const, }); } if ( actionsPerformed && (actionsPerformed.deleted.length > 0 || actionsPerformed.archived.length > 0) ) { nextSteps.push({ action: "Review changes and commit", description: "Verify the cleanup results and commit to version control", priority: "high" as const, }); } return formatMCPResponse( { success: true, data: output, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, recommendations: recommendations.length > 0 ? recommendations : undefined, nextSteps: nextSteps.length > 0 ? nextSteps : undefined, }, { fullResponse: true }, ); } catch (error) { if (error instanceof z.ZodError) { return formatMCPResponse({ success: false, error: { code: "INVALID_INPUT", message: "Invalid input parameters", details: error.errors, resolution: "Check the input schema and provide valid parameters", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }); } return formatMCPResponse({ success: false, error: { code: "TOOL_ERROR", message: error instanceof Error ? error.message : "Unknown error", resolution: "Check the error message and try again", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }); } }
- Helper function that performs the 'clean' operation by deleting high-confidence agent artifacts marked for deletion.async function performCleanup( projectPath: string, artifacts: AgentArtifact[], threshold: number, ): Promise<{ deleted: string[]; archived: string[]; skipped: string[] }> { const deleted: string[] = []; const skipped: string[] = []; for (const artifact of artifacts) { // Only delete high-confidence artifacts with delete recommendation if ( artifact.confidence >= threshold && artifact.recommendation === "delete" ) { const fullPath = path.join(projectPath, artifact.path.split(":")[0]); try { if (artifact.type === "file") { await fs.unlink(fullPath); deleted.push(artifact.path); } else if (artifact.type === "directory") { await fs.rm(fullPath, { recursive: true, force: true }); deleted.push(artifact.path); } else { // Inline and block comments require manual editing skipped.push(artifact.path); } } catch (error) { console.error(`Error deleting ${artifact.path}: ${error}`); skipped.push(artifact.path); } } else { skipped.push(artifact.path); } } return { deleted, archived: [], skipped }; }
- Helper function that performs the 'archive' operation by moving qualifying artifacts to a timestamped .agent-archive/ directory.async function performArchive( projectPath: string, artifacts: AgentArtifact[], threshold: number, ): Promise<{ deleted: string[]; archived: string[]; skipped: string[] }> { const archived: string[] = []; const skipped: string[] = []; // Create archive directory const archiveDir = path.join(projectPath, ".agent-archive"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const archivePath = path.join(archiveDir, timestamp); try { await fs.mkdir(archivePath, { recursive: true }); } catch (error) { console.error(`Error creating archive directory: ${error}`); return { deleted: [], archived: [], skipped: artifacts.map((a) => a.path) }; } for (const artifact of artifacts) { // Archive medium to high confidence artifacts if ( artifact.confidence >= threshold * 0.7 && (artifact.recommendation === "archive" || artifact.recommendation === "delete") ) { const sourcePath = path.join(projectPath, artifact.path.split(":")[0]); try { if (artifact.type === "file" || artifact.type === "directory") { const destPath = path.join(archivePath, artifact.path); const destDir = path.dirname(destPath); // Create destination directory await fs.mkdir(destDir, { recursive: true }); // Move file or directory if (artifact.type === "file") { await fs.copyFile(sourcePath, destPath); await fs.unlink(sourcePath); } else { await fs.cp(sourcePath, destPath, { recursive: true }); await fs.rm(sourcePath, { recursive: true, force: true }); } archived.push(artifact.path); } else { // Inline and block comments require manual editing skipped.push(artifact.path); } } catch (error) { console.error(`Error archiving ${artifact.path}: ${error}`); skipped.push(artifact.path); } } else { skipped.push(artifact.path); } } return { deleted: [], archived, skipped }; }
- src/utils/artifact-detector.ts:1-406 (helper)Core detection utility imported and used for scanning agent artifacts (ArtifactDetector class, types, config)./** * Agent Artifact Detection System * * Detects, classifies, and provides recommendations for artifacts * generated by AI coding agents during workflows. */ import { promises as fs } from "fs"; import path from "path"; import { globby } from "globby"; export interface AgentArtifact { path: string; type: | "file" | "directory" | "inline-comment" | "block-comment" | "code-block"; category: "planning" | "debug" | "temporary" | "state" | "documentation"; confidence: number; // 0-1 how sure we are this is agent-generated recommendation: "delete" | "review" | "keep" | "archive"; context?: string; // surrounding content for review detectedBy: string; // which pattern matched } export interface ArtifactScanResult { scannedFiles: number; artifacts: AgentArtifact[]; summary: { totalArtifacts: number; byCategory: Record<string, number>; byRecommendation: Record<string, number>; }; } export interface ArtifactCleanupConfig { // Detection settings patterns: { files: string[]; // glob patterns for artifact files directories: string[]; // agent state directories inlineMarkers: string[]; // comment markers to detect blockPatterns: RegExp[]; // multi-line patterns }; // Behavior settings autoDeleteThreshold: number; // confidence threshold for auto-delete preserveGitIgnored: boolean; // skip .gitignored artifacts archiveBeforeDelete: boolean; // safety backup // Exclusions excludePaths: string[]; excludePatterns: string[]; } /** * Default configuration for artifact detection */ export const DEFAULT_CONFIG: ArtifactCleanupConfig = { patterns: { files: [ "TODO.md", "TODOS.md", "PLAN.md", "PLANNING.md", "NOTES.md", "SCRATCH.md", "AGENT-*.md", "*.agent.md", ], directories: [ ".claude", ".cursor", ".aider", ".copilot", ".codeium", ".agent-workspace", ], inlineMarkers: [ "// @agent-temp", "// TODO(agent):", "# AGENT-NOTE:", "<!-- agent:ephemeral -->", "// FIXME(claude):", "// FIXME(cursor):", "// FIXME(copilot):", "// TODO(claude):", "// TODO(cursor):", "// TODO(copilot):", ], blockPatterns: [ /\/\*\s*AGENT[-_](?:START|BEGIN)[\s\S]*?AGENT[-_](?:END|FINISH)\s*\*\//gi, /<!--\s*agent:ephemeral\s*-->[\s\S]*?<!--\s*\/agent:ephemeral\s*-->/gi, /\/\/\s*@agent-temp-start[\s\S]*?\/\/\s*@agent-temp-end/gi, ], }, autoDeleteThreshold: 0.9, preserveGitIgnored: true, archiveBeforeDelete: true, excludePaths: ["node_modules", ".git", "dist", "build", ".documcp"], excludePatterns: ["*.lock", "package-lock.json", "yarn.lock"], }; /** * Main Artifact Detector class */ export class ArtifactDetector { private config: ArtifactCleanupConfig; private projectPath: string; constructor(projectPath: string, config?: Partial<ArtifactCleanupConfig>) { this.projectPath = projectPath; this.config = { ...DEFAULT_CONFIG, ...config, patterns: { ...DEFAULT_CONFIG.patterns, ...(config?.patterns || {}), }, }; } /** * Scan for agent artifacts in the project */ async scan(): Promise<ArtifactScanResult> { const artifacts: AgentArtifact[] = []; let scannedFiles = 0; // Detect file-based artifacts const fileArtifacts = await this.detectFileArtifacts(); artifacts.push(...fileArtifacts); // Detect directory artifacts const dirArtifacts = await this.detectDirectoryArtifacts(); artifacts.push(...dirArtifacts); // Detect inline and block artifacts in files const inlineArtifacts = await this.detectInlineArtifacts(); artifacts.push(...inlineArtifacts.artifacts); scannedFiles = inlineArtifacts.scannedFiles; // Generate summary const summary = this.generateSummary(artifacts); return { scannedFiles, artifacts, summary, }; } /** * Detect file-based artifacts (e.g., TODO.md, PLAN.md) */ private async detectFileArtifacts(): Promise<AgentArtifact[]> { const artifacts: AgentArtifact[] = []; // Build glob patterns with exclusions const patterns = this.config.patterns.files.map((pattern) => path.join(this.projectPath, "**", pattern), ); try { const files = await globby(patterns, { ignore: this.config.excludePaths.map((p) => path.join(this.projectPath, p, "**"), ), absolute: true, onlyFiles: true, }); for (const filePath of files) { const relativePath = path.relative(this.projectPath, filePath); const fileName = path.basename(filePath); // Determine category and confidence based on file name const { category, confidence, detectedBy } = this.categorizeFile(fileName); artifacts.push({ path: relativePath, type: "file", category, confidence, recommendation: this.getRecommendation(confidence, category), detectedBy, }); } } catch (error) { // Silently handle globby errors (e.g., permission denied) console.error(`Error scanning files: ${error}`); } return artifacts; } /** * Detect directory-based artifacts (e.g., .claude/, .cursor/) */ private async detectDirectoryArtifacts(): Promise<AgentArtifact[]> { const artifacts: AgentArtifact[] = []; for (const dirName of this.config.patterns.directories) { const dirPath = path.join(this.projectPath, dirName); try { const stats = await fs.stat(dirPath); if (stats.isDirectory()) { artifacts.push({ path: dirName, type: "directory", category: "state", confidence: 0.95, recommendation: "archive", detectedBy: `Directory pattern: ${dirName}`, }); } } catch { // Directory doesn't exist, skip } } return artifacts; } /** * Detect inline comments and block artifacts in code files */ private async detectInlineArtifacts(): Promise<{ artifacts: AgentArtifact[]; scannedFiles: number; }> { const artifacts: AgentArtifact[] = []; let scannedFiles = 0; // Scan common code file types const patterns = [ "**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx", "**/*.py", "**/*.rb", "**/*.go", "**/*.rs", "**/*.java", "**/*.md", "**/*.html", "**/*.css", "**/*.scss", ].map((p) => path.join(this.projectPath, p)); try { const files = await globby(patterns, { ignore: this.config.excludePaths.map((p) => path.join(this.projectPath, p, "**"), ), absolute: true, onlyFiles: true, }); for (const filePath of files) { scannedFiles++; const content = await fs.readFile(filePath, "utf-8"); const relativePath = path.relative(this.projectPath, filePath); // Check for inline markers const inlineArtifacts = this.detectInlineMarkers(content, relativePath); artifacts.push(...inlineArtifacts); // Check for block patterns const blockArtifacts = this.detectBlockPatterns(content, relativePath); artifacts.push(...blockArtifacts); } } catch (error) { console.error(`Error scanning inline artifacts: ${error}`); } return { artifacts, scannedFiles }; } /** * Detect inline comment markers */ private detectInlineMarkers( content: string, filePath: string, ): AgentArtifact[] { const artifacts: AgentArtifact[] = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const marker of this.config.patterns.inlineMarkers) { if (line.includes(marker)) { // Get context (2 lines before and after) const contextStart = Math.max(0, i - 2); const contextEnd = Math.min(lines.length, i + 3); const context = lines.slice(contextStart, contextEnd).join("\n"); artifacts.push({ path: `${filePath}:${i + 1}`, type: "inline-comment", category: this.categorizeMarker(marker), confidence: 0.85, recommendation: "review", context, detectedBy: `Inline marker: ${marker}`, }); } } } return artifacts; } /** * Detect block patterns */ private detectBlockPatterns( content: string, filePath: string, ): AgentArtifact[] { const artifacts: AgentArtifact[] = []; for (const pattern of this.config.patterns.blockPatterns) { const matches = content.matchAll(pattern); for (const match of matches) { if (match[0]) { // Get first 200 chars as context const context = match[0].substring(0, 200); artifacts.push({ path: filePath, type: "block-comment", category: "temporary", confidence: 0.9, recommendation: "delete", context, detectedBy: `Block pattern: ${pattern.source.substring(0, 50)}...`, }); } } } return artifacts; } /** * Categorize a file based on its name */ private categorizeFile(fileName: string): { category: AgentArtifact["category"]; confidence: number; detectedBy: string; } { const upperName = fileName.toUpperCase(); if ( upperName.includes("TODO") || upperName.includes("PLAN") || upperName.includes("SCRATCH") ) { return { category: "planning", confidence: 0.95, detectedBy: `File name pattern: ${fileName}`, }; } if (upperName.includes("NOTES") || upperName.includes("AGENT")) { return { category: "documentation", confidence: 0.9, detectedBy: `File name pattern: ${fileName}`, }; } return { category: "temporary", confidence: 0.8, detectedBy: `File name pattern: ${fileName}`, }; } /** * Categorize a marker */ private categorizeMarker(marker: string): AgentArtifact["category"] { if (marker.includes("TODO") || marker.includes("FIXME")) { return "planning"; } if (marker.includes("temp") || marker.includes("ephemeral")) { return "temporary"; } if (marker.includes("NOTE")) { return "documentation"; } return "debug"; }