UNPKG

ccexp

Version:

CLI tool for exploring and managing Claude Code settings and slash commands

1,463 lines (1,452 loc) 120 kB
#!/usr/bin/env node import { __toESM, require_usingCtx } from "./usingCtx-BLgT6AjQ.js"; import { existsSync, readFileSync } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { program } from "commander"; import { Box, Static, Text, render, useInput, useStdout } from "ink"; import { z } from "zod/v4"; import { Badge, ConfirmInput, Spinner, StatusMessage, ThemeProvider, defaultTheme, extendTheme } from "@inkjs/ui"; import React, { Component, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isError } from "es-toolkit/predicate"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { groupBy, uniq } from "es-toolkit/array"; import { filter, truncate, values } from "es-toolkit/compat"; import { merge } from "es-toolkit/object"; import { open, readFile, readdir, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { P, match } from "ts-pattern"; import { fdir } from "fdir"; import matter from "gray-matter"; import clipboardy from "clipboardy"; import open$1 from "open"; import openEditor from "open-editor"; import { clamp } from "es-toolkit/math"; import BigText from "ink-big-text"; import Gradient from "ink-gradient"; import { marked } from "marked"; import { markedTerminal } from "marked-terminal"; //#region src/styles/theme.ts const theme = { selection: { backgroundColor: "cyan", color: "black" }, fileTypes: { projectMemory: "#FF8A65", projectMemoryLocal: "#FFAB91", userMemory: "#FF8A65", projectSettings: "#4DD0E1", projectSettingsLocal: "#80DEEA", userSettings: "#4DD0E1", projectCommand: "#66BB6A", personalCommand: "#66BB6A", projectSubagent: "#C47FD5", userSubagent: "#C47FD5", unknown: "gray" }, status: { error: "red", success: "green", warning: "yellow", info: "cyan" }, ui: { focus: "white", appTitle: "blue", spinner: "yellow", sectionTitle: "cyan" } }; //#endregion //#region src/components/ErrorBoundary.tsx var ErrorBoundary = class extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { const errorObject = isError(error) ? error : new Error(String(error || "Unknown error")); return { hasError: true, error: errorObject }; } componentDidCatch(error, errorInfo) { console.error("Error caught by boundary:", error, errorInfo); } render() { if (this.state.hasError) { if (this.props.fallback) return this.props.fallback; return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [ /* @__PURE__ */ jsx(Text, { color: theme.status.error, bold: true, children: "Something went wrong" }), /* @__PURE__ */ jsx(Text, { color: theme.status.error, children: this.state.error?.message || "An unexpected error occurred" }), /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Please try again or check the console for more details" }) ] }); } return this.props.children; } }; //#endregion //#region src/_consts.ts const CLAUDE_FILE_PATTERNS = { CLAUDE_MD: "**/CLAUDE.md", CLAUDE_LOCAL_MD: "**/CLAUDE.local.md", GLOBAL_CLAUDE_MD: join(homedir(), ".claude", "CLAUDE.md"), PROJECT_SLASH_COMMANDS: "**/.claude/commands/**/*.md", USER_SLASH_COMMANDS: join(homedir(), ".claude", "commands", "**", "*.md") }; const FILE_SIZE_LIMITS = { MAX_CLAUDE_MD_SIZE: 1024 * 1024, MAX_SLASH_COMMAND_SIZE: 512 * 1024, MAX_SUBAGENT_SIZE: 1024 * 100 }; if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; describe("CLAUDE_FILE_PATTERNS", () => { test("should contain expected patterns", () => { expect(CLAUDE_FILE_PATTERNS.CLAUDE_MD).toBe("**/CLAUDE.md"); expect(CLAUDE_FILE_PATTERNS.CLAUDE_LOCAL_MD).toBe("**/CLAUDE.local.md"); expect(CLAUDE_FILE_PATTERNS.PROJECT_SLASH_COMMANDS).toBe("**/.claude/commands/**/*.md"); }); test("should include home directory paths", () => { expect(CLAUDE_FILE_PATTERNS.GLOBAL_CLAUDE_MD).toContain(".claude"); expect(CLAUDE_FILE_PATTERNS.USER_SLASH_COMMANDS).toContain(".claude/commands"); }); }); describe("FILE_SIZE_LIMITS", () => { test("should have reasonable size limits", () => { expect(FILE_SIZE_LIMITS.MAX_CLAUDE_MD_SIZE).toBe(1024 * 1024); expect(FILE_SIZE_LIMITS.MAX_SLASH_COMMAND_SIZE).toBe(512 * 1024); }); }); } //#endregion //#region src/_types.ts const createClaudeFilePath = (path) => { if (path.length === 0) throw new Error("Path must not be empty"); return path; }; if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; describe("createClaudeFilePath", () => { test("should create branded ClaudeFilePath for valid paths", () => { const validPaths = [ "/test/CLAUDE.md", "~/CLAUDE.md", "./src/file.md", "file.md" ]; for (const path of validPaths) { const claudePath = createClaudeFilePath(path); expect(claudePath).toBe(path); expect(typeof claudePath).toBe("string"); } }); test("should throw for invalid paths", () => { expect(() => createClaudeFilePath("")).toThrow(); }); }); } //#endregion //#region src/_utils.ts var import_usingCtx$3 = __toESM(require_usingCtx(), 1); const HOME_DIR = homedir(); const parseSlashCommandName = (fileName) => { return fileName.replace(/\.md$/, "").replace(/\//g, ":"); }; const normalizeFilePath = (filePath) => { const normalized = filePath.startsWith("~") ? filePath.replace("~", HOME_DIR) : filePath; try { return createClaudeFilePath(normalized); } catch { throw new Error(`Invalid file path: ${filePath}`); } }; const getFileScope = (filePath) => { return filePath.includes(HOME_DIR) ? "user" : "project"; }; const detectClaudeFileType = (filePath) => { const fileName = basename(filePath); const dirPath = dirname(filePath); return match([fileName, dirPath]).with(["CLAUDE.md", P.when((dir) => dir === join(HOME_DIR, ".claude"))], () => "user-memory").with(["CLAUDE.md", P._], () => "project-memory").with(["CLAUDE.local.md", P._], () => "project-memory-local").with([P.when((name) => name.endsWith(".md")), P.when((dir) => dir.includes(join(HOME_DIR, ".claude", "commands")))], () => "personal-command").with([P.when((name) => name.endsWith(".md")), P.when((dir) => dir.includes(".claude/commands"))], () => "project-command").with([P.when((name) => name.endsWith(".md")), P.when((dir) => dir.includes(join(HOME_DIR, ".claude", "agents")))], () => "user-subagent").with([P.when((name) => name.endsWith(".md")), P.when((dir) => dir.includes(".claude/agents"))], () => "project-subagent").with(["settings.json", P.when((dir) => dir === join(HOME_DIR, ".claude"))], () => "user-settings").with(["settings.json", P.when((dir) => dir.endsWith("/.claude") || dir.includes("/.claude/"))], () => "project-settings").with(["settings.local.json", P.when((dir) => dir.endsWith("/.claude") || dir.includes("/.claude/"))], () => "project-settings-local").otherwise(() => "unknown"); }; const validateClaudeMdContent = (content) => { return content.length >= 0; }; const extractTagsFromContent = (content) => { const tagPattern = /#(\w+)/g; const matches = content.match(tagPattern); return matches ? matches.map((tag) => tag.slice(1)) : []; }; const extractCommandsFromContent = (content) => { const commandPattern = /\/(\w+)(?:\s+(.+?))?$/gm; const commands = []; let match$1 = commandPattern.exec(content); while (match$1 !== null) { const [, name, description] = match$1; if (!name) { match$1 = commandPattern.exec(content); continue; } commands.push({ name, description: description?.trim(), hasArguments: Boolean(description?.includes("<") || description?.includes("[")) }); match$1 = commandPattern.exec(content); } return commands; }; const isBinaryFile = async (filePath) => { try { const { readFile: readFile$1 } = await import("node:fs/promises"); const buffer = await readFile$1(filePath); const sampleSize = Math.min(1024, buffer.length); const sample = buffer.subarray(0, sampleSize); return sample.includes(0); } catch { return false; } }; if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; const { createClaudeProjectFixture, testWithFixture } = await import("./test-fixture-helpers-jC6qse8O.js"); describe("parseSlashCommandName", () => { test("should convert file path to command name", () => { expect(parseSlashCommandName("deploy.md")).toBe("deploy"); expect(parseSlashCommandName("frontend/component.md")).toBe("frontend:component"); }); test("should handle nested paths correctly", () => { expect(parseSlashCommandName("git/commit.md")).toBe("git:commit"); expect(parseSlashCommandName("project/test/unit.md")).toBe("project:test:unit"); }); }); describe("validateClaudeMdContent", () => { test("should validate valid CLAUDE.md content", () => { expect(validateClaudeMdContent("# Project Info\n## Setup")).toBe(true); expect(validateClaudeMdContent("## Build Commands")).toBe(true); }); test("should accept any reasonable content", () => { expect(validateClaudeMdContent("Just plain text")).toBe(true); expect(validateClaudeMdContent("")).toBe(true); expect(validateClaudeMdContent("- bullet point\n- another")).toBe(true); }); test("should accept any content size", () => { const largeContent = "x".repeat(1e6); expect(validateClaudeMdContent(largeContent)).toBe(true); }); }); describe("detectClaudeFileType", () => { test("should detect CLAUDE.md files", () => { expect(detectClaudeFileType("/project/CLAUDE.md")).toBe("project-memory"); }); test("should detect CLAUDE.local.md files", () => { expect(detectClaudeFileType("/project/CLAUDE.local.md")).toBe("project-memory-local"); }); test("should detect global CLAUDE.md files", () => { const globalPath = join(HOME_DIR, ".claude", "CLAUDE.md"); expect(detectClaudeFileType(globalPath)).toBe("user-memory"); }); test("should detect project slash command files", () => { expect(detectClaudeFileType("/project/.claude/commands/deploy.md")).toBe("project-command"); expect(detectClaudeFileType("/workspace/.claude/commands/test.md")).toBe("project-command"); }); test("should detect personal slash command files", () => { const personalCommandPath = join(HOME_DIR, ".claude", "commands", "personal.md"); expect(detectClaudeFileType(personalCommandPath)).toBe("personal-command"); const nestedPersonalPath = join(HOME_DIR, ".claude", "commands", "git", "commit.md"); expect(detectClaudeFileType(nestedPersonalPath)).toBe("personal-command"); }); test.each([ ["/project/.claude/settings.json", "project-settings"], ["/workspace/.claude/settings.json", "project-settings"], ["/project/.claude/settings.local.json", "project-settings-local"], ["/workspace/.claude/settings.local.json", "project-settings-local"] ])("should detect %s as %s", (path, expectedType) => { expect(detectClaudeFileType(path)).toBe(expectedType); }); test("should detect user settings.json", () => { const userSettingsPath = join(HOME_DIR, ".claude", "settings.json"); expect(detectClaudeFileType(userSettingsPath)).toBe("user-settings"); }); test("should detect project subagent files", () => { expect(detectClaudeFileType("/project/.claude/agents/test-agent.md")).toBe("project-subagent"); expect(detectClaudeFileType("/workspace/.claude/agents/helper.md")).toBe("project-subagent"); }); test("should detect user subagent files", () => { const userAgentPath = join(HOME_DIR, ".claude", "agents", "personal-agent.md"); expect(detectClaudeFileType(userAgentPath)).toBe("user-subagent"); }); test("should not detect settings files outside .claude", () => { expect(detectClaudeFileType("/project/settings.json")).toBe("unknown"); expect(detectClaudeFileType("/project/settings.local.json")).toBe("unknown"); }); }); describe("extractTagsFromContent", () => { test("should extract hashtags from content", () => { const content = "This is #typescript and #nextjs project"; expect(extractTagsFromContent(content)).toEqual(["typescript", "nextjs"]); }); test("should return empty array for no tags", () => { expect(extractTagsFromContent("No tags here")).toEqual([]); }); }); describe("extractCommandsFromContent", () => { test("should extract slash commands", () => { const content = "/deploy <environment>\n/test --watch"; const commands = extractCommandsFromContent(content); expect(commands).toHaveLength(2); expect(commands[0]?.name).toBe("deploy"); expect(commands[0]?.hasArguments).toBe(true); }); }); describe("getFileScope", () => { test("should detect user scope for home directory files", () => { expect(getFileScope(`${HOME_DIR}/.claude/CLAUDE.md`)).toBe("user"); }); test("should detect project scope for non-home files", () => { expect(getFileScope("/project/CLAUDE.md")).toBe("project"); }); }); describe("normalizeFilePath", () => { test("should expand ~ to home directory", () => { expect(normalizeFilePath("~/test.md")).toBe(`${HOME_DIR}/test.md`); expect(normalizeFilePath("~/.claude/CLAUDE.md")).toBe(`${HOME_DIR}/.claude/CLAUDE.md`); }); test("should handle absolute paths unchanged", () => { expect(normalizeFilePath("/absolute/path/file.md")).toBe("/absolute/path/file.md"); expect(normalizeFilePath("/Users/test/CLAUDE.md")).toBe("/Users/test/CLAUDE.md"); }); test("should handle relative paths unchanged", () => { expect(normalizeFilePath("./relative/path.md")).toBe("./relative/path.md"); expect(normalizeFilePath("../parent/file.md")).toBe("../parent/file.md"); expect(normalizeFilePath("src/file.md")).toBe("src/file.md"); }); test("should handle paths without ~ unchanged", () => { expect(normalizeFilePath("simple.md")).toBe("simple.md"); expect(normalizeFilePath("folder/file.md")).toBe("folder/file.md"); }); test("should throw error for invalid file paths", () => { expect(() => normalizeFilePath("")).toThrow(); }); }); describe("isBinaryFile", () => { test("should detect text files as non-binary", async () => { await testWithFixture({ "test.txt": "Hello world\nThis is a text file", "README.md": "# Project\n\nThis is markdown", "config.json": JSON.stringify({ key: "value" }, null, 2) }, async (f) => { const textResult = await isBinaryFile(f.getPath("test.txt")); expect(textResult).toBe(false); const mdResult = await isBinaryFile(f.getPath("README.md")); expect(mdResult).toBe(false); const jsonResult = await isBinaryFile(f.getPath("config.json")); expect(jsonResult).toBe(false); }); }); test("should detect binary files with null bytes", async () => { try { var _usingCtx = (0, import_usingCtx$3.default)(); const { createFixture } = await import("./dist-D99sJLU7.js"); const { writeFile } = await import("node:fs/promises"); const fixture = _usingCtx.a(await createFixture({ "image.png": "", "binary.dat": "" })); const pngData = Buffer.from([ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0 ]); const binData = Buffer.from([ 0, 1, 2, 0, 4 ]); await writeFile(fixture.getPath("image.png"), pngData); await writeFile(fixture.getPath("binary.dat"), binData); const pngResult = await isBinaryFile(fixture.getPath("image.png")); expect(pngResult).toBe(true); const datResult = await isBinaryFile(fixture.getPath("binary.dat")); expect(datResult).toBe(true); } catch (_) { _usingCtx.e = _; } finally { await _usingCtx.d(); } }); test("should handle non-existent files gracefully", async () => { const result = await isBinaryFile("/non/existent/file.txt"); expect(result).toBe(false); }); test("should handle permission errors gracefully", async () => { await testWithFixture({ "protected.txt": "Protected content" }, async (f) => { const { chmod } = await import("node:fs/promises"); const filePath = f.getPath("protected.txt"); await chmod(filePath, 0); try { const result = await isBinaryFile(filePath); expect(result).toBe(false); } finally { await chmod(filePath, 420).catch(() => {}); } }); }); }); describe("validateClaudeMdContent with real files", () => { test("should validate actual CLAUDE.md files", async () => { try { var _usingCtx3 = (0, import_usingCtx$3.default)(); const fixture = _usingCtx3.a(await createClaudeProjectFixture({ projectName: "validate-test" })); const { readFile: readFile$1 } = await import("node:fs/promises"); const content = await readFile$1(fixture.getPath("validate-test/CLAUDE.md"), "utf-8"); expect(validateClaudeMdContent(content)).toBe(true); } catch (_) { _usingCtx3.e = _; } finally { await _usingCtx3.d(); } }); }); describe("extractCommandsFromContent with real files", () => { test("should extract commands from slash command files", async () => { await testWithFixture({ ".claude": { commands: { "deploy.md": "# Deploy Command\n\n/deploy <environment>\n\nDeploys to specified environment", "test.md": "/test [--watch] [--coverage]\n\nRuns tests with optional flags", "lint.md": "/lint\n\nRuns linting checks" } } }, async (f) => { const { readFile: readFile$1 } = await import("node:fs/promises"); const deployContent = await readFile$1(f.getPath(".claude/commands/deploy.md"), "utf-8"); const deployCommands = extractCommandsFromContent(deployContent); expect(deployCommands).toHaveLength(1); expect(deployCommands[0]?.name).toBe("deploy"); expect(deployCommands[0]?.hasArguments).toBe(true); const testContent = await readFile$1(f.getPath(".claude/commands/test.md"), "utf-8"); const testCommands = extractCommandsFromContent(testContent); expect(testCommands).toHaveLength(1); expect(testCommands[0]?.name).toBe("test"); expect(testCommands[0]?.hasArguments).toBe(true); const lintContent = await readFile$1(f.getPath(".claude/commands/lint.md"), "utf-8"); const lintCommands = extractCommandsFromContent(lintContent); expect(lintCommands).toHaveLength(1); expect(lintCommands[0]?.name).toBe("lint"); expect(lintCommands[0]?.hasArguments).toBe(false); }); }); }); } //#endregion //#region src/base-file-scanner.ts var BaseFileScanner = class { async processFile(filePath) { try { if (!existsSync(filePath)) return null; const stats = await stat(filePath); if (stats.size > this.maxFileSize) { console.warn(`${this.fileType} file too large, skipping: ${filePath}`); return null; } const content = await readFile(filePath, "utf-8"); return await this.parseContent(filePath, content, stats); } catch (error) { console.warn(`Failed to process ${this.fileType} file ${filePath}:`, error); return null; } } }; if (import.meta.vitest != null) { const { describe, test, expect, vi } = import.meta.vitest; class TestScanner extends BaseFileScanner { maxFileSize = 1024; fileType = "test"; async parseContent(_filePath, content, _stats) { if (content.trim() === "") return null; return { data: content }; } } describe("BaseFileScanner", () => { test("returns null for non-existent files", async () => { const scanner$3 = new TestScanner(); const result = await scanner$3.processFile("/non/existent/file.txt"); expect(result).toBeNull(); }); test("logs warning for files exceeding size limit", async () => { const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const scanner$3 = new TestScanner(); await scanner$3.processFile("/large/file.txt"); consoleWarnSpy.mockRestore(); }); test("handles errors gracefully", async () => { const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const scanner$3 = new TestScanner(); await scanner$3.processFile("/invalid/path"); consoleWarnSpy.mockRestore(); }); }); } //#endregion //#region src/scan-exclusions.ts /** * Comprehensive exclusion patterns for secure and performant file scanning */ const SECURITY_EXCLUSIONS = [ ".ssh", ".gnupg", ".gpg", ".pki", ".aws", ".azure", ".gcp", ".kube", ".docker", "credentials", "secrets", ".mozilla", ".chrome", ".chromium", "Library/Application Support/Google/Chrome", "Library/Application Support/Firefox", "Library/Keychains", ".password-store", ".pass", ".certificates", "cert", "certs", ".openvpn", ".wireguard" ]; const DEVELOPMENT_EXCLUSIONS = [ "node_modules", "vendor", "bower_components", "jspm_packages", ".pnpm", ".yarn", "dist", "build", "out", "target", ".next", ".nuxt", ".vuepress", ".docusaurus", ".cache", ".tmp", "tmp", "temp", ".git", ".svn", ".hg", ".bzr", ".vscode", ".idea", ".vs", ".eclipse", ".netbeans", "__pycache__", ".pytest_cache", "venv", ".venv", "env", ".env", "site-packages", ".bundle", "gems", "target", "gradle", ".gradle", "bin", "obj", "packages", "vendor", "vendor", "coverage", ".nyc_output", ".coverage", "htmlcov", "_site", "public", ".DS_Store", "Thumbs.db", "desktop.ini" ]; const MEDIA_EXCLUSIONS = [ "images", "imgs", "photos", "videos", "movies", "music", "audio", "sounds", "Downloads", "Desktop", "Documents/VirtualMachines", "Library/Caches", "Library/Logs", "AppData", "Application Support", "VirtualBox VMs", ".vagrant", "parallels", "vmware", "Dropbox", "Google Drive", "OneDrive", "iCloud Drive" ]; const DEFAULT_EXCLUSIONS = [...SECURITY_EXCLUSIONS, ...DEVELOPMENT_EXCLUSIONS]; const CONSERVATIVE_EXCLUSIONS = [...DEFAULT_EXCLUSIONS, ...MEDIA_EXCLUSIONS]; /** * Security-focused exclusions for paranoid users */ const PARANOID_EXCLUSIONS = [ ...DEFAULT_EXCLUSIONS, ...MEDIA_EXCLUSIONS, "Mail", "Messages", "Safari", "Library/Mail", "Library/Messages", "Library/Safari", "Library/Cookies", "Library/Preferences", "System", "usr", "opt", "var", "etc", "proc", "dev", "sys" ]; /** * Get exclusion patterns based on security level */ const getExclusionPatterns = (level = "default") => { return match(level).with("conservative", () => CONSERVATIVE_EXCLUSIONS).with("paranoid", () => PARANOID_EXCLUSIONS).with("default", () => DEFAULT_EXCLUSIONS).exhaustive(); }; /** * Check if a directory name should be excluded based on security patterns */ const isSecuritySensitive = (dirName) => { return SECURITY_EXCLUSIONS.some((pattern) => dirName === pattern || dirName.includes(pattern)); }; /** * Check if a directory is development-related */ const isDevelopmentDirectory = (dirName) => { return DEVELOPMENT_EXCLUSIONS.some((pattern) => dirName === pattern || dirName.includes(pattern)); }; if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; describe("Exclusion Patterns", () => { test("should identify security-sensitive directories", () => { expect(isSecuritySensitive(".ssh")).toBe(true); expect(isSecuritySensitive(".aws")).toBe(true); expect(isSecuritySensitive("safe-directory")).toBe(false); }); test("should identify development directories", () => { expect(isDevelopmentDirectory("node_modules")).toBe(true); expect(isDevelopmentDirectory(".git")).toBe(true); expect(isDevelopmentDirectory("src")).toBe(false); }); test("should return correct exclusion patterns by level", () => { const defaultExclusions = getExclusionPatterns("default"); const conservativeExclusions = getExclusionPatterns("conservative"); const paranoidExclusions = getExclusionPatterns("paranoid"); expect(defaultExclusions.length).toBeLessThan(conservativeExclusions.length); expect(conservativeExclusions.length).toBeLessThan(paranoidExclusions.length); expect(defaultExclusions).toContain(".ssh"); expect(defaultExclusions).toContain("node_modules"); expect(conservativeExclusions).toContain("Downloads"); expect(paranoidExclusions).toContain("Mail"); }); test("should have non-empty exclusion lists", () => { expect(SECURITY_EXCLUSIONS.length).toBeGreaterThan(0); expect(DEVELOPMENT_EXCLUSIONS.length).toBeGreaterThan(0); expect(MEDIA_EXCLUSIONS.length).toBeGreaterThan(0); }); }); } //#endregion //#region src/fast-scanner.ts var import_usingCtx$2 = __toESM(require_usingCtx(), 1); const CLAUDE_FILE_REGEX = /^CLAUDE\.(md|local\.md)$/; /** * Create a base crawler with common configuration */ const createBaseCrawler = (options$1) => { const crawler = new fdir().withFullPaths().exclude((dirName) => { const exclusions = DEFAULT_EXCLUSIONS; if (exclusions.includes(dirName)) return true; if (!options$1.includeHidden && dirName.startsWith(".") && dirName !== ".claude") return true; return false; }); return options$1.recursive ? crawler.withMaxDepth(options$1.maxDepth) : crawler.withMaxDepth(options$1.maxDepth); }; /** * Find Claude configuration files using fdir * Fast file scanner using fdir (fastest directory crawler for Node.js) * Can crawl 1 million files in < 1 second */ const findClaudeFiles = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const crawler = createBaseCrawler({ includeHidden, recursive, maxDepth: recursive ? 20 : 1 }).filter((filePath) => { const fileName = basename(filePath); return CLAUDE_FILE_REGEX.test(fileName); }); try { const files = await crawler.crawl(path).withPromise(); return files; } catch (error) { console.warn(`Failed to scan Claude files in ${path}: ${error instanceof Error ? error.message : "Unknown error"}`); return []; } }; /** * Find slash command files using fdir */ const findSlashCommands = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const crawler = createBaseCrawler({ includeHidden, recursive, maxDepth: recursive ? 20 : 3 }).filter((filePath) => { return (filePath.includes("/.claude/commands/") || filePath.includes("/commands/")) && filePath.endsWith(".md"); }); try { const files = await crawler.crawl(path).withPromise(); return files; } catch (error) { console.warn(`Failed to scan slash commands in ${path}: ${error instanceof Error ? error.message : "Unknown error"}`); return []; } }; /** * Find subagent files using fdir */ const findSubAgents = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const results = []; const projectCrawler = createBaseCrawler({ includeHidden, recursive, maxDepth: recursive ? 20 : 4 }).filter((filePath) => { return filePath.includes("/.claude/agents/") && filePath.endsWith(".md"); }); try { const projectFiles = await projectCrawler.crawl(path).withPromise(); results.push(...projectFiles); } catch (error) { console.warn(`Failed to scan project subagents in ${path}: ${error instanceof Error ? error.message : "Unknown error"}`); } const userAgentsPath = `${homedir()}/.claude/agents`; const userCrawler = new fdir().withFullPaths().filter((filePath) => filePath.endsWith(".md")); try { const userFiles = await userCrawler.crawl(userAgentsPath).withPromise(); results.push(...userFiles); } catch (_error) {} return results; }; const findSettingsJson = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const crawler = createBaseCrawler({ includeHidden, recursive, maxDepth: recursive ? 20 : 4 }).filter((filePath) => { const fileName = basename(filePath); return filePath.includes("/.claude/") && (fileName === "settings.json" || fileName === "settings.local.json"); }); try { const files = await crawler.crawl(path).withPromise(); return files; } catch (error) { console.warn(`Failed to scan settings.json files in ${path}: ${error instanceof Error ? error.message : "Unknown error"}`); return []; } }; if (import.meta.vitest != null) { /** * Check if fdir is available (always true since it's a dependency) * Internal function for testing only */ const isAvailable = async () => { return true; }; /** * Get fdir version information * Internal function for testing only */ const getVersion = async () => { try { const pkg = await import("fdir/package.json"); return `fdir ${pkg.version}`; } catch { return "fdir (version unknown)"; } }; const { describe, test, expect } = import.meta.vitest; const { createClaudeProjectFixture, createComplexProjectFixture, withTempFixture, DEFAULT_CLAUDE_MD } = await import("./test-fixture-helpers-jC6qse8O.js"); describe("fast-scanner", () => { test("should be available after installation", async () => { const available = await isAvailable(); expect(available).toBe(true); }); test("should return version information", async () => { const version = await getVersion(); expect(version).toMatch(/fdir/); }); test("should find CLAUDE.md files with fs-fixture", async () => { try { var _usingCtx = (0, import_usingCtx$2.default)(); const fixture = _usingCtx.a(await createClaudeProjectFixture({ projectName: "scanner-test", includeLocal: true })); const files = await findClaudeFiles({ path: fixture.getPath("scanner-test"), recursive: false }); expect(Array.isArray(files)).toBe(true); expect(files.length).toBe(2); expect(files.some((file) => file.endsWith("CLAUDE.md"))).toBe(true); expect(files.some((file) => file.endsWith("CLAUDE.local.md"))).toBe(true); } catch (_) { _usingCtx.e = _; } finally { await _usingCtx.d(); } }); test("should respect recursive option with nested structure", async () => { try { var _usingCtx3 = (0, import_usingCtx$2.default)(); const _fixture = _usingCtx3.a(await withTempFixture({ project: { "CLAUDE.md": DEFAULT_CLAUDE_MD, nested: { deep: { "CLAUDE.md": DEFAULT_CLAUDE_MD } } } }, async (f) => { const nonRecursive = await findClaudeFiles({ path: f.getPath("project"), recursive: false }); const recursive = await findClaudeFiles({ path: f.getPath("project"), recursive: true }); expect(nonRecursive.length).toBe(1); expect(recursive.length).toBe(2); return f; })); } catch (_) { _usingCtx3.e = _; } finally { await _usingCtx3.d(); } }); test("should find slash command files in complex structure", async () => { try { var _usingCtx4 = (0, import_usingCtx$2.default)(); const fixture = _usingCtx4.a(await createComplexProjectFixture()); const commands = await findSlashCommands({ path: fixture.getPath("my-app"), recursive: true }); expect(Array.isArray(commands)).toBe(true); expect(commands.length).toBeGreaterThan(0); expect(commands.some((cmd) => cmd.includes("test.md"))).toBe(true); expect(commands.some((cmd) => cmd.includes("production/deploy.md"))).toBe(true); } catch (_) { _usingCtx4.e = _; } finally { await _usingCtx4.d(); } }); test("should handle non-existent paths gracefully", async () => { const files = await findClaudeFiles({ path: "/non/existent/path", recursive: false }); expect(Array.isArray(files)).toBe(true); expect(files.length).toBe(0); }); test("should find settings.json files", async () => { try { var _usingCtx5 = (0, import_usingCtx$2.default)(); const _fixture = _usingCtx5.a(await withTempFixture({ ".claude": { "settings.json": JSON.stringify({ version: "1.0" }), "settings.local.json": JSON.stringify({ local: true }) }, project: { ".claude": { "settings.json": JSON.stringify({ project: true }) } } }, async (f) => { const settings = await findSettingsJson({ path: f.path, recursive: true }); expect(Array.isArray(settings)).toBe(true); expect(settings.length).toBe(3); expect(settings.some((file) => file.endsWith("settings.json"))).toBe(true); expect(settings.some((file) => file.endsWith("settings.local.json"))).toBe(true); return f; })); } catch (_) { _usingCtx5.e = _; } finally { await _usingCtx5.d(); } }); test("should respect exclude patterns", async () => { try { var _usingCtx6 = (0, import_usingCtx$2.default)(); const _fixture = _usingCtx6.a(await withTempFixture({ "test-project": { "CLAUDE.md": DEFAULT_CLAUDE_MD, node_modules: { "CLAUDE.md": "Should be excluded" }, ".git": { "CLAUDE.md": "Should be excluded" } } }, async (f) => { const files = await findClaudeFiles({ path: f.getPath("test-project"), recursive: true }); expect(files.length).toBe(1); expect(files[0]).toContain("test-project/CLAUDE.md"); expect(files[0]).not.toContain("node_modules"); expect(files[0]).not.toContain(".git"); return f; })); } catch (_) { _usingCtx6.e = _; } finally { await _usingCtx6.d(); } }); test("should handle large directory structures efficiently", async () => { try { var _usingCtx7 = (0, import_usingCtx$2.default)(); const largeStructure = {}; for (let i = 0; i < 100; i++) largeStructure[`dir-${i}`] = { "CLAUDE.md": `Content ${i}`, sub: { "CLAUDE.local.md": `Local ${i}` } }; const _fixture = _usingCtx7.a(await withTempFixture(largeStructure, async (f) => { const start = Date.now(); const files = await findClaudeFiles({ path: f.path, recursive: true }); const duration = Date.now() - start; expect(files.length).toBe(200); expect(duration).toBeLessThan(1e3); return f; })); } catch (_) { _usingCtx7.e = _; } finally { await _usingCtx7.d(); } }); test.skip("should handle hidden directories correctly", async () => { try { var _usingCtx8 = (0, import_usingCtx$2.default)(); const _fixture = _usingCtx8.a(await withTempFixture({ ".hidden": { "CLAUDE.md": DEFAULT_CLAUDE_MD }, ".claude": { commands: { "test.md": "Test command" } }, visible: { "CLAUDE.md": DEFAULT_CLAUDE_MD } }, async (f) => { const withoutHidden = await findClaudeFiles({ path: f.path, recursive: true, includeHidden: false }); const withHidden = await findClaudeFiles({ path: f.path, recursive: true, includeHidden: true }); expect(withoutHidden.length).toBe(1); expect(withHidden.length).toBe(2); const commands = await findSlashCommands({ path: f.path, recursive: true, includeHidden: false }); expect(Array.isArray(commands)).toBe(true); expect(commands.length).toBe(1); return f; })); } catch (_) { _usingCtx8.e = _; } finally { await _usingCtx8.d(); } }); }); } //#endregion //#region src/claude-md-scanner.ts var import_usingCtx$1 = __toESM(require_usingCtx(), 1); const scanClaudeFiles = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; try { const files = await findClaudeFiles({ path, recursive, includeHidden }); if (recursive) { const { homedir: homedir$1 } = await import("node:os"); const homeDir = homedir$1(); if (path !== homeDir && path !== join(homeDir, ".claude")) { const globalClaudeDir = join(homeDir, ".claude"); if (existsSync(globalClaudeDir)) { const globalFiles = await findClaudeFiles({ path: globalClaudeDir, recursive: true, includeHidden }); files.push(...globalFiles); } const homeClaudeFile = join(homeDir, "CLAUDE.md"); if (existsSync(homeClaudeFile)) files.push(homeClaudeFile); try { const homeContents = await readdir(homeDir, { withFileTypes: true }); const directoriesToSkip = new Set([ ".cache", ".npm", ".yarn", ".pnpm", "node_modules", ".git", ".svn", ".hg", "Library", "Applications", ".Trash", ".local", ".config", ".vscode", ".idea" ]); const projectDirectories = new Set([ "my_programs", "projects", "dev", "development", "workspace", "work", "code", "repos", "git", "Documents", "Desktop", "src", "source" ]); for (const entry of homeContents) if (entry.isDirectory() && !directoriesToSkip.has(entry.name) && !entry.name.startsWith(".")) { const dirPath = join(homeDir, entry.name); const claudeMdPath = join(dirPath, "CLAUDE.md"); if (existsSync(claudeMdPath)) files.push(claudeMdPath); const claudeLocalPath = join(dirPath, "CLAUDE.local.md"); if (existsSync(claudeLocalPath)) files.push(claudeLocalPath); if (projectDirectories.has(entry.name)) try { const projectFiles = await findClaudeFiles({ path: dirPath, recursive: true, includeHidden: false }); files.push(...projectFiles); } catch (error) { console.warn(`Failed to scan ${entry.name} subdirectories:`, error); } } } catch (error) { console.warn("Failed to scan home subdirectories:", error); } } } const uniqueFiles = uniq(files); const fileInfos = []; for (const filePath of uniqueFiles) try { const fileInfo = await processClaudeFile(filePath); if (fileInfo) fileInfos.push(fileInfo); } catch (error) { console.warn(`Failed to process file: ${filePath}`, error); } return fileInfos.sort((a$1, b$1) => b$1.lastModified.getTime() - a$1.lastModified.getTime()); } catch (error) { throw new Error(`Failed to scan Claude files: ${isError(error) ? error.message : "Unknown error"}`); } }; const getSearchPatterns = (type, recursive = true) => { const patterns = []; const prefix = recursive ? "**/" : ""; if (!type || type === "project-memory") patterns.push(`${prefix}CLAUDE.md`); if (!type || type === "project-memory-local") patterns.push(`${prefix}CLAUDE.local.md`); if (!type || type === "user-memory") patterns.push(CLAUDE_FILE_PATTERNS.GLOBAL_CLAUDE_MD); if (!type || type === "project-command") patterns.push(`${prefix}.claude/commands/**/*.md`); if (!type || type === "personal-command") patterns.push(CLAUDE_FILE_PATTERNS.USER_SLASH_COMMANDS); return patterns; }; var ClaudeMdScanner = class extends BaseFileScanner { maxFileSize = FILE_SIZE_LIMITS.MAX_CLAUDE_MD_SIZE; fileType = "Claude.md"; async parseContent(filePath, content, stats) { if (!validateClaudeMdContent(content)) { console.warn(`Invalid Claude.md content, skipping: ${filePath}`); return null; } const fileType = detectClaudeFileType(filePath); const tags = extractTagsFromContent(content); const commands = extractCommandsFromContent(content); return { path: createClaudeFilePath(filePath), type: fileType, size: stats.size, lastModified: stats.mtime, commands, tags }; } }; const scanner$2 = new ClaudeMdScanner(); const processClaudeFile = (filePath) => scanner$2.processFile(filePath); if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; const { createClaudeProjectFixture, createComplexProjectFixture, withTempFixture, DEFAULT_CLAUDE_MD } = await import("./test-fixture-helpers-jC6qse8O.js"); describe("getSearchPatterns", () => { test("should return all patterns when no type specified", () => { const patterns = getSearchPatterns(void 0, true); expect(patterns).toContain("**/CLAUDE.md"); expect(patterns).toContain("**/CLAUDE.local.md"); expect(patterns).toContain("**/.claude/commands/**/*.md"); }); test("should return specific pattern for project-memory type", () => { const patterns = getSearchPatterns("project-memory", true); expect(patterns).toContain("**/CLAUDE.md"); expect(patterns).not.toContain("**/CLAUDE.local.md"); }); test("should respect recursive option", () => { const patterns = getSearchPatterns("project-memory", false); expect(patterns).toContain("CLAUDE.md"); expect(patterns).not.toContain("**/CLAUDE.md"); }); test("should return user slash commands pattern for personal-command type", () => { const patterns = getSearchPatterns("personal-command", true); expect(patterns).toHaveLength(1); expect(patterns[0]).toContain(".claude/commands/"); }); test("should include personal-command pattern when no type specified", () => { const patterns = getSearchPatterns(void 0, true); expect(patterns.some((p) => p.includes(".claude/commands/") && p.includes("/"))).toBe(true); }); }); describe("scanClaudeFiles", () => { test("should scan files in a fixture directory", async () => { try { var _usingCtx = (0, import_usingCtx$1.default)(); const fixture = _usingCtx.a(await createClaudeProjectFixture({ projectName: "test-scan", includeLocal: true, includeCommands: true })); const result = await scanClaudeFiles({ path: fixture.getPath("test-scan"), recursive: false }); expect(Array.isArray(result)).toBe(true); expect(result.length).toBe(2); const types = result.map((file) => file.type); expect(types).toContain("project-memory"); expect(types).toContain("project-memory-local"); } catch (_) { _usingCtx.e = _; } finally { await _usingCtx.d(); } }, 1e4); test("should handle empty directory", async () => { try { var _usingCtx3 = (0, import_usingCtx$1.default)(); const _fixture = _usingCtx3.a(await withTempFixture({ "empty-dir": {} }, async (f) => { const result = await scanClaudeFiles({ path: f.getPath("empty-dir"), recursive: false }); expect(result).toEqual([]); return f; })); } catch (_) { _usingCtx3.e = _; } finally { await _usingCtx3.d(); } }); test("should use current directory as default path", async () => { const options$1 = {}; expect(options$1.path).toBeUndefined(); }); test("should sort files by last modified date", async () => { try { var _usingCtx4 = (0, import_usingCtx$1.default)(); const fixture = _usingCtx4.a(await createClaudeProjectFixture({ projectName: "sort-test", includeLocal: true })); await new Promise((resolve$1) => setTimeout(resolve$1, 10)); await fixture.writeFile("sort-test/CLAUDE.local.md", `${DEFAULT_CLAUDE_MD}\n// Updated`); const result = await scanClaudeFiles({ path: fixture.getPath("sort-test"), recursive: false }); expect(result[0]?.type).toBe("project-memory-local"); expect(result[1]?.type).toBe("project-memory"); } catch (_) { _usingCtx4.e = _; } finally { await _usingCtx4.d(); } }); }); describe("processClaudeFile", () => { test("should return null for non-existent file", async () => { const result = await processClaudeFile("/non/existent/file.md"); expect(result).toBeNull(); }); test("should process valid CLAUDE.md file", async () => { try { var _usingCtx5 = (0, import_usingCtx$1.default)(); const fixture = _usingCtx5.a(await createClaudeProjectFixture({ projectName: "process-test" })); const filePath = fixture.getPath("process-test/CLAUDE.md"); const result = await processClaudeFile(filePath); expect(result).not.toBeNull(); expect(result?.type).toBe("project-memory"); expect(result?.path).toBe(filePath); expect(result?.size).toBeGreaterThan(0); } catch (_) { _usingCtx5.e = _; } finally { await _usingCtx5.d(); } }); test("should extract project info", async () => { try { var _usingCtx6 = (0, import_usingCtx$1.default)(); const fixture = _usingCtx6.a(await createComplexProjectFixture()); const filePath = fixture.getPath("my-app/CLAUDE.md"); const result = await processClaudeFile(filePath); expect(result).toBeDefined(); expect(result?.type).toBe("project-memory"); } catch (_) { _usingCtx6.e = _; } finally { await _usingCtx6.d(); } }); }); describe("findGlobalClaudeFiles", () => { test("should find Claude files in complex project structure", async () => { try { var _usingCtx7 = (0, import_usingCtx$1.default)(); const fixture = _usingCtx7.a(await createComplexProjectFixture()); const result = await scanClaudeFiles({ path: fixture.getPath("my-app"), recursive: false }); expect(result.length).toBe(2); const types = result.map((f) => f.type); expect(types).toContain("project-memory"); expect(types).toContain("project-memory-local"); } catch (_) { _usingCtx7.e = _; } finally { await _usingCtx7.d(); } }); test("should handle includeHidden option", async () => { try { var _usingCtx8 = (0, import_usingCtx$1.default)(); const { createFixture } = await import("./dist-D99sJLU7.js"); const fixture = _usingCtx8.a(await createFixture({ ".hidden": { "CLAUDE.md": DEFAULT_CLAUDE_MD }, visible: { "CLAUDE.md": DEFAULT_CLAUDE_MD } })); const withoutHidden = await scanClaudeFiles({ path: fixture.path, recursive: false, includeHidden: false }); const withHidden = await scanClaudeFiles({ path: fixture.path, recursive: false, includeHidden: true }); expect(withoutHidden.length).toBe(1); expect(withHidden.length).toBe(2); } catch (_) { _usingCtx8.e = _; } finally { await _usingCtx8.d(); } }, 1e4); }); } //#endregion //#region src/settings-json-scanner.ts /** * Settings JSON scanner for parsing .claude/project/settings.json files */ var SettingsJsonScanner = class extends BaseFileScanner { maxFileSize = 1024 * 1024; fileType = "settings.json"; async parseContent(filePath, content, stats) { try { JSON.parse(content); const tags = []; return { path: createClaudeFilePath(filePath), type: detectClaudeFileType(filePath), size: stats.size, lastModified: stats.mtime, commands: [], tags }; } catch (error) { console.warn(`Invalid JSON in settings file ${filePath}:`, error); return null; } } }; /** * Scan for settings.json files in .claude/project directories */ const scanSettingsJson = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const paths = await findSettingsJson({ path, recursive, includeHidden }); if (recursive) { const { homedir: homedir$1 } = await import("node:os"); const { join: join$1 } = await import("node:path"); const globalClaudePath = join$1(homedir$1(), ".claude"); if (globalClaudePath !== path && !path.startsWith(globalClaudePath)) { const globalFiles = await findSettingsJson({ path: globalClaudePath, recursive: false, includeHidden }); paths.push(...globalFiles); } } const uniquePaths = Array.from(new Set(paths)); const scanner$3 = new SettingsJsonScanner(); const results = await Promise.all(uniquePaths.map((path$1) => scanner$3.processFile(path$1))); return results.filter((file) => file !== null); }; if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; const { createFixture } = await import("./dist-D99sJLU7.js"); describe("SettingsJsonScanner", () => { test("should parse valid settings.json files", async () => { const scanner$3 = new SettingsJsonScanner(); const fixture = await createFixture({ ".claude": { "settings.json": JSON.stringify({ version: "1.0", features: ["feature1", "feature2"] }) } }); try { const result = await scanner$3.processFile(`${fixture.path}/.claude/settings.json`); expect(result).toBeTruthy(); expect(result?.type).toBe("project-settings"); expect(result?.path).toContain("settings.json"); } finally { await fixture.rm(); } }); test("should handle invalid JSON gracefully", async () => { const scanner$3 = new SettingsJsonScanner(); const fixture = await createFixture({ ".claude": { "settings.json": "{ invalid json" } }); try { const result = await scanner$3.processFile(`${fixture.path}/.claude/settings.json`); expect(result).toBeNull(); } finally { await fixture.rm(); } }); test("should handle large files", async () => { const scanner$3 = new SettingsJsonScanner(); const largeContent = JSON.stringify({ data: "x".repeat(1048577) }); const fixture = await createFixture({ ".claude": { "settings.json": largeContent } }); try { const result = await scanner$3.processFile(`${fixture.path}/.claude/settings.json`); expect(result).toBeNull(); } finally { await fixture.rm(); } }); }); describe("scanSettingsJson", () => { test("should scan multiple settings.json files", async () => { const fixture = await createFixture({ project1: { ".claude": { "settings.json": JSON.stringify({ project: "project1" }), "settings.local.json": JSON.stringify({ local: true }) } }, project2: { ".claude": { "settings.json": JSON.stringify({ project: "project2" }) } } }); try { const results = await scanSettingsJson({ path: fixture.path, recursive: false }); expect(results).toHaveLength(3); expect(results.filter((file) => file.type === "project-settings")).toHaveLength(2); expect(results.filter((file) => file.type === "project-settings-local")).toHaveLength(1); } finally { await fixture.rm(); } }, 1e4); }); } //#endregion //#region src/slash-command-scanner.ts const scanSlashCommands = async (options$1 = {}) => { const { path = process.cwd(), recursive = true, includeHidden = false } = options$1; const _patterns = getSlashCommandPatterns(recursive); const searchPaths = [path, join(homedir(), ".claude", "commands")]; try { const allCommands = []; for (const searchPath of searchPaths) { if (!existsSync(searchPath)) continue; const files = await findS