import syncFs from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Type } from "typebox"; import { openBoundaryFile, type BoundaryFileOpenResult } from "../infra/boundary-file-read.js"; import {
mkdirPathWithinRoot,
removePathWithinRoot,
writeFileWithinRoot,
} from "../infra/fs-safe.js"; import { PATH_ALIAS_POLICIES, type PathAliasPolicy } from "../infra/path-alias-guards.js"; import { applyUpdateHunk } from "./apply-patch-update.js"; import { toRelativeSandboxPath, resolvePathFromInput } from "./path-policy.js"; import { assertSandboxPath } from "./sandbox-paths.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
return {
name: "apply_patch",
label: "apply_patch",
description: "Apply a patch to one or more files using the apply_patch format. The input should include *** Begin Patch and *** End Patch markers.",
parameters: applyPatchSchema,
execute: async (_toolCallId, args, signal) => { const params = args as { input?: string }; const input = typeof params.input === "string" ? params.input : ""; if (!input.trim()) { thrownew Error("Provide a patch input.");
} if (signal?.aborted) { const err = new Error("Aborted");
err.name = "AbortError"; throw err;
}
function formatSummary(summary: ApplyPatchSummary): string { const lines = ["Success. Updated the following files:"]; for (const file of summary.added) {
lines.push(`A ${file}`);
} for (const file of summary.modified) {
lines.push(`M ${file}`);
} for (const file of summary.deleted) {
lines.push(`D ${file}`);
} return lines.join("\n");
}
if (firstLine === BEGIN_PATCH_MARKER && lastLine === END_PATCH_MARKER) { returnnull;
} if (firstLine !== BEGIN_PATCH_MARKER) { return"The first line of the patch must be '*** Begin Patch'";
} return"The last line of the patch must be '*** End Patch'";
}
function parseOneHunk(lines: string[], lineNumber: number): { hunk: Hunk; consumed: number } { if (lines.length === 0) { thrownew Error(`Invalid patch hunk at line ${lineNumber}: empty hunk`);
} const firstLine = lines[0].trim(); if (firstLine.startsWith(ADD_FILE_MARKER)) { const targetPath = firstLine.slice(ADD_FILE_MARKER.length);
let contents = "";
let consumed = 1; for (const addLine of lines.slice(1)) { if (addLine.startsWith("+")) {
contents += `${addLine.slice(1)}\n`;
consumed += 1;
} else { break;
}
} return {
hunk: { kind: "add", path: targetPath, contents },
consumed,
};
}
if (firstLine.startsWith(UPDATE_FILE_MARKER)) { const targetPath = firstLine.slice(UPDATE_FILE_MARKER.length);
let remaining = lines.slice(1);
let consumed = 1;
let movePath: string | undefined;
thrownew Error(
`Invalid patch hunk at line ${lineNumber}: '${lines[0]}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'`,
);
}
function parseUpdateFileChunk(
lines: string[],
lineNumber: number,
allowMissingContext: boolean,
): { chunk: UpdateFileChunk; consumed: number } { if (lines.length === 0) { thrownew Error(
`Invalid patch hunk at line ${lineNumber}: Update hunk does not contain any lines`,
);
}
let changeContext: string | undefined;
let startIndex = 0; if (lines[0] === EMPTY_CHANGE_CONTEXT_MARKER) {
startIndex = 1;
} elseif (lines[0].startsWith(CHANGE_CONTEXT_MARKER)) {
changeContext = lines[0].slice(CHANGE_CONTEXT_MARKER.length);
startIndex = 1;
} elseif (!allowMissingContext) { thrownew Error(
`Invalid patch hunk at line ${lineNumber}: Expected update hunk to start with a @@ context marker, got: '${lines[0]}'`,
);
}
if (startIndex >= lines.length) { thrownew Error(
`Invalid patch hunk at line ${lineNumber + 1}: Update hunk does not contain any lines`,
);
}
let parsedLines = 0; for (const line of lines.slice(startIndex)) { if (line === EOF_MARKER) { if (parsedLines === 0) { thrownew Error(
`Invalid patch hunk at line ${lineNumber + 1}: Update hunk does not contain any lines`,
);
}
chunk.isEndOfFile = true;
parsedLines += 1; break;
}
if (parsedLines === 0) { thrownew Error(
`Invalid patch hunk at line ${lineNumber + 1}: Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`,
);
} break;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.