obsidian_vault_themes
Extract key terms from vault files using TF-IDF, then cluster them to map underlying themes.
Instructions
Map vault themes with TF-IDF-style term extraction and lightweight clustering.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| vault | No | Optional configured vault name. Defaults to the server default vault. | |
| limit | No | ||
| minFiles | No |
Implementation Reference
- src/tools.ts:857-867 (registration)Tool registration for 'obsidian_vault_themes'. Defines the tool name, description, Zod schema (vault, limit, minFiles), and calls vaultThemes() from intelligence.ts. Annotated as readOnlyHint.
tool( "obsidian_vault_themes", "Map vault themes with TF-IDF-style term extraction and lightweight clustering.", { vault: vaultArg, limit: z.number().int().min(1).max(100).optional().default(20), minFiles: z.number().int().min(1).max(20).optional().default(2), }, async (args) => vaultThemes(await loadNotes(vaults, args.vault), args.limit, args.minFiles), { readOnlyHint: true }, ); - src/tools.ts:857-867 (schema)Input schema for 'obsidian_vault_themes': vault (optional string), limit (1-100, default 20), minFiles (1-20, default 2).
tool( "obsidian_vault_themes", "Map vault themes with TF-IDF-style term extraction and lightweight clustering.", { vault: vaultArg, limit: z.number().int().min(1).max(100).optional().default(20), minFiles: z.number().int().min(1).max(20).optional().default(2), }, async (args) => vaultThemes(await loadNotes(vaults, args.vault), args.limit, args.minFiles), { readOnlyHint: true }, ); - src/intelligence.ts:81-123 (handler)Core handler function vaultThemes(). Takes NoteRecord[], limit, and minFiles. Extracts top TF-IDF terms per note, clusters notes with Jaccard similarity >= 0.25, filters by minFiles, sorts by file count/coherence, and returns themes + orphanCandidates.
export function vaultThemes(notes: NoteRecord[], limit = 20, minFiles = 2): { themes: Theme[]; orphanCandidates: string[] } { const topTermsByNote = notes.map((note) => ({ note, terms: topTerms(note, 12), })); const themes: Theme[] = []; for (const item of topTermsByNote) { if (item.terms.length === 0) continue; let best: { theme: Theme; score: number } | null = null; for (const theme of themes) { const overlap = jaccard(new Set(item.terms.slice(0, 8)), new Set(theme.keyTerms.slice(0, 8))); if (!best || overlap > best.score) best = { theme, score: overlap }; } if (best && best.score >= 0.25) { best.theme.files.push(item.note.path); best.theme.keyTerms = mergeTopTerms(best.theme.keyTerms, item.terms); best.theme.folders = uniqueFolders(best.theme.files); best.theme.crossFolder = best.theme.folders.length > 1; best.theme.coherence = Number(((best.theme.coherence + best.score) / 2).toFixed(3)); } else { const files = [item.note.path]; themes.push({ id: `theme-${themes.length + 1}`, label: labelFromTerms(item.terms), keyTerms: item.terms, files, folders: uniqueFolders(files), coherence: 1, crossFolder: false, }); } } const filtered = themes .filter((theme) => theme.files.length >= minFiles) .map((theme, i) => ({ ...theme, id: `theme-${i + 1}`, label: labelFromTerms(theme.keyTerms) })) .sort((a, b) => b.files.length - a.files.length || b.coherence - a.coherence) .slice(0, limit); const themed = new Set(filtered.flatMap((theme) => theme.files)); return { themes: filtered, orphanCandidates: notes.filter((note) => !themed.has(note.path)).map((note) => note.path).slice(0, 100), }; } - src/intelligence.ts:14-22 (helper)Type definition for Theme: id, label, keyTerms, files, folders, coherence, crossFolder. Used as the return type of vaultThemes().
export type Theme = { id: string; label: string; keyTerms: string[]; files: string[]; folders: string[]; coherence: number; crossFolder: boolean; }; - src/intelligence.ts:259-269 (helper)Helper functions used by vaultThemes: labelFromTerms() (creates label from top 3 key terms), safeTitle() (Title Case formatting), uniqueFolders() (extracts unique dirnames), jaccard() (set similarity), mergeTopTerms() (union of term arrays), topTerms() (extract top weighted terms from a note).
function labelFromTerms(terms: string[]): string { return safeTitle(terms.slice(0, 3).join(" ")); } function safeTitle(text: string): string { return text .split(/\s+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); }