UNPKG

@notes-sync/service

Version:

Background service for AI-powered note synchronization

1,012 lines (1,003 loc) 46.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NoteInteractor = void 0; const daily_template_config_1 = require("./config/daily-template.config"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const logger_1 = require("./logger"); // Section constants for parsing const TODAY_SECTION = `**Today's Focus**`; const NOTE_SECTION = `**Notes**`; const DONE_SECTION = `**Done**`; const TOMORROW_SECTION = `**Tomorrow**`; class NoteInteractor { constructor(notesDir, noteFile, aiService) { this.notesDir = notesDir; this.noteFile = noteFile; this.aiService = aiService; } async writeNewDay() { try { let quote = 'Progress over perfection'; let author = 'Unknown'; // Try to generate AI quote if available if (this.aiService?.isDailyQuotesEnabled()) { try { // Get context from recent notes for better quote generation const context = this.getRecentNotesContext(); const aiQuote = await this.aiService.generateQuoteWithFallback(context); if (aiQuote) { quote = aiQuote.quote; author = aiQuote.author; logger_1.Logger.log(`Generated AI quote: "${quote}" - ${author}`); } } catch (error) { logger_1.Logger.error(`Failed to generate AI quote, using fallback: ${error.message}`); // Continue with default quote - don't let AI failures break daily creation } } const template = (0, daily_template_config_1.getDailyTemplate)(new Date().toLocaleDateString(), quote, author); this.append('\n\n' + template); logger_1.Logger.log(`Daily section created for ${new Date().toLocaleDateString()}`); } catch (error) { logger_1.Logger.error(`Failed to write daily section: ${error.message}`); } } // Get recent notes context for AI quote generation getRecentNotesContext() { try { const recentDays = this.getPreviousDays(3); // Get last 3 days for context const context = recentDays .map(day => this.getNotes(day)) .filter(notes => notes.trim().length > 0) .join(' ') .substring(0, 300); // Limit context length return context; } catch (error) { logger_1.Logger.error(`Failed to get recent notes context: ${error.message}`); return ''; } } append(text) { fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), text, { flag: 'a', }); } // Helper method to read the full notes file readNotesFile() { const filePath = path_1.default.join(this.notesDir, this.noteFile); if (!fs_1.default.existsSync(filePath)) { return ''; } return fs_1.default.readFileSync(filePath, 'utf8'); } // Helper method to parse days from the notes file parseDays(content) { const days = new Map(); // Split by headers that start with # const dayBlocks = content.split(/^# /m).filter(block => block.trim()); for (const block of dayBlocks) { const lines = block.split('\n'); const dateMatch = lines[0].match(/^(.+?)$/); if (dateMatch) { const dateStr = dateMatch[1].trim(); const dayContent = '# ' + block; days.set(dateStr, dayContent); } } return days; } // Helper method to extract section content extractSection(text, sectionHeader, nextSectionHeader) { const sectionIndex = text.indexOf(sectionHeader); if (sectionIndex === -1) return ''; const startIndex = sectionIndex + sectionHeader.length; let endIndex = text.length; if (nextSectionHeader) { const nextIndex = text.indexOf(nextSectionHeader, startIndex); if (nextIndex !== -1) { endIndex = nextIndex; } } return text.substring(startIndex, endIndex).trim(); } // Helper method to find today's date section in the file getTodaySection() { const content = this.readNotesFile(); const todayDate = new Date().toLocaleDateString(); const todayHeaderRegex = new RegExp(`^# ${todayDate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm'); const todayMatch = content.match(todayHeaderRegex); if (!todayMatch) return null; const startPos = todayMatch.index; // Find the next day section or end of file const nextDayRegex = /^# \d+\/\d+\/\d+/m; const remainingContent = content.substring(startPos + todayMatch[0].length); const nextDayMatch = remainingContent.match(nextDayRegex); const endPos = nextDayMatch ? startPos + todayMatch[0].length + nextDayMatch.index : content.length; const dayContent = content.substring(startPos, endPos); return { content: dayContent, startPos, endPos }; } getPreviousDays(days) { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); // Get dates and sort them in descending order (most recent first) const sortedDates = Array.from(dayMap.keys()).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); // Take the requested number of days (excluding today) const today = new Date().toLocaleDateString(); const previousDates = sortedDates .filter(date => date !== today) .slice(0, days); return previousDates.map(date => dayMap.get(date) || ''); } catch (error) { logger_1.Logger.error(`Failed to get previous days: ${error.message}`); return []; } } getDate(date) { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); const dateStr = date.toLocaleDateString(); return dayMap.get(dateStr) || ''; } catch (error) { logger_1.Logger.error(`Failed to get date ${date.toLocaleDateString()}: ${error.message}`); return ''; } } getTodos(dayText) { try { const todosSection = this.extractSection(dayText, TODAY_SECTION, NOTE_SECTION); // Extract checkbox items (- [ ] or - [x]) const todoRegex = /^- \[[ x]\] (.+)$/gm; const todos = []; let match; while ((match = todoRegex.exec(todosSection)) !== null) { todos.push(match[1].trim()); } return todos; } catch (error) { logger_1.Logger.error(`Failed to extract todos: ${error.message}`); return []; } } getNotes(dayText) { try { return this.extractSection(dayText, NOTE_SECTION, DONE_SECTION); } catch (error) { logger_1.Logger.error(`Failed to extract notes: ${error.message}`); return ''; } } async addTodo(text) { try { const todaySection = this.getTodaySection(); if (!todaySection) { logger_1.Logger.error("Today's section not found. Creating new day first."); await this.writeNewDay(); return this.addTodo(text); } const content = this.readNotesFile(); const todayFocusStart = todaySection.content.indexOf(TODAY_SECTION); if (todayFocusStart === -1) { logger_1.Logger.error("Today's Focus section not found"); return; } // Find the end of the Today's Focus section const notesSectionStart = todaySection.content.indexOf(NOTE_SECTION, todayFocusStart); if (notesSectionStart === -1) { logger_1.Logger.error('Notes section not found'); return; } // Find the last todo item in the Today's Focus section const focusSection = todaySection.content.substring(todayFocusStart, notesSectionStart); const todoRegex = /^- \[[ x]\] .+$/gm; let lastTodoEnd = todayFocusStart + TODAY_SECTION.length; let match; while ((match = todoRegex.exec(focusSection)) !== null) { lastTodoEnd = todayFocusStart + match.index + match[0].length; } // Insert the new todo const newTodo = `\n- [ ] ${text}`; const insertPosition = todaySection.startPos + lastTodoEnd; const newContent = content.substring(0, insertPosition) + newTodo + content.substring(insertPosition); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), newContent); logger_1.Logger.log(`Added todo: ${text}`); } catch (error) { logger_1.Logger.error(`Failed to add todo: ${error.message}`); } } async addNote(text) { try { const todaySection = this.getTodaySection(); if (!todaySection) { logger_1.Logger.error("Today's section not found. Creating new day first."); await this.writeNewDay(); return this.addNote(text); } const content = this.readNotesFile(); const notesSectionStart = todaySection.content.indexOf(NOTE_SECTION); if (notesSectionStart === -1) { logger_1.Logger.error('Notes section not found'); return; } // Find the end of the Notes section (before Done section) const doneSectionStart = todaySection.content.indexOf(DONE_SECTION, notesSectionStart); if (doneSectionStart === -1) { logger_1.Logger.error('Done section not found'); return; } // Find where to insert the note (at the end of the Notes section) const notesSection = todaySection.content.substring(notesSectionStart + NOTE_SECTION.length, doneSectionStart); // Find the last non-empty line in the notes section const noteLines = notesSection.split('\n'); let lastContentIndex = -1; for (let i = noteLines.length - 1; i >= 0; i--) { if (noteLines[i].trim()) { lastContentIndex = i; break; } } // Calculate insertion position const basePosition = todaySection.startPos + notesSectionStart + NOTE_SECTION.length; let insertPosition; let newNoteText; if (lastContentIndex === -1) { // No existing notes, insert right after the header insertPosition = basePosition; newNoteText = `\n\n${text}`; } else { // Insert after existing notes const contentUpToLastLine = noteLines .slice(0, lastContentIndex + 1) .join('\n'); insertPosition = basePosition + contentUpToLastLine.length; newNoteText = `\n\n${text}`; } const newContent = content.substring(0, insertPosition) + newNoteText + content.substring(insertPosition); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), newContent); logger_1.Logger.log(`Added note: ${text.substring(0, 50)}...`); } catch (error) { logger_1.Logger.error(`Failed to add note: ${error.message}`); } } markTodoComplete(todoText) { try { const content = this.readNotesFile(); const todoPattern = new RegExp(`^(- \\[ \\] )(${todoText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})$`, 'gm'); if (!todoPattern.test(content)) { logger_1.Logger.error(`Todo not found: ${todoText}`); return false; } const updatedContent = content.replace(todoPattern, '- [x] $2'); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), updatedContent); logger_1.Logger.log(`Marked todo complete: ${todoText}`); return true; } catch (error) { logger_1.Logger.error(`Failed to mark todo complete: ${error.message}`); return false; } } deleteTodo(todoText) { try { const content = this.readNotesFile(); // Match both incomplete and complete todos const todoPattern = new RegExp(`^- \\[[x ]\\] ${todoText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'gm'); if (!todoPattern.test(content)) { logger_1.Logger.error(`Todo not found: ${todoText}`); return false; } // Remove the entire line including newlines const updatedContent = content.replace(todoPattern, ''); // Clean up any double newlines that might result const cleanedContent = updatedContent.replace(/\n\n\n+/g, '\n\n'); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), cleanedContent); logger_1.Logger.log(`Deleted todo: ${todoText}`); return true; } catch (error) { logger_1.Logger.error(`Failed to delete todo: ${error.message}`); return false; } } searchNotes(query, daysBack = 30) { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); const results = []; // Get dates and sort them in descending order const sortedDates = Array.from(dayMap.keys()).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); // Search through the specified number of days const searchDates = sortedDates.slice(0, daysBack); const searchRegex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); for (const date of searchDates) { const dayContent = dayMap.get(date) || ''; const lines = dayContent.split('\n'); for (let i = 0; i < lines.length; i++) { if (searchRegex.test(lines[i])) { // Get context: 1 line before and after the match const contextStart = Math.max(0, i - 1); const contextEnd = Math.min(lines.length, i + 2); const context = lines.slice(contextStart, contextEnd).join('\n'); results.push({ date, context: context.trim(), }); } } } logger_1.Logger.log(`Search for "${query}" found ${results.length} results`); return results; } catch (error) { logger_1.Logger.error(`Failed to search notes: ${error.message}`); return []; } } getIncompleteTodos(daysBack = 7) { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); const incompleteTodos = []; // Get dates and sort them in descending order const sortedDates = Array.from(dayMap.keys()).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); // Check the specified number of days const checkDates = sortedDates.slice(0, daysBack); for (const date of checkDates) { const dayContent = dayMap.get(date) || ''; const todos = this.getTodos(dayContent); // Get incomplete todos (those that start with [ ]) const incompleteRegex = /^- \[ \] (.+)$/gm; const todayFocusSection = this.extractSection(dayContent, TODAY_SECTION, NOTE_SECTION); let match; while ((match = incompleteRegex.exec(todayFocusSection)) !== null) { incompleteTodos.push({ date, todo: match[1].trim(), }); } } logger_1.Logger.log(`Found ${incompleteTodos.length} incomplete todos from last ${daysBack} days`); return incompleteTodos; } catch (error) { logger_1.Logger.error(`Failed to get incomplete todos: ${error.message}`); return []; } } archiveCompletedTodos() { try { const todaySection = this.getTodaySection(); if (!todaySection) { logger_1.Logger.error("Today's section not found"); return 0; } const content = this.readNotesFile(); const todayFocusSection = this.extractSection(todaySection.content, TODAY_SECTION, NOTE_SECTION); const doneSection = this.extractSection(todaySection.content, DONE_SECTION, TOMORROW_SECTION); // Find completed todos in Today's Focus const completedTodoRegex = /^- \[x\] (.+)$/gm; const completedTodos = []; let match; while ((match = completedTodoRegex.exec(todayFocusSection)) !== null) { completedTodos.push(match[1].trim()); } if (completedTodos.length === 0) { logger_1.Logger.log('No completed todos to archive'); return 0; } // Remove completed todos from Today's Focus section let updatedTodayFocus = todayFocusSection; for (const todo of completedTodos) { const todoPattern = new RegExp(`^- \\[x\\] ${todo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm'); updatedTodayFocus = updatedTodayFocus.replace(todoPattern, ''); } // Clean up extra newlines updatedTodayFocus = updatedTodayFocus.replace(/\n\n+/g, '\n\n').trim(); // Add completed todos to Done section const archivedTodos = completedTodos.map(todo => `- ${todo}`).join('\n'); const updatedDoneSection = doneSection ? `${doneSection}\n${archivedTodos}` : archivedTodos; // Rebuild the day's content const todayFocusStart = todaySection.content.indexOf(TODAY_SECTION); const notesSectionStart = todaySection.content.indexOf(NOTE_SECTION); const doneSectionStart = todaySection.content.indexOf(DONE_SECTION); const tomorrowSectionStart = todaySection.content.indexOf(TOMORROW_SECTION); let newDayContent = todaySection.content.substring(0, todayFocusStart + TODAY_SECTION.length); newDayContent += `\n\n${updatedTodayFocus}\n\n`; newDayContent += todaySection.content.substring(notesSectionStart, doneSectionStart + DONE_SECTION.length); newDayContent += `\n\n${updatedDoneSection}\n\n`; newDayContent += todaySection.content.substring(tomorrowSectionStart); // Replace the day's content in the full file const newContent = content.substring(0, todaySection.startPos) + newDayContent + content.substring(todaySection.endPos); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), newContent); logger_1.Logger.log(`Archived ${completedTodos.length} completed todos`); return completedTodos.length; } catch (error) { logger_1.Logger.error(`Failed to archive completed todos: ${error.message}`); return 0; } } formatDocument() { try { const content = this.readNotesFile(); if (!content.trim()) { logger_1.Logger.log('No content to format'); return { formatted: false, changesMade: [] }; } const changes = []; let formattedContent = content; // 1. Remove trailing whitespace from all lines const beforeTrailing = formattedContent; formattedContent = formattedContent.replace(/[ \t]+$/gm, ''); if (beforeTrailing !== formattedContent) { changes.push('Removed trailing whitespace'); } // 2. Normalize multiple consecutive blank lines (max 2 blank lines) but preserve content integrity const beforeBlankLines = formattedContent; formattedContent = formattedContent.replace(/\n{4,}/g, '\n\n\n'); if (beforeBlankLines !== formattedContent) { changes.push('Normalized excessive blank lines'); } // 2.5. Fix any accidentally broken quotes and dates const beforeFixes = formattedContent; // Fix broken quotes (rejoin lines that should be together without adding extra spaces) formattedContent = formattedContent.replace(/(_[^_\n]*)\n([^_\n]*_)/g, '$1$2'); // Fix broken dates (rejoin date parts) formattedContent = formattedContent.replace(/(# \d{1,2}\/\d{1,2}\/\d{2,3})\n(\d)/gm, '$1$2'); if (beforeFixes !== formattedContent) { changes.push('Fixed broken content lines'); } // 3. Ensure consistent spacing around headers (but don't break dates) const beforeHeaders = formattedContent; // Add blank line before headers (except at start of file) - but only for complete date lines formattedContent = formattedContent.replace(/(?<!^)(?<!\n\n)(\n)(# \d{1,2}\/\d{1,2}\/\d{4})/gm, '\n\n$2'); // Ensure blank line after complete date headers formattedContent = formattedContent.replace(/(^# \d{1,2}\/\d{1,2}\/\d{4})(?!\n\n)/gm, '$1\n'); if (beforeHeaders !== formattedContent) { changes.push('Standardized header spacing'); } // 4. Standardize todo formatting (ensure space after checkbox) const beforeTodos = formattedContent; formattedContent = formattedContent.replace(/^-\s*\[([x ])\]([^ ])/gm, '- [$1] $2'); formattedContent = formattedContent.replace(/^-\s*\[([x ])\]\s+/gm, '- [$1] '); if (beforeTodos !== formattedContent) { changes.push('Standardized todo formatting'); } // 5. Ensure consistent bullet point formatting const beforeBullets = formattedContent; formattedContent = formattedContent.replace(/^-([^ ])/gm, '- $1'); formattedContent = formattedContent.replace(/^-\s{2,}/gm, '- '); if (beforeBullets !== formattedContent) { changes.push('Standardized bullet points'); } // 6. Standardize section spacing (ensure double line break after section headers) const beforeSections = formattedContent; const sectionHeaders = [ TODAY_SECTION, NOTE_SECTION, DONE_SECTION, TOMORROW_SECTION, ]; for (const section of sectionHeaders) { const sectionRegex = new RegExp(`(\\*\\*${section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\*\\*)(?!\\n\\n)`, 'g'); formattedContent = formattedContent.replace(sectionRegex, '$1\n'); } if (beforeSections !== formattedContent) { changes.push('Standardized section spacing'); } // 7. Clean up quote formatting (ensure proper spacing around quotes, but don't break quote content) const beforeQuotes = formattedContent; // Fix malformed quotes (remove space after opening underscore) formattedContent = formattedContent.replace(/(_ )([^_]*_)/g, '_$2'); // Only add spacing after quotes that are followed by section headers or other content, but preserve quote integrity formattedContent = formattedContent.replace(/(_[^_\n]+_)(\s*)(\*\*)/gm, '$1\n\n$3'); if (beforeQuotes !== formattedContent) { changes.push('Cleaned quote formatting'); } // 8. Ensure file ends with single newline const beforeEnding = formattedContent; formattedContent = formattedContent.replace(/\n*$/, '\n'); if (beforeEnding !== formattedContent) { changes.push('Normalized file ending'); } // 9. Remove empty todo items and clean up orphaned formatting const beforeEmpty = formattedContent; formattedContent = formattedContent.replace(/^- \[ \]\s*$/gm, ''); formattedContent = formattedContent.replace(/^- \[x\]\s*$/gm, ''); if (beforeEmpty !== formattedContent) { changes.push('Removed empty todo items'); } // 10. Final cleanup - remove any triple+ newlines and fix any remaining broken content formattedContent = formattedContent.replace(/\n{4,}/g, '\n\n\n'); // Final pass: ensure quotes and dates weren't accidentally broken by any of the above rules formattedContent = formattedContent.replace(/(_[^_\n]*)\n([^_\n]*_)/g, '$1$2'); formattedContent = formattedContent.replace(/(# \d{1,2}\/\d{1,2}\/\d{2,3})\n(\d)/gm, '$1$2'); if (changes.length > 0) { fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), formattedContent); logger_1.Logger.log(`Document formatted with ${changes.length} changes: ${changes.join(', ')}`); return { formatted: true, changesMade: changes }; } else { logger_1.Logger.log('Document already properly formatted'); return { formatted: false, changesMade: [] }; } } catch (error) { logger_1.Logger.error(`Failed to format document: ${error.message}`); return { formatted: false, changesMade: [] }; } } // Advanced formatter for specific cleanup scenarios formatSection(sectionName) { try { const todaySection = this.getTodaySection(); if (!todaySection) { logger_1.Logger.error("Today's section not found"); return false; } const content = this.readNotesFile(); let updatedSection = todaySection.content; // Format specific section based on name switch (sectionName) { case 'todos': // Clean up Today's Focus section const focusSection = this.extractSection(updatedSection, TODAY_SECTION, NOTE_SECTION); let cleanFocus = focusSection .replace(/^- \[ \]\s*$/gm, '') // Remove empty todos .replace(/^-\s*\[([x ])\]\s+/gm, '- [$1] ') // Standardize formatting .replace(/\n{3,}/g, '\n\n'); // Remove excessive blank lines // Replace the section updatedSection = updatedSection.replace(new RegExp(`(${TODAY_SECTION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})([\\s\\S]*?)(?=${NOTE_SECTION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`), `$1\n\n${cleanFocus.trim()}\n\n`); break; case 'notes': // Clean up Notes section const notesSection = this.extractSection(updatedSection, NOTE_SECTION, DONE_SECTION); let cleanNotes = notesSection .replace(/[ \t]+$/gm, '') // Remove trailing whitespace .replace(/\n{4,}/g, '\n\n\n') // Limit consecutive blank lines .trim(); // Replace the section updatedSection = updatedSection.replace(new RegExp(`(${NOTE_SECTION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})([\\s\\S]*?)(?=${DONE_SECTION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`), `$1\n\n${cleanNotes}\n\n`); break; default: logger_1.Logger.error(`Unknown section: ${sectionName}`); return false; } // Replace the day's content in the full file const newContent = content.substring(0, todaySection.startPos) + updatedSection + content.substring(todaySection.endPos); fs_1.default.writeFileSync(path_1.default.join(this.notesDir, this.noteFile), newContent); logger_1.Logger.log(`Formatted ${sectionName} section`); return true; } catch (error) { logger_1.Logger.error(`Failed to format section ${sectionName}: ${error.message}`); return false; } } // Validation helper to check common formatting issues validateFormatting() { try { const content = this.readNotesFile(); const issues = []; // Check for broken dates const brokenDatePattern = /(# \d{1,2}\/\d{1,2}\/\d{2,3})\n(\d)/gm; if (brokenDatePattern.test(content)) { issues.push('Found broken date headers'); } // Check for broken quotes const brokenQuotePattern = /(_[^_\n]*)\n([^_\n]*_)/g; if (brokenQuotePattern.test(content)) { issues.push('Found broken quote formatting'); } // Check for malformed quotes (space after opening underscore) const malformedQuotePattern = /(_ [^_]*_)/g; if (malformedQuotePattern.test(content)) { issues.push('Found quotes with incorrect spacing'); } // Check for excessive blank lines if (/\n{4,}/.test(content)) { issues.push('Found excessive blank lines (4+)'); } // Check for trailing whitespace if (/[ \t]+$/m.test(content)) { issues.push('Found trailing whitespace'); } // Check for inconsistent todo formatting if (/^-\s*\[([x ])\]([^ ])/m.test(content)) { issues.push('Found inconsistent todo formatting'); } logger_1.Logger.log(`Formatting validation: ${issues.length} issues found`); return { isValid: issues.length === 0, issues }; } catch (error) { logger_1.Logger.error(`Failed to validate formatting: ${error.message}`); return { isValid: false, issues: ['Validation failed'] }; } } // Check if today's section already exists hasTodaySection() { return this.getTodaySection() !== null; } // Check if today's section is missing (only creates today, never backfills gaps) checkForMissingDays() { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); if (dayMap.size === 0) { // No days exist, we definitely need today return [new Date().toLocaleDateString()]; } const missingDays = []; // Only check if today is missing - we don't backfill gaps between days if (!this.hasTodaySection()) { missingDays.push(new Date().toLocaleDateString()); } return missingDays; } catch (error) { logger_1.Logger.error(`Failed to check for missing days: ${error.message}`); return []; } } // Auto-create daily section if needed (enhanced version) async autoCreateDailySection(force = false) { try { // Check if today already exists (unless forced) if (!force && this.hasTodaySection()) { return { created: false, reason: "Today's section already exists" }; } // Check for missing days const missingDays = this.checkForMissingDays(); if (missingDays.length === 0 && !force) { return { created: false, reason: 'No missing days detected' }; } // Create today's section (or force create) const today = new Date().toLocaleDateString(); if (missingDays.includes(today) || force) { await this.writeNewDay(); logger_1.Logger.log(`Auto-created daily section for ${today}`); return { created: true, reason: `Created section for ${today}` }; } return { created: false, reason: "Today's section not needed" }; } catch (error) { logger_1.Logger.error(`Failed to auto-create daily section: ${error.message}`); return { created: false, reason: `Error: ${error.message}` }; } } // Get time since last entry (useful for wake detection logic) getTimeSinceLastEntry() { try { const content = this.readNotesFile(); const dayMap = this.parseDays(content); if (dayMap.size === 0) { return Infinity; // No entries, very long time } const sortedDates = Array.from(dayMap.keys()).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); const lastDate = new Date(sortedDates[0]); const now = new Date(); return now.getTime() - lastDate.getTime(); } catch (error) { logger_1.Logger.error(`Failed to get time since last entry: ${error.message}`); return 0; } } // AI Query Processing async processAIQuery(request) { // Extract FULL daily sections with smart limiting const { content, metadata } = this.extractFullDayContext(request.timeRange); logger_1.Logger.log(`AI Query context: ${metadata.daysCovered} days, ${metadata.charactersUsed} chars${metadata.truncated ? ' (truncated)' : ''}`); // Debug: Log first 100 chars of content to see what's being sent const contentPreview = content.length > 100 ? content.substring(0, 100) + '...' : content; logger_1.Logger.log(`Content preview: "${contentPreview}"`); // Simple, concise prompt const prompt = `You are analyzing someone's personal daily notes to help answer their question: "${request.query}" Please provide a concise, actionable response (2 paragraphs max). Prefer one to two sentences if it's a simple question. ${content}`; if (!this.aiService?.isEnabled()) { throw new Error('AI service is not available'); } const response = await this.aiService.processQuery(prompt); const actionItems = this.extractActionableItems(response); return { response, contextUsed: metadata, ...(actionItems.length > 0 && { suggestions: actionItems }), }; } extractFullDayContext(timeRange) { const DAILY_USE_LIMITS = { DEFAULT_DAYS: 3, // Good recent context MAX_DAYS: 14, // 2 weeks maximum MAX_CHARACTERS: 15000, // ~4000 tokens, reasonable for Gemini PRIORITY_RECENT_DAYS: 2, // Always include last 2 days fully }; let targetDays = 3; // Default switch (timeRange.type) { case 'today': targetDays = 1; break; case 'week': targetDays = 7; break; case 'month': targetDays = Math.min(14, 30); break; // Cap at 2 weeks case 'custom': targetDays = Math.min(timeRange.days || 3, 14); break; } try { // Read notes file and parse days to get both dates and content const notesFileContent = this.readNotesFile(); const dayMap = this.parseDays(notesFileContent); logger_1.Logger.log(`Found ${dayMap.size} days in notes for AI context`); if (dayMap.size === 0) { return { content: `No daily notes found for the requested time range. To get started: 1. Create a daily section: notes-sync daily --create 2. Add some notes: notes-sync add -n "your note" 3. Add some tasks: notes-sync add -t "your task" Then try your AI query again!`, metadata: { daysCovered: 0, charactersUsed: 0, truncated: false, }, }; } // Get dates and sort them in descending order (most recent first) const sortedDates = Array.from(dayMap.keys()).sort((a, b) => { const dateA = new Date(a); const dateB = new Date(b); return dateB.getTime() - dateA.getTime(); }); // Take the requested number of days, prioritizing recent ones const selectedDates = sortedDates.slice(0, targetDays); const sections = []; let totalCharacters = 0; let truncated = false; let daysIncluded = 0; for (const date of selectedDates) { const fullDayContent = dayMap.get(date) || ''; // Skip days with no meaningful content if (!fullDayContent.trim()) { continue; } const daySection = `## ${date}\n\n${fullDayContent.trim()}`; const dayLength = daySection.length; // Always include first 2 days (most recent) if (daysIncluded < DAILY_USE_LIMITS.PRIORITY_RECENT_DAYS) { sections.push(daySection); totalCharacters += dayLength; daysIncluded++; continue; } // For remaining days, check if we can fit them if (totalCharacters + dayLength <= DAILY_USE_LIMITS.MAX_CHARACTERS) { sections.push(daySection); totalCharacters += dayLength; daysIncluded++; } else { // Try to include a truncated version const availableSpace = DAILY_USE_LIMITS.MAX_CHARACTERS - totalCharacters - 100; // Leave buffer if (availableSpace > 500) { // Only truncate if we have reasonable space const truncatedDay = this.truncateDayContent(daySection, availableSpace); sections.push(truncatedDay); totalCharacters += truncatedDay.length; daysIncluded++; truncated = true; } break; // Stop processing older days } } const content = sections.join('\n\n---\n\n'); if (!content.trim()) { return { content: `Your daily sections exist but appear to be empty. Try adding some content first: - Add a note: notes-sync add -n "Had a productive meeting with the team" - Add a task: notes-sync add -t "Review quarterly budget" - Check your current status: notes-sync daily --status Once you have some content, AI can analyze your notes and provide insights!`, metadata: { daysCovered: daysIncluded, charactersUsed: 0, truncated: false, }, }; } return { content, metadata: { daysCovered: daysIncluded, charactersUsed: totalCharacters, truncated, }, }; } catch (error) { logger_1.Logger.error(`Failed to extract context: ${error.message}`); return { content: `Unable to read your notes file. This could mean: 1. Your notes directory doesn't exist yet 2. Your Daily.md file hasn't been created 3. There's a permissions issue Try running: notes-sync daily --create Error details: ${error.message}`, metadata: { daysCovered: 0, charactersUsed: 0, truncated: false, }, }; } } truncateDayContent(daySection, maxLength) { if (daySection.length <= maxLength) return daySection; const lines = daySection.split('\n'); const header = lines[0]; // Keep the date header let truncatedContent = header + '\n\n'; // Try to keep structure by preserving section headers for (const line of lines.slice(2)) { if (truncatedContent.length + line.length + 1 < maxLength - 50) { truncatedContent += line + '\n'; } else if (line.startsWith('**') || line.startsWith('#')) { // Always try to include section headers if (truncatedContent.length + line.length + 1 < maxLength - 20) { truncatedContent += line + '\n'; } break; } } return truncatedContent + '\n[... content truncated for length ...]'; } extractActionableItems(response) { // Simple extraction of bullet points or numbered items const lines = response.split('\n'); const actionItems = []; for (const line of lines) { const trimmed = line.trim(); // Look for bullet points or numbered items that seem actionable if (trimmed.match(/^[\d\-\*]\s+/) && trimmed.length > 10) { const cleaned = trimmed.replace(/^[\d\-\*\.\)]\s*/, '').trim(); if (cleaned) { actionItems.push(cleaned); } } } return actionItems.slice(0, 5); // Max 5 suggestions } async viewNotes(request) { try { const content = this.readNotesFile(); switch (request.type) { case 'today': { const today = new Date(); const todaySection = this.getDate(today); if (!todaySection) { return { content: `No notes found for today (${today.toLocaleDateString()})`, metadata: { type: 'today', totalLines: 1, }, }; } return { content: todaySection, metadata: { type: 'today', totalLines: todaySection.split('\n').length, dateRange: { start: today.toLocaleDateString(), end: today.toLocaleDateString(), }, }, }; } case 'recent': { const days = request.days || 7; const dayMap = this.parseDays(content); const sortedDates = Array.from(dayMap.keys()) .sort((a, b) => new Date(b).getTime() - new Date(a).getTime()) .slice(0, days); if (sortedDates.length === 0) { return { content: `No notes found in the last ${days} days`, metadata: { type: 'recent', daysCovered: 0, totalLines: 1, }, }; } const recentContent = sortedDates .map(date => dayMap.get(date)) .filter(Boolean) .join('\n\n'); return { content: recentContent, metadata: { type: 'recent', daysCovered: sortedDates.length, totalLines: recentContent.split('\n').length, dateRange: { start: sortedDates[sortedDates.length - 1], end: sortedDates[0], }, }, }; } case 'all': { return { content: content, metadata: { type: 'all', totalLines: content.split('\n').length, }, }; } default: throw new Error(`Unknown view type: ${request.type}`); } } catch (error) { logger_1.Logger.error(`Failed to view notes: ${error.message}`); throw error; } } } exports.NoteInteractor = NoteInteractor; //# sourceMappingURL=note-interactor.js.map