// pi-wiki: Cross-linker - auto-discover and insert wikilinks import * as fs from "fs"; import * as path from "path"; const WIKI_DIR = process.env.PI_WIKI_DIR || path.join(process.env.HOME || "", "pi-wiki"); const PAGES_DIR = path.join(WIKI_DIR, "wiki"); function slugify(text: string): string { return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function getAllPageTitles(): string[] { if (!fs.existsSync(PAGES_DIR)) return []; return fs.readdirSync(PAGES_DIR) .filter(f => f.endsWith(".md")) .map(f => f.replace(/\.md$/, "")); } export async function crosslink(): Promise { const titles = getAllPageTitles(); if (titles.length === 0) return "❌ No wiki pages found"; let totalLinks = 0; const results: string[] = []; for (const title of titles) { const filePath = path.join(PAGES_DIR, title + ".md"); let content = fs.readFileSync(filePath, "utf8"); const originalContent = content; // Skip pages without body (just frontmatter) const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/); if (!bodyMatch) continue; const body = bodyMatch[1]; // Find all existing wikilinks const existingLinks = new Set(); let match; const linkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; while ((match = linkRegex.exec(body)) !== null) { existingLinks.add(slugify(match[1])); } // Check for mentions without wikilinks let linksAdded = 0; for (const otherTitle of titles) { if (otherTitle === title) continue; if (existingLinks.has(slugify(otherTitle))) continue; // Case-insensitive mention without wikilink const mentionRegex = new RegExp(`\\b${otherTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, "gi"); if (mentionRegex.test(body)) { // Find exact position to insert wikilink const firstMention = body.search(mentionRegex); if (firstMention >= 0) { // Replace first mention with wikilink const before = body.slice(0, firstMention); const after = body.slice(firstMention + otherTitle.length); body = before + "[[" + otherTitle + "]]" + after; existingLinks.add(slugify(otherTitle)); // prevent double-linking linksAdded++; } } } if (linksAdded > 0) { // Rebuild full content const frontmatterMatch = content.match(/^(---\n[\s\S]*?\n---)\n/); content = frontmatterMatch ? frontmatterMatch[1] + "\n" + body : body; // Update frontmatter if (frontmatterMatch) { const front = frontmatterMatch[1]; const updatedFront = front.includes("updated:") ? front.replace(/updated:.*/m, `updated: ${new Date().toISOString().split("T")[0]}`) : front.replace(/---$/, `updated: ${new Date().toISOString().split("T")[0]}\n---`); content = updatedFront + "\n" + body; } fs.writeFileSync(filePath, content); results.push(`+${linksAdded} links in [[${title}]]`); totalLinks += linksAdded; } } if (totalLinks === 0) { return "✅ No new links needed - all pages already connected!"; } return `## 🔗 Cross-link Results\n\n${results.join("\n")}\n\n**Total: ${totalLinks} links added**`; }