Update CLI tool to modify user prompt for file creation, change model to 'gpt-5-mini', and adjust verbosity levels for text and reasoning. Enable output logging for function call results and deltas.
This commit is contained in:
18
cli.js
18
cli.js
@@ -23,8 +23,7 @@ async function loadTools() {
|
||||
return Object.fromEntries(toolEntries);
|
||||
}
|
||||
|
||||
|
||||
streamOnce(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), 'Zeig mir die Dateiein in /');
|
||||
streamOnce(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), 'Zeig mir die Dateiein in / und lege index.html an mit dummydaten, kurz');
|
||||
|
||||
async function streamOnce(openai, userText) {
|
||||
const toolsByFile = await loadTools();
|
||||
@@ -39,10 +38,10 @@ async function streamOnce(openai, userText) {
|
||||
console.log('input:', input.length);
|
||||
|
||||
const call = {
|
||||
model: 'gpt-5-nano',
|
||||
model: 'gpt-5-mini',
|
||||
input: input,
|
||||
text: { format: { type: 'text' }, verbosity: 'low' },
|
||||
reasoning: { effort: 'high', summary: 'detailed' },
|
||||
text: { format: { type: 'text' }, verbosity: 'medium' },
|
||||
reasoning: { effort: 'low', summary: 'detailed' },
|
||||
tools: Object.values(toolsByFile).map(t => t.def),
|
||||
store: true,
|
||||
}
|
||||
@@ -55,10 +54,10 @@ async function streamOnce(openai, userText) {
|
||||
}
|
||||
});
|
||||
stream.on('response.reasoning_summary_text.delta', (event) => {
|
||||
//process.stdout.write(event.delta);
|
||||
process.stdout.write(event.delta);
|
||||
});
|
||||
stream.on('response.reasoning_summary_text.done', () => {
|
||||
//process.stdout.write('\n');
|
||||
process.stdout.write('\n');
|
||||
//clear on next delta
|
||||
});
|
||||
|
||||
@@ -73,7 +72,7 @@ async function streamOnce(openai, userText) {
|
||||
}
|
||||
});
|
||||
stream.on('response.function_call_arguments.delta', (event) => {
|
||||
//process.stdout.write(event.delta);
|
||||
process.stdout.write(event.delta);
|
||||
});
|
||||
|
||||
const functionCalls = [];
|
||||
@@ -88,6 +87,7 @@ async function streamOnce(openai, userText) {
|
||||
} catch (e){
|
||||
console.error('Error parsing arguments:', e, event.item.arguments);
|
||||
}
|
||||
console.log('function call:', id, name);
|
||||
functionCalls.push({ id, name, args, promise: toolsByFile[name].run(args) });
|
||||
}
|
||||
});
|
||||
@@ -107,7 +107,7 @@ async function streamOnce(openai, userText) {
|
||||
call_id: call.id,
|
||||
output: JSON.stringify(result),
|
||||
})
|
||||
//console.log('function call result:', call,result);
|
||||
console.log('function call result:', call,result);
|
||||
} catch (err) {
|
||||
console.error('Error in function call:', call.name, err);
|
||||
}
|
||||
|
||||
675
tools/patch_files.js
Normal file
675
tools/patch_files.js
Normal file
@@ -0,0 +1,675 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* A self-contained JavaScript utility for applying human-readable
|
||||
* "pseudo-diff" patch files to a collection of text files.
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Domain objects
|
||||
// --------------------------------------------------------------------------- //
|
||||
class ActionType {
|
||||
static ADD = "add";
|
||||
static DELETE = "delete";
|
||||
static UPDATE = "update";
|
||||
}
|
||||
|
||||
class FileChange {
|
||||
constructor(type, old_content = null, new_content = null, move_path = null) {
|
||||
this.type = type;
|
||||
this.old_content = old_content;
|
||||
this.new_content = new_content;
|
||||
this.move_path = move_path;
|
||||
}
|
||||
}
|
||||
|
||||
class Commit {
|
||||
constructor() {
|
||||
this.changes = {};
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Exceptions
|
||||
// --------------------------------------------------------------------------- //
|
||||
class DiffError extends Error {
|
||||
/** Any problem detected while parsing or applying a patch. */
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "DiffError";
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Helper classes used while parsing patches
|
||||
// --------------------------------------------------------------------------- //
|
||||
class Chunk {
|
||||
constructor(orig_index = -1, del_lines = [], ins_lines = []) {
|
||||
this.orig_index = orig_index;
|
||||
this.del_lines = del_lines;
|
||||
this.ins_lines = ins_lines;
|
||||
}
|
||||
}
|
||||
|
||||
class PatchAction {
|
||||
constructor(type, new_file = null, chunks = [], move_path = null) {
|
||||
this.type = type;
|
||||
this.new_file = new_file;
|
||||
this.chunks = chunks;
|
||||
this.move_path = move_path;
|
||||
}
|
||||
}
|
||||
|
||||
class Patch {
|
||||
constructor() {
|
||||
this.actions = {};
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Patch text parser
|
||||
// --------------------------------------------------------------------------- //
|
||||
class Parser {
|
||||
constructor(current_files, lines) {
|
||||
this.current_files = current_files;
|
||||
this.lines = lines;
|
||||
this.index = 0;
|
||||
this.patch = new Patch();
|
||||
this.fuzz = 0;
|
||||
}
|
||||
|
||||
// ------------- low-level helpers -------------------------------------- //
|
||||
_cur_line() {
|
||||
if (this.index >= this.lines.length) {
|
||||
throw new DiffError("Unexpected end of input while parsing patch");
|
||||
}
|
||||
return this.lines[this.index];
|
||||
}
|
||||
|
||||
static _norm(line) {
|
||||
/** Strip CR so comparisons work for both LF and CRLF input. */
|
||||
return line.replace(/\r$/, "");
|
||||
}
|
||||
|
||||
// ------------- scanning convenience ----------------------------------- //
|
||||
is_done(prefixes = null) {
|
||||
if (this.index >= this.lines.length) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
prefixes &&
|
||||
prefixes.length > 0
|
||||
) {
|
||||
const currentLine = Parser._norm(this._cur_line());
|
||||
for (const prefix of prefixes) {
|
||||
if (currentLine.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
startswith(prefix) {
|
||||
return Parser._norm(this._cur_line()).startsWith(prefix);
|
||||
}
|
||||
|
||||
read_str(prefix) {
|
||||
/**
|
||||
* Consume the current line if it starts with *prefix* and return the text
|
||||
* **after** the prefix. Raises if prefix is empty.
|
||||
*/
|
||||
if (prefix === "") {
|
||||
throw new Error("read_str() requires a non-empty prefix");
|
||||
}
|
||||
if (Parser._norm(this._cur_line()).startsWith(prefix)) {
|
||||
const text = this._cur_line().substring(prefix.length);
|
||||
this.index += 1;
|
||||
return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
read_line() {
|
||||
/** Return the current raw line and advance. */
|
||||
const line = this._cur_line();
|
||||
this.index += 1;
|
||||
return line;
|
||||
}
|
||||
|
||||
// ------------- public entry point -------------------------------------- //
|
||||
parse() {
|
||||
while (!this.is_done(["*** End Patch"])) {
|
||||
// ---------- UPDATE ---------- //
|
||||
let path = this.read_str("*** Update File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate update for file: ${path}`);
|
||||
}
|
||||
const move_to = this.read_str("*** Move to: ");
|
||||
if (!(path in this.current_files)) {
|
||||
throw new DiffError(`Update File Error - missing file: ${path}`);
|
||||
}
|
||||
const text = this.current_files[path];
|
||||
const action = this._parse_update_file(text);
|
||||
action.move_path = move_to || null;
|
||||
this.patch.actions[path] = action;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- DELETE ---------- //
|
||||
path = this.read_str("*** Delete File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate delete for file: ${path}`);
|
||||
}
|
||||
if (!(path in this.current_files)) {
|
||||
throw new DiffError(`Delete File Error - missing file: ${path}`);
|
||||
}
|
||||
this.patch.actions[path] = new PatchAction(ActionType.DELETE);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- ADD ---------- //
|
||||
path = this.read_str("*** Add File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate add for file: ${path}`);
|
||||
}
|
||||
if (path in this.current_files) {
|
||||
throw new DiffError(`Add File Error - file already exists: ${path}`);
|
||||
}
|
||||
this.patch.actions[path] = this._parse_add_file();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new DiffError(`Unknown line while parsing: ${this._cur_line()}`);
|
||||
}
|
||||
|
||||
if (!this.startswith("*** End Patch")) {
|
||||
throw new DiffError("Missing *** End Patch sentinel");
|
||||
}
|
||||
this.index += 1; // consume sentinel
|
||||
}
|
||||
|
||||
// ------------- section parsers ---------------------------------------- //
|
||||
_parse_update_file(text) {
|
||||
const action = new PatchAction(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 def_str = this.read_str("@@ ");
|
||||
let section_str = "";
|
||||
if (!def_str && Parser._norm(this._cur_line()) === "@@") {
|
||||
section_str = this.read_line();
|
||||
}
|
||||
|
||||
if (!(def_str || section_str || index === 0)) {
|
||||
throw new DiffError(`Invalid line in update section:\n${this._cur_line()}`);
|
||||
}
|
||||
|
||||
if (def_str && def_str.trim()) {
|
||||
let found = false;
|
||||
if (!lines.slice(0, index).includes(def_str)) {
|
||||
for (let i = index; i < lines.length; i++) {
|
||||
if (lines[i] === def_str) {
|
||||
index = i + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found && !lines.slice(0, index).map(s => s.trim()).includes(def_str.trim())) {
|
||||
for (let i = index; i < lines.length; i++) {
|
||||
if (lines[i].trim() === def_str.trim()) {
|
||||
index = i + 1;
|
||||
this.fuzz += 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [next_ctx, chunks, end_idx, eof] = peek_next_section(this.lines, this.index);
|
||||
const [new_index, fuzz] = find_context(lines, next_ctx, index, eof);
|
||||
if (new_index === -1) {
|
||||
const ctx_txt = next_ctx.join("\n");
|
||||
throw new DiffError(
|
||||
`Invalid ${eof ? 'EOF ' : ''}context at ${index}:\n${ctx_txt}`
|
||||
);
|
||||
}
|
||||
this.fuzz += fuzz;
|
||||
for (const ch of chunks) {
|
||||
ch.orig_index += new_index;
|
||||
action.chunks.push(ch);
|
||||
}
|
||||
index = new_index + next_ctx.length;
|
||||
this.index = end_idx;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
_parse_add_file() {
|
||||
const lines = [];
|
||||
while (!this.is_done([
|
||||
"*** End Patch",
|
||||
"*** Update File:",
|
||||
"*** Delete File:",
|
||||
"*** Add File:"
|
||||
])) {
|
||||
const s = this.read_line();
|
||||
if (!s.startsWith("+")) {
|
||||
throw new DiffError(`Invalid Add File line (missing '+'): ${s}`);
|
||||
}
|
||||
lines.push(s.substring(1)); // strip leading '+'
|
||||
}
|
||||
return new PatchAction(ActionType.ADD, lines.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Helper functions
|
||||
// --------------------------------------------------------------------------- //
|
||||
function find_context_core(lines, context, start) {
|
||||
if (!context || context.length === 0) {
|
||||
return [start, 0];
|
||||
}
|
||||
|
||||
// Exact match
|
||||
for (let i = start; i <= lines.length - context.length; i++) {
|
||||
let match = true;
|
||||
for (let j = 0; j < context.length; j++) {
|
||||
if (lines[i + j] !== context[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return [i, 0];
|
||||
}
|
||||
}
|
||||
|
||||
// Rstrip match
|
||||
for (let i = start; i <= lines.length - context.length; i++) {
|
||||
let match = true;
|
||||
for (let j = 0; j < context.length; j++) {
|
||||
if (lines[i + j].replace(/\s+$/, "") !== context[j].replace(/\s+$/, "")) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return [i, 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Trim match
|
||||
for (let i = start; i <= lines.length - context.length; i++) {
|
||||
let match = true;
|
||||
for (let j = 0; j < context.length; j++) {
|
||||
if (lines[i + j].trim() !== context[j].trim()) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return [i, 100];
|
||||
}
|
||||
}
|
||||
|
||||
return [-1, 0];
|
||||
}
|
||||
|
||||
function find_context(lines, context, start, eof) {
|
||||
if (eof) {
|
||||
let [new_index, fuzz] = find_context_core(lines, context, Math.max(0, 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 + 10000];
|
||||
}
|
||||
return find_context_core(lines, context, start);
|
||||
}
|
||||
|
||||
function peek_next_section(lines, index) {
|
||||
const old = [];
|
||||
let del_lines = [];
|
||||
let ins_lines = [];
|
||||
const chunks = [];
|
||||
let mode = "keep";
|
||||
const orig_index = index;
|
||||
|
||||
while (index < lines.length) {
|
||||
let s = lines[index];
|
||||
if (s.startsWith("@@") ||
|
||||
s === "*** End Patch" ||
|
||||
s.startsWith("*** Update File:") ||
|
||||
s.startsWith("*** Delete File:") ||
|
||||
s.startsWith("*** Add File:") ||
|
||||
s === "*** End of File") {
|
||||
break;
|
||||
}
|
||||
if (s === "***") {
|
||||
break;
|
||||
}
|
||||
if (s.startsWith("***")) {
|
||||
throw new DiffError(`Invalid Line: ${s}`);
|
||||
}
|
||||
index += 1;
|
||||
|
||||
const last_mode = mode;
|
||||
if (s === "") {
|
||||
s = " ";
|
||||
}
|
||||
if (s[0] === "+") {
|
||||
mode = "add";
|
||||
} else if (s[0] === "-") {
|
||||
mode = "delete";
|
||||
} else if (s[0] === " ") {
|
||||
mode = "keep";
|
||||
} else {
|
||||
throw new DiffError(`Invalid Line: ${s}`);
|
||||
}
|
||||
s = s.substring(1);
|
||||
|
||||
if (mode === "keep" && last_mode !== mode) {
|
||||
if (ins_lines.length > 0 || del_lines.length > 0) {
|
||||
chunks.push(
|
||||
new Chunk(
|
||||
old.length - del_lines.length,
|
||||
[...del_lines],
|
||||
[...ins_lines]
|
||||
)
|
||||
);
|
||||
}
|
||||
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 > 0 || del_lines.length > 0) {
|
||||
chunks.push(
|
||||
new Chunk(
|
||||
old.length - del_lines.length,
|
||||
[...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");
|
||||
}
|
||||
return [old, chunks, index, false];
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Patch → Commit and Commit application
|
||||
// --------------------------------------------------------------------------- //
|
||||
function _get_updated_file(text, action, path) {
|
||||
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 = [];
|
||||
let orig_index = 0;
|
||||
|
||||
for (const chunk of action.chunks) {
|
||||
if (chunk.orig_index > orig_lines.length) {
|
||||
throw new DiffError(
|
||||
`${path}: chunk.orig_index ${chunk.orig_index} exceeds file length`
|
||||
);
|
||||
}
|
||||
if (orig_index > chunk.orig_index) {
|
||||
throw new DiffError(
|
||||
`${path}: overlapping chunks at ${orig_index} > ${chunk.orig_index}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add lines before the chunk
|
||||
for (let i = orig_index; i < chunk.orig_index; i++) {
|
||||
dest_lines.push(orig_lines[i]);
|
||||
}
|
||||
orig_index = chunk.orig_index;
|
||||
|
||||
// Add inserted lines
|
||||
dest_lines.push(...chunk.ins_lines);
|
||||
|
||||
// Skip deleted lines
|
||||
orig_index += chunk.del_lines.length;
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
for (let i = orig_index; i < orig_lines.length; i++) {
|
||||
dest_lines.push(orig_lines[i]);
|
||||
}
|
||||
|
||||
return dest_lines.join("\n");
|
||||
}
|
||||
|
||||
function patch_to_commit(patch, orig) {
|
||||
const commit = new Commit();
|
||||
for (const [path, action] of Object.entries(patch.actions)) {
|
||||
if (action.type === ActionType.DELETE) {
|
||||
commit.changes[path] = new FileChange(
|
||||
ActionType.DELETE,
|
||||
orig[path],
|
||||
null,
|
||||
null
|
||||
);
|
||||
} else if (action.type === ActionType.ADD) {
|
||||
if (action.new_file === null) {
|
||||
throw new DiffError("ADD action without file content");
|
||||
}
|
||||
commit.changes[path] = new FileChange(
|
||||
ActionType.ADD,
|
||||
null,
|
||||
action.new_file,
|
||||
null
|
||||
);
|
||||
} else if (action.type === ActionType.UPDATE) {
|
||||
const new_content = _get_updated_file(orig[path], action, path);
|
||||
commit.changes[path] = new FileChange(
|
||||
ActionType.UPDATE,
|
||||
orig[path],
|
||||
new_content,
|
||||
action.move_path
|
||||
);
|
||||
}
|
||||
}
|
||||
return commit;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// User-facing helpers
|
||||
// --------------------------------------------------------------------------- //
|
||||
function text_to_patch(text, orig) {
|
||||
// Handle different line ending scenarios
|
||||
let lines = text.split(/\r?\n/);
|
||||
|
||||
// Remove trailing empty line if it exists (from trailing newline)
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
lines = lines.slice(0, -1);
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log("Lines count:", lines.length);
|
||||
console.log("First line:", JSON.stringify(lines[0]));
|
||||
if (lines.length > 0) {
|
||||
console.log("Last line:", JSON.stringify(lines[lines.length - 1]));
|
||||
console.log("First line normalized:", JSON.stringify(Parser._norm(lines[0])));
|
||||
console.log("Last line normalized:", JSON.stringify(Parser._norm(lines[lines.length - 1])));
|
||||
}
|
||||
|
||||
if (
|
||||
lines.length < 2 ||
|
||||
!Parser._norm(lines[0]).startsWith("*** Begin Patch") ||
|
||||
(lines.length > 0 && Parser._norm(lines[lines.length - 1]) !== "*** End Patch")
|
||||
) {
|
||||
throw new DiffError("Invalid patch text - missing sentinels");
|
||||
}
|
||||
|
||||
const parser = new Parser(orig, lines);
|
||||
parser.index = 1;
|
||||
parser.parse();
|
||||
return [parser.patch, parser.fuzz];
|
||||
}
|
||||
|
||||
function identify_files_needed(text) {
|
||||
// Handle line splitting consistently
|
||||
let lines = text.split(/\r?\n/);
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
lines = lines.slice(0, -1);
|
||||
}
|
||||
|
||||
const updateFiles = lines
|
||||
.filter(line => line.startsWith("*** Update File: "))
|
||||
.map(line => line.substring("*** Update File: ".length));
|
||||
|
||||
const deleteFiles = lines
|
||||
.filter(line => line.startsWith("*** Delete File: "))
|
||||
.map(line => line.substring("*** Delete File: ".length));
|
||||
|
||||
return [...updateFiles, ...deleteFiles];
|
||||
}
|
||||
|
||||
function identify_files_added(text) {
|
||||
// Handle line splitting consistently
|
||||
let lines = text.split(/\r?\n/);
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
lines = lines.slice(0, -1);
|
||||
}
|
||||
|
||||
return lines
|
||||
.filter(line => line.startsWith("*** Add File: "))
|
||||
.map(line => line.substring("*** Add File: ".length));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// File-system helpers
|
||||
// --------------------------------------------------------------------------- //
|
||||
function load_files(paths, open_fn) {
|
||||
const result = {};
|
||||
for (const path of paths) {
|
||||
result[path] = open_fn(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function apply_commit(commit, write_fn, remove_fn) {
|
||||
for (const [path, change] of Object.entries(commit.changes)) {
|
||||
if (change.type === ActionType.DELETE) {
|
||||
remove_fn(path);
|
||||
} else if (change.type === ActionType.ADD) {
|
||||
if (change.new_content === null) {
|
||||
throw new DiffError(`ADD change for ${path} has no content`);
|
||||
}
|
||||
write_fn(path, change.new_content);
|
||||
} else if (change.type === ActionType.UPDATE) {
|
||||
if (change.new_content === null) {
|
||||
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
||||
}
|
||||
const target = change.move_path || path;
|
||||
write_fn(target, change.new_content);
|
||||
if (change.move_path) {
|
||||
remove_fn(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function process_patch(text, open_fn, write_fn, remove_fn) {
|
||||
if (!text.startsWith("*** Begin Patch")) {
|
||||
throw new DiffError("Patch text must start with *** Begin Patch");
|
||||
}
|
||||
const paths = identify_files_needed(text);
|
||||
const orig = load_files(paths, open_fn);
|
||||
const [patch, _fuzz] = text_to_patch(text, orig);
|
||||
const commit = patch_to_commit(patch, orig);
|
||||
apply_commit(commit, write_fn, remove_fn);
|
||||
return "Done!";
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Default FS helpers
|
||||
// --------------------------------------------------------------------------- //
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function open_file(filepath) {
|
||||
return fs.readFileSync(filepath, 'utf8');
|
||||
}
|
||||
|
||||
function write_file(filepath, content) {
|
||||
const dir = path.dirname(filepath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filepath, content, 'utf8');
|
||||
}
|
||||
|
||||
function remove_file(filepath) {
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.unlinkSync(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for the new interface
|
||||
export default {
|
||||
type: "function",
|
||||
name: "patch_files",
|
||||
description: "Apply a unified diff patch to files within a chroot directory, with option to reverse the patch",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
patch: {
|
||||
type: "string",
|
||||
description: "The unidiff patch string to apply.",
|
||||
}
|
||||
},
|
||||
required: ["patch"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true,
|
||||
};
|
||||
|
||||
export async function run(args) {
|
||||
console.log('patch_files:', args);
|
||||
|
||||
try {
|
||||
const result = process_patch(
|
||||
args.patch,
|
||||
open_file,
|
||||
write_file,
|
||||
remove_file
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof DiffError) {
|
||||
throw new Error(`Patch error: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user