// @generated — DO NOT EDIT. Source: packages/shared/integrations-common.ts import { existsSync, readFileSync } from "fs"; import { join } from "path"; // --- Types --- export interface ObsidianConfig { vaultPath: string; folder: string; plan: string; filenameFormat?: string; // Custom format string, e.g. '{YYYY}-{MM}-{DD} - {title}' filenameSeparator?: "space" | "dash" | "underscore"; // Replace spaces in filename } export interface BearConfig { plan: string; customTags?: string; tagPosition?: "prepend" | "append"; } export interface OctarineConfig { plan: string; workspace: string; folder: string; } export interface IntegrationResult { success: boolean; error?: string; path?: string; } /** * Detect Obsidian vaults by reading Obsidian's config file * Returns array of vault paths found on the system */ export function detectObsidianVaults(): string[] { try { const home = process.env.HOME || process.env.USERPROFILE || ""; let configPath: string; // Platform-specific config locations if (process.platform === "darwin") { configPath = join( home, "Library/Application Support/obsidian/obsidian.json", ); } else if (process.platform === "win32") { const appData = process.env.APPDATA || join(home, "AppData/Roaming"); configPath = join(appData, "obsidian/obsidian.json"); } else { // Linux configPath = join(home, ".config/obsidian/obsidian.json"); } if (!existsSync(configPath)) { return []; } const configContent = readFileSync(configPath, "utf-8"); const config = JSON.parse(configContent); if (!config.vaults || typeof config.vaults !== "object") { return []; } // Extract vault paths, filter to ones that exist const vaults: string[] = []; for (const vaultId of Object.keys(config.vaults)) { const vault = config.vaults[vaultId]; if (vault.path && existsSync(vault.path)) { vaults.push(vault.path); } } return vaults; } catch { return []; } } // --- Frontmatter and Filename Generation --- /** * Generate frontmatter for the note */ export function generateFrontmatter(tags: string[]): string { const now = new Date().toISOString(); const tagList = tags.map((t) => t.toLowerCase()).join(", "); return `--- created: ${now} source: plannotator tags: [${tagList}] ---`; } /** * Extract title from markdown (first H1 heading) */ export function extractTitle(markdown: string): string { const h1Match = markdown.match( /^#\s+(?:Implementation\s+Plan:|Plan:)?\s*(.+)$/im, ); if (h1Match) { // Clean up the title for use as filename return h1Match[1] .trim() .replace(/[<>:"/\\|?*(){}\[\]#~`]/g, "") // Remove invalid/problematic filename chars .replace(/\s+/g, " ") // Normalize whitespace .trim() // Re-trim after stripping .slice(0, 50); // Limit length } return "Plan"; } /** Default filename format matching original behavior */ export const DEFAULT_FILENAME_FORMAT = "{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}"; /** * Generate filename from a format string with variable substitution. * * Supported variables: * {title} - Plan title from first H1 heading * {YYYY} - 4-digit year * {MM} - 2-digit month (01-12) * {DD} - 2-digit day (01-31) * {Mon} - Abbreviated month name (Jan, Feb, ...) * {D} - Day without leading zero * {HH} - 2-digit hour, 24h (00-23) * {h} - Hour without leading zero, 12h * {hh} - 2-digit hour, 12h (01-12) * {mm} - 2-digit minutes (00-59) * {ss} - 2-digit seconds (00-59) * {ampm} - am/pm * * Default format: '{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}' * Example output: 'User Authentication - Jan 2, 2026 2-30pm.md' */ export function generateFilename( markdown: string, format?: string, separator?: "space" | "dash" | "underscore", ): string { const title = extractTitle(markdown); const now = new Date(); const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; const hour24 = now.getHours(); const hour12 = hour24 % 12 || 12; const ampm = hour24 >= 12 ? "pm" : "am"; const vars: Record = { title, YYYY: String(now.getFullYear()), MM: String(now.getMonth() + 1).padStart(2, "0"), DD: String(now.getDate()).padStart(2, "0"), Mon: months[now.getMonth()], D: String(now.getDate()), HH: String(hour24).padStart(2, "0"), h: String(hour12), hh: String(hour12).padStart(2, "0"), mm: String(now.getMinutes()).padStart(2, "0"), ss: String(now.getSeconds()).padStart(2, "0"), ampm, }; const template = format?.trim() || DEFAULT_FILENAME_FORMAT; const result = template.replace( /\{(\w+)\}/g, (match, key) => vars[key] ?? match, ); // Sanitize: remove characters invalid in filenames let sanitized = result .replace(/[<>:"/\\|?*]/g, "") .replace(/\s+/g, " ") .trim(); // Apply separator preference (replace spaces with dash or underscore) if (separator === "dash") { sanitized = sanitized.replace(/ /g, "-"); } else if (separator === "underscore") { sanitized = sanitized.replace(/ /g, "_"); } return sanitized.endsWith(".md") ? sanitized : `${sanitized}.md`; } // --- Bear Integration --- export function stripH1(plan: string): string { return plan.replace(/^#\s+.+\n?/m, "").trimStart(); } export function buildHashtags( customTags: string | undefined, autoTags: string[], ): string { if (customTags?.trim()) { return customTags .split(",") .map((t) => `#${t.trim()}`) .filter((t) => t !== "#") .join(" "); } return autoTags.map((t) => `#${t}`).join(" "); } export function buildBearContent( body: string, hashtags: string, tagPosition: "prepend" | "append", ): string { return tagPosition === "prepend" ? `${hashtags}\n\n${body}` : `${body}\n\n${hashtags}`; } // --- Octarine Integration --- /** * Generate YAML frontmatter for an Octarine note. * Uses Octarine's property format (list-style tags, Status, Author, Last Edited). */ export function generateOctarineFrontmatter(tags: string[]): string { const now = new Date().toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM const tagLines = tags.map((t) => ` - ${t.toLowerCase()}`).join("\n"); return `---\ntags:\n${tagLines}\nStatus: Draft\nAuthor: plannotator\nLast Edited: ${now}\n---`; }