obsidian_vault_suggest
Analyzes Obsidian vault to suggest reorganization actions like creating MOCs, consolidating notes, archiving candidates, and identifying missing tags.
Instructions
Suggest vault reorganization actions: MOCs, consolidation, archive candidates, and missing tags.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| vault | No | Optional configured vault name. Defaults to the server default vault. | |
| staleDays | No | ||
| maxSuggestions | No |
Implementation Reference
- src/tools.ts:869-883 (registration)Registration of the 'obsidian_vault_suggest' tool with its schema and handler. Calls vaultThemes and vaultSuggestions from intelligence.ts.
tool( "obsidian_vault_suggest", "Suggest vault reorganization actions: MOCs, consolidation, archive candidates, and missing tags.", { vault: vaultArg, staleDays: z.number().int().min(30).max(5000).optional().default(365), maxSuggestions: z.number().int().min(1).max(200).optional().default(50), }, async (args) => { const notes = await loadNotes(vaults, args.vault); const themes = vaultThemes(notes, 30, 2).themes; return { themes, suggestions: vaultSuggestions(notes, themes, args) }; }, { readOnlyHint: true }, ); - src/tools.ts:870-876 (schema)Input schema for obsidian_vault_suggest: vault (optional string), staleDays (optional int, default 365), maxSuggestions (optional int, default 50).
"obsidian_vault_suggest", "Suggest vault reorganization actions: MOCs, consolidation, archive candidates, and missing tags.", { vault: vaultArg, staleDays: z.number().int().min(30).max(5000).optional().default(365), maxSuggestions: z.number().int().min(1).max(200).optional().default(50), }, - src/tools.ts:877-882 (handler)Handler function for obsidian_vault_suggest: loads notes, computes themes via vaultThemes(), and generates suggestions via vaultSuggestions().
async (args) => { const notes = await loadNotes(vaults, args.vault); const themes = vaultThemes(notes, 30, 2).themes; return { themes, suggestions: vaultSuggestions(notes, themes, args) }; }, { readOnlyHint: true }, - src/intelligence.ts:125-175 (helper)The vaultSuggestions() function that generates reorganization suggestions (MOC creation, consolidation, archiving, tagging) based on notes, themes, and options.
export function vaultSuggestions( notes: NoteRecord[], themes: Theme[], options: { staleDays?: number; maxSuggestions?: number } = {}, ): Suggestion[] { const staleCutoff = Date.now() - (options.staleDays ?? 365) * 24 * 60 * 60 * 1000; const suggestions: Suggestion[] = []; for (const theme of themes) { if (theme.files.length >= 5) { suggestions.push({ type: "create_moc", priority: "medium", action: `Create MOC-${safeTitle(theme.label)}.md linking ${theme.files.length} notes`, paths: theme.files, reason: `Theme "${theme.label}" has enough related notes to justify a map-of-content note.`, }); } if (theme.crossFolder && theme.files.length >= 3) { const dominant = dominantFolder(theme.files); suggestions.push({ type: "consolidate", priority: "low", action: `Review whether ${theme.files.length} "${theme.label}" notes should live closer to ${dominant}/`, paths: theme.files, reason: `The theme spans ${theme.folders.length} folders: ${theme.folders.join(", ")}.`, }); } } for (const note of notes) { const modified = Date.parse(note.mtime); if (Number.isFinite(modified) && modified < staleCutoff && note.path.split("/")[0] !== "04-Archive") { suggestions.push({ type: "archive", priority: "low", action: `Consider archiving ${note.path}`, paths: [note.path], reason: `Not modified since ${note.mtime.slice(0, 10)}.`, }); } if (note.tags.length === 0 && extractHeadings(note.content).length > 1) { suggestions.push({ type: "tag", priority: "low", action: `Add tags to ${note.path}`, paths: [note.path], reason: "The note has structure but no discovered tags.", }); } } return suggestions.slice(0, options.maxSuggestions ?? 50); } - src/intelligence.ts:81-123 (helper)The vaultThemes() function that clusters notes into themes using TF-IDF-style term extraction, used by obsidian_vault_suggest.
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), }; }