feat: expand smart-explore to 24 languages with markdown support and user-installable grammars

Add 15 new tree-sitter language grammars (Kotlin, Swift, PHP, Elixir, Lua, Scala,
Bash, Haskell, Zig, CSS, SCSS, TOML, YAML, SQL, Markdown) with verified SCM queries.
Add markdown-specific formatting with heading hierarchy, code block detection, and
section-aware unfold. Add user-installable grammar system via .claude-mem.json config
with custom query file support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-07 13:24:56 -07:00
parent 9f01228a2b
commit 95889c7b4e
7 changed files with 641 additions and 35 deletions
+33 -8
View File
@@ -12,7 +12,7 @@
import { readFile, readdir, stat } from "node:fs/promises";
import { join, relative } from "node:path";
import { parseFilesBatch, formatFoldedView, type FoldedFile } from "./parser.js";
import { parseFilesBatch, formatFoldedView, loadUserGrammars, type FoldedFile } from "./parser.js";
const CODE_EXTENSIONS = new Set([
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
@@ -22,11 +22,22 @@ const CODE_EXTENSIONS = new Set([
".rb",
".java",
".cs",
".cpp", ".c", ".h", ".hpp",
".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hh",
".swift",
".kt",
".kt", ".kts",
".php",
".vue", ".svelte",
".ex", ".exs",
".lua",
".scala", ".sc",
".sh", ".bash", ".zsh",
".hs",
".zig",
".css", ".scss",
".toml",
".yml", ".yaml",
".sql",
".md", ".mdx",
]);
const IGNORE_DIRS = new Set([
@@ -59,8 +70,9 @@ export interface SymbolMatch {
/**
* Walk a directory recursively, yielding file paths.
* extraExtensions: additional file extensions to include (from user grammar config).
*/
async function* walkDir(dir: string, rootDir: string, maxDepth: number = 20): AsyncGenerator<string> {
async function* walkDir(dir: string, rootDir: string, maxDepth: number = 20, extraExtensions?: Set<string>): AsyncGenerator<string> {
if (maxDepth <= 0) return;
let entries;
@@ -77,10 +89,10 @@ async function* walkDir(dir: string, rootDir: string, maxDepth: number = 20): As
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(fullPath, rootDir, maxDepth - 1);
yield* walkDir(fullPath, rootDir, maxDepth - 1, extraExtensions);
} else if (entry.isFile()) {
const ext = entry.name.slice(entry.name.lastIndexOf("."));
if (CODE_EXTENSIONS.has(ext)) {
if (CODE_EXTENSIONS.has(ext) || (extraExtensions && extraExtensions.has(ext))) {
yield fullPath;
}
}
@@ -121,16 +133,29 @@ export async function searchCodebase(
maxResults?: number;
includeImports?: boolean;
filePattern?: string;
projectRoot?: string;
} = {}
): Promise<SearchResult> {
const maxResults = options.maxResults || 20;
const queryLower = query.toLowerCase();
const queryParts = queryLower.split(/[\s_\-./]+/).filter(p => p.length > 0);
// Load user grammar config for extra file extensions
const projectRoot = options.projectRoot || rootDir;
const userConfig = loadUserGrammars(projectRoot);
const extraExtensions = new Set<string>();
for (const entry of Object.values(userConfig.grammars)) {
for (const ext of entry.extensions) {
if (!CODE_EXTENSIONS.has(ext)) {
extraExtensions.add(ext);
}
}
}
// Phase 1: Collect files
const filesToParse: Array<{ absolutePath: string; relativePath: string; content: string }> = [];
for await (const filePath of walkDir(rootDir, rootDir)) {
for await (const filePath of walkDir(rootDir, rootDir, 20, extraExtensions.size > 0 ? extraExtensions : undefined)) {
if (options.filePattern) {
const relPath = relative(rootDir, filePath);
if (!relPath.toLowerCase().includes(options.filePattern.toLowerCase())) continue;
@@ -147,7 +172,7 @@ export async function searchCodebase(
}
// Phase 2: Batch parse (one CLI call per language)
const parsedFiles = parseFilesBatch(filesToParse);
const parsedFiles = parseFilesBatch(filesToParse, projectRoot);
// Phase 3: Match query against symbols
const foldedFiles: FoldedFile[] = [];