// pi-wiki: Obsidian Integration - vault linking, CLI, graph export import * as fs from "fs"; import * as path from "path"; const HOME = process.env.USERPROFILE || process.env.HOME || ""; const CONFIG_PATH = path.join(HOME, ".pi-wiki", "config"); interface ObsidianConfig { vault_path: string; obsidian_vault?: string; link_format: "wikilink" | "markdown"; } function loadConfig(): ObsidianConfig { const defaults: ObsidianConfig = { vault_path: path.join(HOME, "pi-wiki"), link_format: "wikilink", }; if (fs.existsSync(CONFIG_PATH)) { try { return { ...defaults, ...JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")) }; } catch {} } return defaults; } function saveConfig(config: ObsidianConfig) { const dir = path.dirname(CONFIG_PATH); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); } // ── Vault Operations ────────────────────────────────────────────────────── export function getVaultPath(): string { const config = loadConfig(); return config.obsidian_vault || config.vault_path; } export function setObsidianVault(vaultPath: string): string { const config = loadConfig(); config.obsidian_vault = vaultPath; saveConfig(config); return `✅ Obsidian vault set to: ${vaultPath}`; } export function getVaultStatus(): string { const config = loadConfig(); const vault = config.obsidian_vault || config.vault_path; const exists = fs.existsSync(vault); let pages = 0; if (exists) { const wikiDir = path.join(vault, "wiki"); if (fs.existsSync(wikiDir)) { pages = fs.readdirSync(wikiDir).filter(f => f.endsWith(".md")).length; } else if (fs.existsSync(vault)) { pages = fs.readdirSync(vault).filter(f => f.endsWith(".md")).length; } } return `## 📚 Vault Status **Path:** ${vault} **Exists:** ${exists ? "✅" : "❌"} **Pages:** ${pages} **Obsidian vault:** ${config.obsidian_vault ? "Yes" : "No (using pi-wiki's own vault)"}`; } // ── Obsidian CLI Integration ───────────────────────────────────────────── export async function obsidianRead(notePath: string): Promise { // Try to use obsidian CLI if available const { execSync } = require("child_process"); try { const result = execSync(`obsidian read file="${notePath}"`, { encoding: "utf8", timeout: 5000 }); return result; } catch { // Fallback to direct file read const vaultPath = getVaultPath(); const filePath = path.join(vaultPath, notePath); if (fs.existsSync(filePath)) { return fs.readFileSync(filePath, "utf8"); } return `❌ Note not found: ${notePath}`; } } export async function obsidianSearch(query: string): Promise { const { execSync } = require("child_process"); try { const result = execSync(`obsidian search query="${query}"`, { encoding: "utf8", timeout: 10000 }); return result; } catch { // Fallback to grep const vaultPath = getVaultPath(); const { execSync: exec } = require("child_process"); try { const result = exec(`grep -r "${query}" "${vaultPath}" --include="*.md" -l 2>/dev/null | head -10`, { encoding: "utf8" }); return result || "No matches found"; } catch { return "❌ Search failed. Install obsidian CLI or add content first."; } } } // ── Graph Export ───────────────────────────────────────────────────────── interface GraphNode { id: string; label: string; tags: string[]; } interface GraphLink { source: string; target: string; } export function exportGraphJson(): string { const vaultPath = getVaultPath(); const wikiDir = path.join(vaultPath, "wiki"); if (!fs.existsSync(wikiDir)) { return "❌ No wiki directory found"; } const nodes: GraphNode[] = []; const links: GraphLink[] = []; const linkSet = new Set(); const files = fs.readdirSync(wikiDir).filter(f => f.endsWith(".md")); for (const file of files) { const content = fs.readFileSync(path.join(wikiDir, file), "utf8"); const titleMatch = content.match(/^title:\s*(.+)$/m); const tagsMatch = content.match(/^tags:\s*\[(.*?)\]/m); const id = file.replace(/\.md$/, ""); const label = titleMatch ? titleMatch[1] : id; const tags = tagsMatch ? tagsMatch[1].split(",").map(t => t.trim()) : []; nodes.push({ id, label, tags }); // Find wikilinks const linkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; let match; while ((match = linkRegex.exec(content)) !== null) { const target = match[1].toLowerCase().replace(/[^a-z0-9]+/g, "-"); const linkKey = `${id}→${target}`; if (!linkSet.has(linkKey) && target !== id) { links.push({ source: id, target }); linkSet.add(linkKey); } } } const graph = { nodes: nodes.map(n => ({ id: n.id, label: n.label, tags: n.tags, links: links.filter(l => l.source === n.id || l.target === n.id).length })), links: links.map(l => ({ source: l.source, target: l.target })) }; // Save to vault const graphPath = path.join(vaultPath, ".obsidian", "graph.json"); const obsidianDir = path.dirname(graphPath); if (!fs.existsSync(obsidianDir)) fs.mkdirSync(obsidianDir, { recursive: true }); fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2)); return `✅ Graph exported to ${graphPath}\n\n**Nodes:** ${nodes.length}\n**Links:** ${links.length}`; } export function exportGraphHtml(): string { const vaultPath = getVaultPath(); const wikiDir = path.join(vaultPath, "wiki"); if (!fs.existsSync(wikiDir)) { return "❌ No wiki directory found"; } // Build nodes and links data const files = fs.readdirSync(wikiDir).filter(f => f.endsWith(".md")); const nodeMap = new Map(); for (const file of files) { const content = fs.readFileSync(path.join(wikiDir, file), "utf8"); const titleMatch = content.match(/^title:\s*(.+)$/m); const tagsMatch = content.match(/^tags:\s*\[(.*?)\]/m); const id = file.replace(/\.md$/, ""); nodeMap.set(id, { id, label: titleMatch ? titleMatch[1] : id, tags: tagsMatch ? tagsMatch[1].split(",").map(t => t.trim()) : [] }); } const links: {source: string, target: string}[] = []; const linkSet = new Set(); for (const file of files) { const content = fs.readFileSync(path.join(wikiDir, file), "utf8"); const linkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; let match; while ((match = linkRegex.exec(content)) !== null) { const target = match[1].toLowerCase().replace(/[^a-z0-9]+/g, "-"); const linkKey = `${file.replace(/\.md$/, "")}→${target}`; if (!linkSet.has(linkKey) && nodeMap.has(target)) { links.push({ source: file.replace(/\.md$/, ""), target }); linkSet.add(linkKey); } } } const html = ` Pi-Wiki Graph `; const htmlPath = path.join(vaultPath, "wiki-graph.html"); fs.writeFileSync(htmlPath, html); return `✅ Interactive graph saved to ${htmlPath}\n\nOpen in browser to explore your wiki!`; } // ── Daily Update ───────────────────────────────────────────────────────── export function dailyUpdate(): string { const vaultPath = getVaultPath(); const logFile = path.join(vaultPath, "log.md"); const lines = [`\n## [${new Date().toISOString().split("T")[0]}] daily-update\n`]; // Check pages needing review const wikiDir = path.join(vaultPath, "wiki"); if (fs.existsSync(wikiDir)) { const files = fs.readdirSync(wikiDir).filter(f => f.endsWith(".md")); lines.push(`**Pages:** ${files.length}`); // Check drafts const draftsDir = path.join(vaultPath, "_drafts"); if (fs.existsSync(draftsDir)) { const drafts = fs.readdirSync(draftsDir).filter(f => f.endsWith(".md") || f.endsWith(".txt")); if (drafts.length > 0) { lines.push(`**Drafts:** ${drafts.length} (promote with /wiki-promote)`); } } // Check raw const rawDir = path.join(vaultPath, "_raw"); if (fs.existsSync(rawDir)) { const raw = fs.readdirSync(rawDir); if (raw.length > 0) { lines.push(`**Raw:** ${raw.length} (ingest with /wiki-lint)`); } } } fs.appendFileSync(logFile, lines.join("\n")); return "## 🌅 Daily Update Complete\n\n" + lines.slice(1).join("\n"); }