Skip to main content
Glama

myAI Memory Sync

by Jktfe
platformSync.ts•23.9 kB
import { promises as fs } from 'fs'; import path from 'path'; import puppeteer from 'puppeteer'; import { homedir } from 'os'; import { SyncStatus, PlatformType } from './types.js'; import { generateTemplate } from './templateParser.js'; import { config } from './config.js'; // Import the config object import * as fsSync from 'fs'; // Custom logger that writes to stderr instead of stdout const logger = { log: (...args: any[]) => console.error('[platformSync]', ...args), error: (...args: any[]) => console.error('[platformSync:error]', ...args) }; /** * Base platform sync interface */ interface PlatformSyncer { sync(templateContent: string): Promise<SyncStatus>; } /** * Helper function to expand tilde in paths */ function expandTildePath(filePath: string): string { if (!filePath || typeof filePath !== 'string') { return filePath; } // Replace tilde with home directory if (filePath.startsWith('~/') || filePath === '~') { return filePath.replace(/^~/, homedir()); } // If path is relative, make it absolute if (!path.isAbsolute(filePath)) { return path.resolve(process.cwd(), filePath); } return filePath; } /** * Helper function to ensure a file is writable * Will attempt to fix permissions if possible */ export async function ensureFileWritable(filePath: string): Promise<boolean> { try { // Make sure the directory exists const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); // Check if file exists try { await fs.access(filePath); // Try to make it writable try { await fs.chmod(filePath, 0o644); console.error(`Made file writable: ${filePath}`); } catch (err) { console.error(`Failed to change permissions on ${filePath}:`, err); // Continue anyway - it might still work } } catch (err) { // File doesn't exist, try to create it try { // Create an empty file await fs.writeFile(filePath, '', { flag: 'wx' }); console.error(`Created empty file: ${filePath}`); } catch (fileErr) { // That's okay, we'll try to write to it anyway console.error(`Failed to create file ${filePath}:`, fileErr); } } // Final test - try to open it for writing try { const handle = await fs.open(filePath, 'r+'); await handle.close(); return true; } catch (err) { console.error(`File not writable: ${filePath}`, err); return false; } } catch (err) { console.error(`Error ensuring file is writable: ${filePath}`, err); return false; } } /** * Helper to extract the myAI Memory section from template content */ function extractMyAIMemorySection(templateContent: string): string { // Check if the template contains "# myAI Memory" section if (templateContent.includes('# myAI Memory')) { // Find the index of the "# myAI Memory" header const startIndex = templateContent.indexOf('# myAI Memory'); // Extract everything from that point onwards return templateContent.substring(startIndex).trim(); } // If there's no myAI Memory section, create a minimal one return '# myAI Memory\n\n'; } /** * Helper to update the myAI Memory section in a file * Using the simplified approach: * 1. If "# myAI Memory" exists, remove everything from that line to the end * 2. Append the new myAI Memory section */ async function updateMyAIMemorySection(filePath: string, memorySection: string): Promise<void> { try { // Ensure the file is writable if (!await ensureFileWritable(filePath)) { throw new Error(`File not writable: ${filePath}`); } // Read the file let content = await fs.readFile(filePath, 'utf-8'); // Check if it has a myAI Memory section const hasMemorySection = content.includes('# myAI Memory'); if (hasMemorySection) { // Remove everything from "# myAI Memory" to the end of the file content = content.replace(/# myAI Memory[\s\S]*$/, ''); // Trim any trailing whitespace content = content.trim(); // Append the new myAI Memory section with proper spacing content = `${content}\n\n${memorySection}`; } else { // Append the myAI Memory section at the end with proper spacing content = content.trim(); content = `${content}\n\n${memorySection}`; } // Write the updated content back to the file await fs.writeFile(filePath, content, 'utf-8'); console.error(`Successfully updated myAI Memory section in ${filePath}`); } catch (err) { console.error(`Error updating myAI Memory section in ${filePath}:`, err); throw err; } } /** * Check if CLAUDE.md is gitignored in a project * @param projectPath The path to the project root * @returns True if CLAUDE.md is gitignored, false otherwise */ async function isClaudeMdGitignored(projectPath: string): Promise<boolean> { try { const gitignorePath = path.join(projectPath, '.gitignore'); // Check if .gitignore exists try { await fs.access(gitignorePath); } catch (err) { // .gitignore doesn't exist return false; } // Read the .gitignore file const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); // Check if CLAUDE.md is mentioned const lines = gitignoreContent.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); // Check for any of these patterns that would match CLAUDE.md if ( trimmedLine === 'CLAUDE.md' || trimmedLine === '/CLAUDE.md' || trimmedLine === '**/CLAUDE.md' || trimmedLine === 'CLAUDE.*' ) { return true; } } return false; } catch (err) { console.error(`Error checking if CLAUDE.md is gitignored in ${projectPath}:`, err); return false; } } /** * Adds CLAUDE.md to the .gitignore file of a project * @param projectPath The path to the project root */ async function addClaudeMdToGitignore(projectPath: string): Promise<void> { try { const gitignorePath = path.join(projectPath, '.gitignore'); // Create .gitignore if it doesn't exist let content = ''; try { await fs.access(gitignorePath); content = await fs.readFile(gitignorePath, 'utf-8'); } catch (err) { // .gitignore doesn't exist, create it console.error(`Creating new .gitignore file in ${projectPath}`); } // Add CLAUDE.md to .gitignore if it's not already there if (!content.includes('CLAUDE.md')) { // Add a header if this is a new file if (!content.trim()) { content = '# Git ignore file\n\n'; } // Make sure it ends with a newline if (content.length > 0 && !content.endsWith('\n')) { content += '\n'; } // Add CLAUDE.md content += '/CLAUDE.md\n'; // Write the updated content back to the file await fs.writeFile(gitignorePath, content, 'utf-8'); console.error(`Added CLAUDE.md to .gitignore in ${projectPath}`); } } catch (err) { console.error(`Error adding CLAUDE.md to .gitignore in ${projectPath}:`, err); throw err; } } /** * Claude Code synchronization (CLAUDE.md files in project roots) */ export class ClaudeCodeSyncer implements PlatformSyncer { private projectRoots: string[] = []; private lastSyncTime: number = 0; private syncCooldown: number = 5000; // 5 second cooldown private claudeProjectsPath: string; constructor(claudeProjectsPath?: string) { // Set the Claude projects path this.claudeProjectsPath = claudeProjectsPath || path.join(homedir(), 'CascadeProjects'); console.error(`ClaudeCodeSyncer initialized with path: ${this.claudeProjectsPath}`); // Add the current project as a starting point const myAiClaudeMdPath = path.join(process.cwd(), 'CLAUDE.md'); if (fsSync.existsSync(myAiClaudeMdPath)) { this.projectRoots.push(process.cwd()); console.error(`Found CLAUDE.md in current project: ${myAiClaudeMdPath}`); } // Log initialization console.error('ClaudeCodeSyncer initialized - will search common project directories for CLAUDE.md files'); } async sync(templateContent: string): Promise<SyncStatus> { console.error('ClaudeCodeSyncer.sync called, claudeProjectsPath:', this.claudeProjectsPath); try { const now = Date.now(); // Check if we're within the cooldown period if (now - this.lastSyncTime < this.syncCooldown) { console.error(`Skipping Claude Code sync - within cooldown period (${this.syncCooldown}ms)`); return { platform: 'claude-code', success: true, message: `Skipped update to Claude Code files (within throttle period)` }; } // Make sure we're working with a properly expanded path const resolvedProjectsPath = expandTildePath(this.claudeProjectsPath); console.error(`Resolved projects path: ${resolvedProjectsPath}`); // Start with home directory CLAUDE.md const homeClaudeMdPath = path.join(homedir(), 'CLAUDE.md'); const successfulSyncs: string[] = []; const failedSyncs: { path: string; error: string }[] = []; // Handle home directory CLAUDE.md console.error(`Checking home directory CLAUDE.md at ${homeClaudeMdPath}`); try { // Ensure writable const homeFileWritable = await ensureFileWritable(homeClaudeMdPath); if (homeFileWritable) { // Extract the "myAI Memory" section const memorySection = extractMyAIMemorySection(templateContent); if (memorySection) { // Update the memory section await updateMyAIMemorySection(homeClaudeMdPath, memorySection); successfulSyncs.push(homeClaudeMdPath); console.error(`Successfully updated ${homeClaudeMdPath}`); } else { failedSyncs.push({ path: homeClaudeMdPath, error: 'Failed to extract "myAI Memory" section from template' }); } } else { failedSyncs.push({ path: homeClaudeMdPath, error: 'Could not make file writable' }); } } catch (err) { console.error(`Error updating ${homeClaudeMdPath}:`, err); failedSyncs.push({ path: homeClaudeMdPath, error: err instanceof Error ? err.message : String(err) }); } try { // Check if the projects directory exists await fs.access(resolvedProjectsPath); // Get all directories within the project directory const dirEntries = await fs.readdir(resolvedProjectsPath, { withFileTypes: true }); const directories = dirEntries.filter(entry => entry.isDirectory()); console.error(`Found ${directories.length} project directories to check for CLAUDE.md files`); // Extract the "myAI Memory" section const memorySection = extractMyAIMemorySection(templateContent); if (!memorySection) { throw new Error('Failed to extract "myAI Memory" section from template'); } // Check each directory for a CLAUDE.md file for (const dir of directories) { try { const dirPath = path.join(resolvedProjectsPath, dir.name); const claudeMdPath = path.join(dirPath, 'CLAUDE.md'); // First, ensure CLAUDE.md is gitignored const isGitignored = await isClaudeMdGitignored(dirPath); if (!isGitignored) { console.error(`CLAUDE.md is not gitignored in ${dirPath}. Adding to .gitignore...`); try { await addClaudeMdToGitignore(dirPath); console.error(`Successfully added CLAUDE.md to .gitignore in ${dirPath}`); } catch (gitignoreErr) { console.error(`Failed to add CLAUDE.md to .gitignore in ${dirPath}:`, gitignoreErr); // Continue anyway - we'll still update the file } } // Check if CLAUDE.md exists and ensure it's writable const isWritable = await ensureFileWritable(claudeMdPath); if (isWritable) { // Update the memory section await updateMyAIMemorySection(claudeMdPath, memorySection); successfulSyncs.push(claudeMdPath); console.error(`Successfully updated ${claudeMdPath}`); } else { failedSyncs.push({ path: claudeMdPath, error: 'Could not make file writable' }); } } catch (err) { console.error(`Error updating ${dir.name}/CLAUDE.md:`, err); failedSyncs.push({ path: `${dir.name}/CLAUDE.md`, error: err instanceof Error ? err.message : String(err) }); } } } catch (err) { console.error(`Error accessing projects directory ${resolvedProjectsPath}:`, err); failedSyncs.push({ path: resolvedProjectsPath, error: err instanceof Error ? err.message : String(err) }); } // Update last sync time this.lastSyncTime = now; if (successfulSyncs.length === 0) { return { platform: 'claude-code', success: false, message: `Failed to update any CLAUDE.md files. Errors: ${failedSyncs.map(f => `${f.path}: ${f.error}`).join(', ')}` }; } else if (failedSyncs.length === 0) { return { platform: 'claude-code', success: true, message: `Successfully updated ${successfulSyncs.length} CLAUDE.md files` }; } else { return { platform: 'claude-code', success: true, message: `Partially successful: Updated ${successfulSyncs.length} CLAUDE.md files, ${failedSyncs.length} failures` }; } } catch (err) { console.error('Error in ClaudeCodeSyncer.sync:', err); return { platform: 'claude-code', success: false, message: `Failed to sync CLAUDE.md files: ${err instanceof Error ? err.message : String(err)}` }; } } } /** * Claude.ai web interface synchronization */ export class ClaudeWebSyncer implements PlatformSyncer { private email?: string; constructor(options?: { email?: string }) { this.email = options?.email; } async sync(templateContent: string): Promise<SyncStatus> { logger.log('Syncing with Claude Web Profile Settings...'); // Launch browser const browser = await puppeteer.launch({ headless: config.puppeteer.headless, slowMo: config.puppeteer.slowMo, args: ['--no-sandbox'] }); try { const page = await browser.newPage(); // Set default timeout page.setDefaultTimeout(config.puppeteer.defaultTimeout); // Navigate directly to Claude.ai profile settings await page.goto('https://claude.ai/settings/profile'); // Check if already logged in by looking for profile settings elements const isLoggedIn = await page.evaluate(() => { return document.querySelector('textarea[name="preferences"]') !== null || document.querySelector('form[action="/settings/profile"]') !== null; }); if (!isLoggedIn) { logger.log('Not logged in, initiating login process...'); // Wait for the email input field const emailInput = await page.waitForSelector('input[type="email"]'); // Fill in email if provided if (this.email && emailInput) { await emailInput.type(this.email); logger.log(`Email field filled with: ${this.email}`); // Click continue button const continueButton = await page.waitForSelector('button[type="submit"]'); if (continueButton) { await continueButton.click(); logger.log('Clicked continue button'); } } else { logger.log('No email provided or email input not found. Please enter your email manually in the browser.'); } // Wait for user to complete login process logger.log('Waiting for user to complete login process...'); await page.waitForSelector('textarea[name="preferences"], form[action="/settings/profile"]', { timeout: 300000 }); // 5 minute timeout logger.log('Login successful!'); } else { logger.log('Already logged in to Claude.ai'); } // Wait for the preferences textarea to be available logger.log('Accessing profile preferences...'); const preferencesTextarea = await page.waitForSelector('textarea[name="preferences"]'); if (preferencesTextarea) { // Extract myAI Memory section from template content const myAIMemorySection = extractMyAIMemorySection(templateContent); // Update preferences content logger.log('Updating profile preferences...'); await page.evaluate((el: HTMLTextAreaElement, content: string) => { el.value = content; // Trigger input event to ensure the UI recognizes the change const event = new Event('input', { bubbles: true }); el.dispatchEvent(event); }, preferencesTextarea, myAIMemorySection); // Find and click the Save button const saveButton = await page.waitForSelector('button[type="submit"]'); if (saveButton) { await saveButton.click(); logger.log('Clicked save button'); // Wait for save to complete (you might need to adjust this based on actual page behavior) await page.waitForFunction( () => { // Look for success notification or changes in button state return ( document.querySelector('.success-notification') !== null || !document.querySelector('button[type="submit"][disabled]') ); }, { timeout: 10000 } ); logger.log('Preferences successfully saved'); } else { throw new Error('Save button not found'); } } else { throw new Error('Preferences textarea not found'); } logger.log('Closing browser...'); await browser.close(); return { platform: 'claude-web', success: true, message: 'Successfully updated Claude.ai profile preferences' }; } catch (err) { if (browser) { await browser.close(); } logger.error('Error syncing with Claude.ai profile settings:', err); return { platform: 'claude-web', success: false, message: `Failed to sync with Claude.ai profile settings: ${err instanceof Error ? err.message : String(err)}` }; } } } /** * Windsurf synchronization (global_rules.md file) */ export class WindsurfSyncer implements PlatformSyncer { private rulesPath: string; private lastSyncTime: number = 0; private syncCooldown: number = 5000; // 5 second cooldown constructor(rulesPath?: string) { // IMPORTANT: Use the correct path with plural "rules" instead of "rule" // Always use the path from config if available if (rulesPath) { this.rulesPath = expandTildePath(rulesPath); console.error(`WindsurfSyncer using configured rulesPath: ${this.rulesPath}`); } else { // Use the default path with explicit homedir this.rulesPath = path.join(homedir(), '.codeium', 'windsurf', 'memories', 'global_rules.md'); console.error(`WindsurfSyncer using default path: ${this.rulesPath}`); } // Direct reference to ensure we're using the right path if (config.paths && config.paths.windsurfMemoryPath) { const configPath = expandTildePath(config.paths.windsurfMemoryPath); console.error(`Config contains windsurfMemoryPath: ${config.paths.windsurfMemoryPath} (expanded: ${configPath})`); // If we have a config path, use it instead this.rulesPath = configPath; console.error(`WindsurfSyncer using config path: ${this.rulesPath}`); } } async sync(templateContent: string): Promise<SyncStatus> { try { const now = Date.now(); // Check if we're within the cooldown period if (now - this.lastSyncTime < this.syncCooldown) { console.error(`Skipping Windsurf sync - within cooldown period (${this.syncCooldown}ms)`); return { platform: 'windsurf', success: true, message: `Skipped update to Windsurf (within throttle period)` }; } // Make sure the path is properly expanded and resolved const resolvedPath = expandTildePath(this.rulesPath); console.error(`Preparing to sync with Windsurf at resolved path: ${resolvedPath}`); // Ensure directory exists and file is writable const isWritable = await ensureFileWritable(resolvedPath); if (!isWritable) { return { platform: 'windsurf', success: false, message: `Failed to sync with Windsurf: Could not make file writable: ${resolvedPath}` }; } // Extract the "myAI Memory" section const memorySection = extractMyAIMemorySection(templateContent); if (!memorySection) { return { platform: 'windsurf', success: false, message: 'Failed to extract "myAI Memory" section from template' }; } // Update the memory section (this handles creating/replacing logic) await updateMyAIMemorySection(resolvedPath, memorySection); // Update last sync time this.lastSyncTime = now; return { platform: 'windsurf', success: true, message: `Successfully updated Windsurf memory at ${resolvedPath}` }; } catch (err) { console.error('Error syncing with Windsurf:', err); return { platform: 'windsurf', success: false, message: `Failed to sync with Windsurf: ${err instanceof Error ? err.message : String(err)}` }; } } } /** * Platform synchronization manager */ export class PlatformSyncManager { private syncers: Map<PlatformType, PlatformSyncer> = new Map(); constructor() { // Default syncers this.syncers.set('claude-code', new ClaudeCodeSyncer()); this.syncers.set('windsurf', new WindsurfSyncer()); // Claude web syncer requires credentials, so it's not created by default } /** * Set the syncer for a specific platform */ setSyncer(platform: PlatformType, syncer: PlatformSyncer): void { this.syncers.set(platform, syncer); } /** * Sync template with all platforms */ async syncAll(templateContent: string): Promise<SyncStatus[]> { const results: SyncStatus[] = []; for (const [platform, syncer] of this.syncers.entries()) { logger.log(`Syncing with platform: ${platform}`); const result = await syncer.sync(templateContent); results.push(result); } return results; } /** * Sync template with a specific platform */ async syncPlatform(platform: PlatformType, templateContent: string): Promise<SyncStatus> { const syncer = this.syncers.get(platform); if (!syncer) { return { platform, success: false, message: `No syncer configured for platform: ${platform}` }; } return await syncer.sync(templateContent); } }

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/Jktfe/myAImemory-mcp'

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