From 439cdfcfd1d47bd65f67f67538b2854617fb075a Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Mon, 11 Aug 2025 17:12:03 +0200 Subject: [PATCH] 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. --- cli.js | 18 +- tools/patch_files.js | 675 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 tools/patch_files.js diff --git a/cli.js b/cli.js index a12f816..b908333 100644 --- a/cli.js +++ b/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); } diff --git a/tools/patch_files.js b/tools/patch_files.js new file mode 100644 index 0000000..87851e2 --- /dev/null +++ b/tools/patch_files.js @@ -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; + } +} \ No newline at end of file