diff options
| author | Adam Malczewski <[email protected]> | 2026-03-24 19:57:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-24 19:57:16 +0900 |
| commit | 0d7e2758d28bb37c9d724f79008239a5e29e6ce4 (patch) | |
| tree | cdd294a7a45e712d86c39e15340cadf2a9a1bc91 /src/vault-context.ts | |
| parent | a5f54269f6b7ace71c4509fb8105993a7f064e63 (diff) | |
| download | ai-pulse-obsidian-plugin-0d7e2758d28bb37c9d724f79008239a5e29e6ce4.tar.gz ai-pulse-obsidian-plugin-0d7e2758d28bb37c9d724f79008239a5e29e6ce4.zip | |
Add vault context injection, frontmatter tool, vision idea
Diffstat (limited to 'src/vault-context.ts')
| -rw-r--r-- | src/vault-context.ts | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/src/vault-context.ts b/src/vault-context.ts new file mode 100644 index 0000000..80afa03 --- /dev/null +++ b/src/vault-context.ts @@ -0,0 +1,168 @@ +import type { App } from "obsidian"; + +/** + * Collected vault context summary injected into the AI system prompt. + */ +export interface VaultContext { + vaultName: string; + totalNotes: number; + totalFolders: number; + folderTree: string; + tagTaxonomy: string; + recentFiles: string; +} + +/** + * Build a folder tree string from the vault. + * Produces an indented tree like: + * / + * ├── folder-a/ + * │ ├── subfolder/ + * ├── folder-b/ + */ +function buildFolderTree(app: App): string { + const folders = app.vault.getAllFolders(true); + // Build a map of parent → children folder names + const tree = new Map<string, string[]>(); + + for (const folder of folders) { + if (folder.isRoot()) continue; + const parentPath = folder.parent?.path ?? "/"; + const key = parentPath === "/" || parentPath === "" ? "/" : parentPath; + if (!tree.has(key)) { + tree.set(key, []); + } + tree.get(key)!.push(folder.path); + } + + const lines: string[] = []; + + function walk(path: string, prefix: string): void { + const children = tree.get(path) ?? []; + children.sort(); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const isLast = i === children.length - 1; + const connector = isLast ? "└── " : "├── "; + const childPrefix = isLast ? " " : "│ "; + // Show just the folder name, not the full path + const name = child.split("/").pop() ?? child; + lines.push(`${prefix}${connector}${name}/`); + walk(child, prefix + childPrefix); + } + } + + lines.push("/"); + walk("/", ""); + + return lines.join("\n"); +} + +/** + * Collect all tags in the vault with their usage counts. + * Returns a formatted string like: #tag1 (12), #tag2 (8), ... + */ +function buildTagTaxonomy(app: App): string { + const tagCounts = new Map<string, number>(); + const files = app.vault.getMarkdownFiles(); + + for (const file of files) { + const cache = app.metadataCache.getFileCache(file); + if (cache === null) continue; + + // Inline tags + if (cache.tags !== undefined) { + for (const tagEntry of cache.tags) { + const tag = tagEntry.tag.toLowerCase(); + tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); + } + } + + // Frontmatter tags + if (cache.frontmatter?.tags !== undefined) { + const fmTags = cache.frontmatter.tags; + if (Array.isArray(fmTags)) { + for (const raw of fmTags) { + const tag = typeof raw === "string" + ? (raw.startsWith("#") ? raw.toLowerCase() : `#${raw.toLowerCase()}`) + : ""; + if (tag !== "") { + tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); + } + } + } + } + } + + if (tagCounts.size === 0) { + return "No tags in vault."; + } + + // Sort by count descending + const sorted = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]); + + // Cap at 100 tags to avoid overwhelming context + const maxTags = 100; + const limited = sorted.slice(0, maxTags); + const lines = limited.map(([tag, count]) => `${tag} (${count})`); + const suffix = sorted.length > maxTags + ? `\n...and ${sorted.length - maxTags} more tags.` + : ""; + + return lines.join(", ") + suffix; +} + +/** + * Get the most recently modified files. + */ +function buildRecentFiles(app: App, maxFiles: number): string { + const files = app.vault.getMarkdownFiles(); + + // Sort by modification time descending + const sorted = [...files].sort((a, b) => b.stat.mtime - a.stat.mtime); + const limited = sorted.slice(0, maxFiles); + + if (limited.length === 0) { + return "No notes in vault."; + } + + return limited.map((f) => f.path).join("\n"); +} + +/** + * Collect the full vault context summary. + * This is cheap — all data comes from the metadata cache and vault indexes. + */ +export function collectVaultContext(app: App, maxRecentFiles: number): VaultContext { + const markdownFiles = app.vault.getMarkdownFiles(); + const allFolders = app.vault.getAllFolders(false); + + return { + vaultName: app.vault.getName(), + totalNotes: markdownFiles.length, + totalFolders: allFolders.length, + folderTree: buildFolderTree(app), + tagTaxonomy: buildTagTaxonomy(app), + recentFiles: buildRecentFiles(app, maxRecentFiles), + }; +} + +/** + * Format the vault context into a system prompt block. + */ +export function formatVaultContext(ctx: VaultContext): string { + return ( + "VAULT CONTEXT (auto-injected summary of the user's Obsidian vault):\n\n" + + `Vault name: ${ctx.vaultName}\n` + + `Total notes: ${ctx.totalNotes}\n` + + `Total folders: ${ctx.totalFolders}\n\n` + + "Folder structure:\n" + + "```\n" + + ctx.folderTree + "\n" + + "```\n\n" + + "Tags in use:\n" + + ctx.tagTaxonomy + "\n\n" + + "Recently modified notes:\n" + + ctx.recentFiles + ); +} |
