Skip to main content
Glama

MCP Task

by just-every
patcher.ts21.2 kB
// patcher.ts // A TypeScript port of your Python implementation. // Target: Node.js (fs/path), ES2020+ // Compile: tsc patcher.ts && node patcher.js < patch.txt import * as fs from 'fs'; import * as path from 'path'; // ---------- Types & Models ---------- export enum ActionType { ADD = 'add', DELETE = 'delete', UPDATE = 'update', } export interface FileChange { type: ActionType; old_content?: string; new_content?: string; move_path?: string; } export class Commit { changes: Record<string, FileChange> = {}; } export class Chunk { orig_index: number = -1; // line index of the first line in the original file del_lines: string[] = []; ins_lines: string[] = []; } export class PatchAction { type: ActionType = ActionType.UPDATE; new_file?: string; chunks: Chunk[] = []; move_path?: string; } export class Patch { actions: Record<string, PatchAction> = {}; } export class DiffError extends Error {} // ---------- Utility Helpers ---------- function arraysEqual(a: string[], b: string[]): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } function rstrip(s: string): string { return s.replace(/\s+$/, ''); } // ---------- assemble_changes ---------- export function assemble_changes( orig: Record<string, string | null | undefined>, dest: Record<string, string | null | undefined> ): Commit { const commit = new Commit(); const keys = new Set<string>([...Object.keys(orig), ...Object.keys(dest)]); const sorted = [...keys].sort(); for (const p of sorted) { const old_content = orig[p] ?? undefined; const new_content = dest[p] ?? undefined; if (old_content !== new_content) { if (old_content !== undefined && new_content !== undefined) { commit.changes[p] = { type: ActionType.UPDATE, old_content, new_content, }; } else if (new_content) { commit.changes[p] = { type: ActionType.ADD, new_content, }; } else if (old_content) { commit.changes[p] = { type: ActionType.DELETE, old_content, }; } else { // Mirrors the Python "assert False" throw new DiffError( 'assemble_changes: unreachable state for path ' + p ); } } } return commit; } // ---------- Parser & Parsing ---------- class Parser { current_files: Record<string, string> = {}; lines: string[] = []; index: number = 0; patch: Patch = new Patch(); fuzz: number = 0; constructor(init?: Partial<Parser>) { Object.assign(this, init); } is_done(prefixes?: string[]): boolean { if (this.index >= this.lines.length) return true; if ( prefixes && prefixes.some(pre => this.lines[this.index].startsWith(pre)) ) { return true; } return false; } startswith(prefix: string | string[]): boolean { if (this.index >= this.lines.length) { throw new DiffError(`Index: ${this.index} >= ${this.lines.length}`); } const s = this.lines[this.index]; if (Array.isArray(prefix)) return prefix.some(p => s.startsWith(p)); return s.startsWith(prefix); } read_str(prefix: string = '', return_everything: boolean = false): string { if (this.index >= this.lines.length) { throw new DiffError(`Index: ${this.index} >= ${this.lines.length}`); } const s = this.lines[this.index]; if (s.startsWith(prefix)) { const text = return_everything ? s : s.slice(prefix.length); this.index += 1; return text; } return ''; } parse(): void { while (!this.is_done(['*** End Patch'])) { let pathStr = this.read_str('*** Update File: '); if (pathStr) { if (this.patch.actions[pathStr]) { throw new DiffError( `Update File Error: Duplicate Path: ${pathStr}` ); } const move_to = this.read_str('*** Move to: '); if (!(pathStr in this.current_files)) { throw new DiffError( `Update File Error: Missing File: ${pathStr}` ); } const text = this.current_files[pathStr]; const action = this.parse_update_file(text); action.move_path = move_to || undefined; // TODO: optional validation this.patch.actions[pathStr] = action; continue; } pathStr = this.read_str('*** Delete File: '); if (pathStr) { if (this.patch.actions[pathStr]) { throw new DiffError( `Delete File Error: Duplicate Path: ${pathStr}` ); } if (!(pathStr in this.current_files)) { throw new DiffError( `Delete File Error: Missing File: ${pathStr}` ); } const act = new PatchAction(); act.type = ActionType.DELETE; this.patch.actions[pathStr] = act; continue; } pathStr = this.read_str('*** Add File: '); if (pathStr) { if (this.patch.actions[pathStr]) { throw new DiffError( `Add File Error: Duplicate Path: ${pathStr}` ); } this.patch.actions[pathStr] = this.parse_add_file(); continue; } throw new DiffError(`Unknown Line: ${this.lines[this.index]}`); } if (!this.startswith('*** End Patch')) { throw new DiffError('Missing End Patch'); } this.index += 1; } parse_update_file(text: string): PatchAction { const action = new PatchAction(); action.type = ActionType.UPDATE; const lines = text.split('\n'); let index = 0; while ( !this.is_done([ '*** End Patch', '*** Update File:', '*** Delete File:', '*** Add File:', '*** End of File', ]) ) { const defStr = this.read_str('@@ '); let sectionStr = ''; if (!defStr) { if (this.lines[this.index] === '@@') { sectionStr = this.lines[this.index]; this.index += 1; } } if (!defStr && !sectionStr && index !== 0) { throw new DiffError(`Invalid Line:\n${this.lines[this.index]}`); } if (defStr.trim()) { let found = false; if (!lines.slice(0, index).some(s => s === defStr)) { for (let i = index; i < lines.length; i++) { const s = lines[i]; if (s === defStr) { index = i + 1; found = true; break; } } } if ( !found && !lines.slice(0, index).some(s => s.trim() === defStr.trim()) ) { for (let i = index; i < lines.length; i++) { const s = lines[i]; if (s.trim() === defStr.trim()) { index = i + 1; this.fuzz += 1; found = true; break; } } } } const [next_chunk_context, chunks, end_patch_index, eof] = peek_next_section(this.lines, this.index); const next_chunk_text = next_chunk_context.join('\n'); const { new_index, fuzz } = find_context( lines, next_chunk_context, index, eof ); if (new_index === -1) { if (eof) { throw new DiffError( `Invalid EOF Context ${index}:\n${next_chunk_text}` ); } else { throw new DiffError( `Invalid Context ${index}:\n${next_chunk_text}` ); } } this.fuzz += fuzz; for (const ch of chunks) { ch.orig_index += new_index; action.chunks.push(ch); } index = new_index + next_chunk_context.length; this.index = end_patch_index; continue; } return action; } parse_add_file(): PatchAction { const lines: string[] = []; while ( !this.is_done([ '*** End Patch', '*** Update File:', '*** Delete File:', '*** Add File:', ]) ) { let s = this.read_str(); if (!s.startsWith('+')) { throw new DiffError(`Invalid Add File Line: ${s}`); } s = s.slice(1); lines.push(s); } const act = new PatchAction(); act.type = ActionType.ADD; act.new_file = lines.join('\n'); return act; } } // ---------- Context Finding ---------- function find_context_core( lines: string[], context: string[], start: number ): { new_index: number; fuzz: number } { if (!context || context.length === 0) { // console.log("context is empty"); return { new_index: start, fuzz: 0 }; } // Prefer identical for (let i = start; i < lines.length; i++) { const slice = lines.slice(i, i + context.length); if (arraysEqual(slice, context)) { return { new_index: i, fuzz: 0 }; } } // RStrip is ok for (let i = start; i < lines.length; i++) { const slice = lines.slice(i, i + context.length).map(s => rstrip(s)); const ctx = context.map(s => rstrip(s)); if (arraysEqual(slice, ctx)) { return { new_index: i, fuzz: 1 }; } } // Fine, Strip is ok too for (let i = start; i < lines.length; i++) { const slice = lines.slice(i, i + context.length).map(s => s.trim()); const ctx = context.map(s => s.trim()); if (arraysEqual(slice, ctx)) { return { new_index: i, fuzz: 100 }; } } return { new_index: -1, fuzz: 0 }; } function find_context( lines: string[], context: string[], start: number, eof: boolean ): { new_index: number; fuzz: number } { if (eof) { let { new_index, fuzz } = find_context_core( lines, context, lines.length - context.length ); if (new_index !== -1) { return { new_index, fuzz }; } ({ new_index, fuzz } = find_context_core(lines, context, start)); return { new_index, fuzz: fuzz + 10000 }; } return find_context_core(lines, context, start); } function peek_next_section( lines: string[], index: number ): [string[], Chunk[], number, boolean] { const old: string[] = []; let del_lines: string[] = []; let ins_lines: string[] = []; const chunks: Chunk[] = []; let mode: 'keep' | 'add' | 'delete' = 'keep'; const orig_index = index; function startsWithAny(s: string, prefixes: string[]): boolean { return prefixes.some(p => s.startsWith(p)); } while (index < lines.length) { let s = lines[index]; if ( startsWithAny(s, [ '@@', '*** End Patch', '*** Update File:', '*** Delete File:', '*** Add File:', '*** End of File', ]) ) { break; } if (s === '***') { break; } else if (s.startsWith('***')) { throw new DiffError(`Invalid Line: ${s}`); } index += 1; const last_mode: 'keep' | 'add' | 'delete' = mode; if (s === '') s = ' '; const first = s[0]; if (first === '+') mode = 'add'; else if (first === '-') mode = 'delete'; else if (first === ' ') mode = 'keep'; else throw new DiffError(`Invalid Line: ${s}`); s = s.slice(1); if (mode === 'keep' && last_mode !== mode) { if (ins_lines.length || del_lines.length) { const ch = new Chunk(); ch.orig_index = old.length - del_lines.length; ch.del_lines = del_lines; ch.ins_lines = ins_lines; chunks.push(ch); } del_lines = []; ins_lines = []; } if (mode === 'delete') { del_lines.push(s); old.push(s); } else if (mode === 'add') { ins_lines.push(s); } else if (mode === 'keep') { old.push(s); } } if (ins_lines.length || del_lines.length) { const ch = new Chunk(); ch.orig_index = old.length - del_lines.length; ch.del_lines = del_lines; ch.ins_lines = ins_lines; chunks.push(ch); del_lines = []; ins_lines = []; } if (index < lines.length && lines[index] === '*** End of File') { index += 1; return [old, chunks, index, true]; } if (index === orig_index) { throw new DiffError( `Nothing in this section - index=${index} ${lines[index]}` ); } return [old, chunks, index, false]; } // ---------- Text <-> Patch ---------- export function text_to_patch( text: string, orig: Record<string, string> ): { patch: Patch; fuzz: number } { const lines = text.trim().split('\n'); if ( lines.length < 2 || !lines[0].startsWith('*** Begin Patch') || lines[lines.length - 1] !== '*** End Patch' ) { throw new DiffError('Invalid patch text'); } const parser = new Parser({ current_files: orig, lines, index: 1, }); parser.parse(); return { patch: parser.patch, fuzz: parser.fuzz }; } export function identify_files_needed(text: string): string[] { const lines = text.trim().split('\n'); const result = new Set<string>(); for (const line of lines) { if (line.startsWith('*** Update File: ')) { result.add(line.slice('*** Update File: '.length)); } if (line.startsWith('*** Delete File: ')) { result.add(line.slice('*** Delete File: '.length)); } } return Array.from(result); } // ---------- Patch -> Commit ---------- function get_updated_file( text: string, action: PatchAction, p: string ): string { if (action.type !== ActionType.UPDATE) { throw new DiffError('get_updated_file called with non-UPDATE action'); } const orig_lines = text.split('\n'); const dest_lines: string[] = []; let orig_index = 0; let dest_index = 0; for (const chunk of action.chunks) { if (chunk.orig_index > orig_lines.length) { const msg = `_get_updated_file: ${p}: chunk.orig_index ${chunk.orig_index} > len(lines) ${orig_lines.length}`; console.log(msg); throw new DiffError(msg); } if (orig_index > chunk.orig_index) { throw new DiffError( `_get_updated_file: ${p}: orig_index ${orig_index} > chunk.orig_index ${chunk.orig_index}` ); } dest_lines.push(...orig_lines.slice(orig_index, chunk.orig_index)); const delta = chunk.orig_index - orig_index; orig_index += delta; dest_index += delta; if (chunk.ins_lines && chunk.ins_lines.length) { for (let i = 0; i < chunk.ins_lines.length; i++) { dest_lines.push(chunk.ins_lines[i]); } dest_index += chunk.ins_lines.length; } orig_index += chunk.del_lines.length; } dest_lines.push(...orig_lines.slice(orig_index)); const delta = orig_lines.length - orig_index; orig_index += delta; dest_index += delta; if (orig_index !== orig_lines.length || dest_index !== dest_lines.length) { throw new DiffError('_get_updated_file: index mismatch'); } return dest_lines.join('\n'); } export function patch_to_commit( patch: Patch, orig: Record<string, string> ): Commit { const commit = new Commit(); for (const [p, action] of Object.entries(patch.actions)) { if (action.type === ActionType.DELETE) { commit.changes[p] = { type: ActionType.DELETE, old_content: orig[p], }; } else if (action.type === ActionType.ADD) { commit.changes[p] = { type: ActionType.ADD, new_content: action.new_file, }; } else if (action.type === ActionType.UPDATE) { const new_content = get_updated_file(orig[p], action, p); commit.changes[p] = { type: ActionType.UPDATE, old_content: orig[p], new_content, move_path: action.move_path, }; } } return commit; } // ---------- IO + Orchestration ---------- export function load_files( paths: string[], open_fn: (p: string) => string ): Record<string, string> { const orig: Record<string, string> = {}; for (const p of paths) { orig[p] = open_fn(p); } return orig; } export function apply_commit( commit: Commit, write_fn: (p: string, content: string) => void, remove_fn: (p: string) => void ): void { for (const [p, change] of Object.entries(commit.changes)) { if (change.type === ActionType.DELETE) { remove_fn(p); } else if (change.type === ActionType.ADD) { write_fn(p, change.new_content ?? ''); } else if (change.type === ActionType.UPDATE) { if (change.move_path) { write_fn(change.move_path, change.new_content ?? ''); remove_fn(p); } else { write_fn(p, change.new_content ?? ''); } } } } export function process_patch( text: string, open_fn: (p: string) => string, write_fn: (p: string, content: string) => void, remove_fn: (p: string) => void ): string { if (!text.startsWith('*** Begin Patch')) { throw new DiffError('Invalid patch text'); } const paths = identify_files_needed(text); const orig = load_files(paths, open_fn); const { patch } = text_to_patch(text, orig); const commit = patch_to_commit(patch, orig); apply_commit(commit, write_fn, remove_fn); return 'Done!'; } // ---------- Default Node FS Hooks ---------- export function open_file(p: string): string { return fs.readFileSync(p, 'utf8'); } export function write_file(p: string, content: string): void { if (p.startsWith('/')) { console.log('We do not support absolute paths.'); return; } const dir = path.dirname(p); if (dir && dir !== '.') { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(p, content, 'utf8'); } export function remove_file(p: string): void { fs.unlinkSync(p); } // ---------- CLI ---------- export function main(): void { const chunks: Buffer[] = []; process.stdin.on('data', (c: Buffer) => chunks.push(c)); process.stdin.on('end', () => { const patch_text = Buffer.concat(chunks).toString('utf8'); if (!patch_text) { console.log('Please pass patch text through stdin'); return; } try { const result = process_patch( patch_text, open_file, write_file, remove_file ); console.log(result); } catch (e: unknown) { if (e instanceof DiffError) { console.log(String(e.message)); return; } throw e; } }); } // Run if invoked directly // Note: This check doesn't work the same way in ES modules // For ES modules, you'd check import.meta.url === pathToFileURL(process.argv[1]).href // But since this is primarily a library, we'll skip the direct execution check

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/just-every/mcp-task'

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