Skip to main content
Glama

Obsidian MCP Server

by bazylhorsey
PeriodicNotesService.ts•12.7 kB
/** * Service for managing periodic notes (daily, weekly, monthly, yearly) */ import { promises as fs } from 'fs'; import path from 'path'; import type { PeriodicNoteType, PeriodicNotesSettings, PeriodicNoteInfo, DateRange } from '../types/periodic.js'; import { DEFAULT_PERIODIC_NOTES_SETTINGS } from '../types/periodic.js'; import type { VaultOperationResult } from '../types/index.js'; import { TemplateService } from './TemplateService.js'; export class PeriodicNotesService { private settings: PeriodicNotesSettings; private templateService: TemplateService; constructor(settings?: Partial<PeriodicNotesSettings>, templateService?: TemplateService) { this.settings = { daily: { ...DEFAULT_PERIODIC_NOTES_SETTINGS.daily, ...settings?.daily }, weekly: { ...DEFAULT_PERIODIC_NOTES_SETTINGS.weekly, ...settings?.weekly }, monthly: { ...DEFAULT_PERIODIC_NOTES_SETTINGS.monthly, ...settings?.monthly }, yearly: { ...DEFAULT_PERIODIC_NOTES_SETTINGS.yearly, ...settings?.yearly }, }; this.templateService = templateService || new TemplateService(); } /** * Create a periodic note for a specific date */ async createPeriodicNote( vaultPath: string, type: PeriodicNoteType, date?: Date, variables?: Record<string, any> ): Promise<VaultOperationResult<string>> { try { const noteDate = date || new Date(); const config = this.settings[type]; if (!config.enabled) { return { success: false, error: `${type} notes are not enabled` }; } // Generate note path const notePath = this.getPeriodicNotePath(type, noteDate); const fullPath = path.join(vaultPath, notePath); // Check if note already exists try { await fs.access(fullPath); return { success: true, data: notePath }; // Already exists } catch { // Note doesn't exist, continue to create } // Ensure directory exists await fs.mkdir(path.dirname(fullPath), { recursive: true }); let content: string; // Use template if specified if (config.templatePath) { const renderResult = await this.templateService.renderTemplate( vaultPath, config.templatePath, { targetPath: notePath, variables: { ...this.getPeriodicNoteVariables(type, noteDate), ...variables, }, } ); if (!renderResult.success || !renderResult.data) { return { success: false, error: renderResult.error }; } content = renderResult.data.content; } else { // Generate default content content = this.generateDefaultContent(type, noteDate); } // Write file await fs.writeFile(fullPath, content, 'utf-8'); return { success: true, data: notePath }; } catch (error) { return { success: false, error: `Failed to create ${type} note: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Get info about a periodic note */ async getPeriodicNoteInfo( vaultPath: string, type: PeriodicNoteType, date?: Date ): Promise<VaultOperationResult<PeriodicNoteInfo>> { try { const noteDate = date || new Date(); const notePath = this.getPeriodicNotePath(type, noteDate); const fullPath = path.join(vaultPath, notePath); let exists = false; try { await fs.access(fullPath); exists = true; } catch { exists = false; } const info: PeriodicNoteInfo = { type, date: noteDate, path: notePath, title: this.getPeriodicNoteTitle(type, noteDate), exists, }; return { success: true, data: info }; } catch (error) { return { success: false, error: `Failed to get ${type} note info: ${error instanceof Error ? error.message : String(error)}` }; } } /** * List all periodic notes of a type within a date range */ async listPeriodicNotes( vaultPath: string, type: PeriodicNoteType, startDate?: Date, endDate?: Date ): Promise<VaultOperationResult<PeriodicNoteInfo[]>> { try { const config = this.settings[type]; const folderPath = path.join(vaultPath, config.folder); // Check if folder exists try { await fs.access(folderPath); } catch { return { success: true, data: [] }; } const { glob } = await import('glob'); const pattern = path.join(folderPath, '**/*.md'); const files = await glob(pattern); const notes: PeriodicNoteInfo[] = []; for (const file of files) { const relativePath = path.relative(vaultPath, file); const filename = path.basename(file, '.md'); // Try to parse date from filename const date = this.parseDateFromFilename(filename, config.format); if (!date) continue; // Filter by date range if specified if (startDate && date < startDate) continue; if (endDate && date > endDate) continue; notes.push({ type, date, path: relativePath, title: filename, exists: true, }); } // Sort by date descending notes.sort((a, b) => b.date.getTime() - a.date.getTime()); return { success: true, data: notes }; } catch (error) { return { success: false, error: `Failed to list ${type} notes: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Get the date range for a periodic note */ getDateRange(type: PeriodicNoteType, date: Date): DateRange { const start = new Date(date); const end = new Date(date); switch (type) { case 'daily': start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); break; case 'weekly': // Start on Monday const day = start.getDay(); const diff = start.getDate() - day + (day === 0 ? -6 : 1); start.setDate(diff); start.setHours(0, 0, 0, 0); end.setDate(start.getDate() + 6); end.setHours(23, 59, 59, 999); break; case 'monthly': start.setDate(1); start.setHours(0, 0, 0, 0); end.setMonth(end.getMonth() + 1); end.setDate(0); end.setHours(23, 59, 59, 999); break; case 'yearly': start.setMonth(0, 1); start.setHours(0, 0, 0, 0); end.setMonth(11, 31); end.setHours(23, 59, 59, 999); break; } return { start, end }; } /** * Generate the path for a periodic note */ private getPeriodicNotePath(type: PeriodicNoteType, date: Date): string { const config = this.settings[type]; const filename = this.formatDate(date, config.format); return path.join(config.folder, `${filename}.md`); } /** * Generate the title for a periodic note */ private getPeriodicNoteTitle(type: PeriodicNoteType, date: Date): string { const config = this.settings[type]; return this.formatDate(date, config.format); } /** * Get template variables specific to periodic notes */ private getPeriodicNoteVariables(type: PeriodicNoteType, date: Date): Record<string, any> { const range = this.getDateRange(type, date); const vars: Record<string, any> = { type, date: this.formatDate(date, 'YYYY-MM-DD'), startDate: this.formatDate(range.start, 'YYYY-MM-DD'), endDate: this.formatDate(range.end, 'YYYY-MM-DD'), }; switch (type) { case 'daily': vars.weekday = this.getWeekday(date); break; case 'weekly': vars.week = this.getWeekNumber(date); vars.year = date.getFullYear(); break; case 'monthly': vars.month = this.getMonthName(date); vars.monthNumber = date.getMonth() + 1; vars.year = date.getFullYear(); break; case 'yearly': vars.year = date.getFullYear(); break; } return vars; } /** * Generate default content for a periodic note */ private generateDefaultContent(type: PeriodicNoteType, date: Date): string { const vars = this.getPeriodicNoteVariables(type, date); const title = this.getPeriodicNoteTitle(type, date); let content = `---\ndate: ${vars.date}\ntype: ${type}\n---\n\n`; content += `# ${title}\n\n`; switch (type) { case 'daily': content += `## Tasks\n\n- [ ] \n\n## Notes\n\n`; break; case 'weekly': content += `## Week ${vars.week} Overview\n\n`; content += `**Period:** ${vars.startDate} to ${vars.endDate}\n\n`; content += `## Goals\n\n- \n\n## Reflection\n\n`; break; case 'monthly': content += `## ${vars.month} ${vars.year}\n\n`; content += `## Goals\n\n- \n\n## Highlights\n\n`; break; case 'yearly': content += `## ${vars.year} Overview\n\n`; content += `## Goals\n\n- \n\n## Review\n\n`; break; } return content; } /** * Format date according to format string */ private formatDate(date: Date, format: string): string { const pad = (n: number) => String(n).padStart(2, '0'); let result = format; // Handle literals in square brackets const literals: string[] = []; result = result.replace(/\[([^\]]+)\]/g, (_match, literal) => { literals.push(literal); return `__LITERAL_${literals.length - 1}__`; }); const replacements: Record<string, string> = { 'YYYY': String(date.getFullYear()), 'YY': String(date.getFullYear()).slice(-2), 'MM': pad(date.getMonth() + 1), 'M': String(date.getMonth() + 1), 'DD': pad(date.getDate()), 'D': String(date.getDate()), 'WW': pad(this.getWeekNumber(date)), 'W': String(this.getWeekNumber(date)), }; for (const [token, value] of Object.entries(replacements)) { result = result.replace(new RegExp(token, 'g'), value); } // Restore literals result = result.replace(/__LITERAL_(\d+)__/g, (_match, index) => { return literals[parseInt(index)]; }); return result; } /** * Parse date from filename using format */ private parseDateFromFilename(filename: string, _format: string): Date | null { try { // Simple parsing for common formats // YYYY-MM-DD const dailyMatch = filename.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (dailyMatch) { return new Date(parseInt(dailyMatch[1]), parseInt(dailyMatch[2]) - 1, parseInt(dailyMatch[3])); } // YYYY-WXX const weeklyMatch = filename.match(/^(\d{4})-W(\d{2})$/); if (weeklyMatch) { const year = parseInt(weeklyMatch[1]); const week = parseInt(weeklyMatch[2]); return this.getDateFromWeek(year, week); } // YYYY-MM const monthlyMatch = filename.match(/^(\d{4})-(\d{2})$/); if (monthlyMatch) { return new Date(parseInt(monthlyMatch[1]), parseInt(monthlyMatch[2]) - 1, 1); } // YYYY const yearlyMatch = filename.match(/^(\d{4})$/); if (yearlyMatch) { return new Date(parseInt(yearlyMatch[1]), 0, 1); } return null; } catch { return null; } } /** * Get date from ISO week number */ private getDateFromWeek(year: number, week: number): Date { const simple = new Date(year, 0, 1 + (week - 1) * 7); const dow = simple.getDay(); const ISOweekStart = simple; if (dow <= 4) ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1); else ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay()); return ISOweekStart; } /** * Get weekday name */ private getWeekday(date: Date): string { const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return days[date.getDay()]; } /** * Get month name */ private getMonthName(date: Date): string { const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; return months[date.getMonth()]; } /** * Get ISO week number */ private getWeekNumber(date: Date): number { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bazylhorsey/obsidian-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server