Skip to main content
Glama
patcher.ts4.93 kB
import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; export interface PatchResult { success: boolean; branch_name?: string; git_result?: string; error?: string; files_changed?: string[]; } export class Patcher { private projectRoot: string; private sentinelDir: string; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.sentinelDir = path.join(process.env.HOME || '', '.sentinel'); } public async applyPatch(unifiedDiff: string): Promise<PatchResult> { try { // Extract patch from sentinel fences if present const cleanDiff = this.extractPatch(unifiedDiff); // Check if repo is clean, stash if needed const needsStash = await this.checkNeedsStash(); if (needsStash) { await this.executeGit(['stash', 'push', '-m', 'Sentinel auto-stash']); } // Create new branch const branchName = `ai/fix-${this.generateTimestamp()}`; await this.executeGit(['checkout', '-b', branchName]); // Write patch to file const patchPath = path.join(this.sentinelDir, 'patch.diff'); await fs.writeFile(patchPath, cleanDiff); // Apply patch await this.executeGit(['apply', '--whitespace=fix', patchPath]); // Stage and commit changes await this.executeGit(['add', '-A']); const commitMessage = `ai: patch applied from sentinel\n\n🤖 Generated with Sentinel MCP`; await this.executeGit(['commit', '-m', commitMessage]); // Get list of changed files const filesChanged = await this.getChangedFiles(); // Restore stash if needed if (needsStash) { await this.executeGit(['stash', 'pop']); } return { success: true, branch_name: branchName, git_result: `Applied patch successfully on branch ${branchName}`, files_changed: filesChanged }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); // Try to recover by going back to original branch try { await this.executeGit(['checkout', '-']); } catch (recoveryError) { // Ignore recovery errors } return { success: false, error: errorMsg }; } } private extractPatch(input: string): string { // Look for sentinel patch fences const beginMarker = '*** begin patch'; const endMarker = '*** end patch'; const beginIndex = input.indexOf(beginMarker); const endIndex = input.indexOf(endMarker); if (beginIndex !== -1 && endIndex !== -1) { const patchContent = input.substring(beginIndex + beginMarker.length, endIndex).trim(); // Remove "*** update file:" lines and extract actual diff const lines = patchContent.split('\n'); const diffLines = lines.filter(line => !line.startsWith('*** update file:') && !line.trim().startsWith('***') ); return diffLines.join('\n'); } // If no fences found, assume the entire input is a unified diff return input; } private async checkNeedsStash(): Promise<boolean> { try { const output = await this.executeGit(['status', '--porcelain']); return output.trim().length > 0; } catch (error) { return false; } } private async getChangedFiles(): Promise<string[]> { try { const output = await this.executeGit(['diff', '--name-only', 'HEAD~1', 'HEAD']); return output.trim().split('\n').filter(line => line.length > 0); } catch (error) { return []; } } private generateTimestamp(): string { const now = new Date(); return now.toISOString() .replace(/[-:]/g, '') .replace(/\..+/, '') .replace('T', '-'); } private async executeGit(args: string[]): Promise<string> { return new Promise((resolve, reject) => { const child = spawn('git', args, { cwd: this.projectRoot }); let stdout = ''; let stderr = ''; if (child.stdout) { child.stdout.on('data', (data) => { stdout += data.toString(); }); } if (child.stderr) { child.stderr.on('data', (data) => { stderr += data.toString(); }); } child.on('close', (code) => { if (code === 0) { resolve(stdout); } else { reject(new Error(stderr || `Git command failed with code ${code}`)); } }); child.on('error', reject); }); } public async getCurrentBranch(): Promise<string> { try { const output = await this.executeGit(['branch', '--show-current']); return output.trim(); } catch (error) { return 'main'; } } public async getRecentDiff(): Promise<string> { try { return await this.executeGit(['diff', 'HEAD~1..HEAD']); } catch (error) { return ''; } } }

Latest Blog Posts

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/Snack-JPG/Godot-Sentinel-MCP'

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