801 lines
29 KiB
C++
801 lines
29 KiB
C++
#include "server-tools.h"
|
|
|
|
#include <sheredom/subprocess.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <climits>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
//
|
|
// internal helpers
|
|
//
|
|
|
|
static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {
|
|
std::vector<char *> r;
|
|
r.reserve(v.size() + 1);
|
|
for (const auto & s : v) {
|
|
r.push_back(const_cast<char *>(s.c_str()));
|
|
}
|
|
r.push_back(nullptr);
|
|
return r;
|
|
}
|
|
|
|
struct run_proc_result {
|
|
std::string output;
|
|
int exit_code = -1;
|
|
bool timed_out = false;
|
|
};
|
|
|
|
static run_proc_result run_process(
|
|
const std::vector<std::string> & args,
|
|
size_t max_output,
|
|
int timeout_secs) {
|
|
run_proc_result res;
|
|
|
|
subprocess_s proc;
|
|
auto argv = to_cstr_vec(args);
|
|
|
|
int options = subprocess_option_no_window
|
|
| subprocess_option_combined_stdout_stderr
|
|
| subprocess_option_inherit_environment
|
|
| subprocess_option_search_user_path;
|
|
|
|
if (subprocess_create(argv.data(), options, &proc) != 0) {
|
|
res.output = "failed to spawn process";
|
|
return res;
|
|
}
|
|
|
|
std::atomic<bool> done{false};
|
|
std::atomic<bool> timed_out{false};
|
|
|
|
std::thread timeout_thread([&]() {
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_secs);
|
|
while (!done.load()) {
|
|
if (std::chrono::steady_clock::now() >= deadline) {
|
|
timed_out.store(true);
|
|
subprocess_terminate(&proc);
|
|
return;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
});
|
|
|
|
FILE * f = subprocess_stdout(&proc);
|
|
std::string output;
|
|
bool truncated = false;
|
|
if (f) {
|
|
char buf[4096];
|
|
while (fgets(buf, sizeof(buf), f) != nullptr) {
|
|
if (!truncated) {
|
|
size_t len = strlen(buf);
|
|
if (output.size() + len <= max_output) {
|
|
output.append(buf, len);
|
|
} else {
|
|
output.append(buf, max_output - output.size());
|
|
truncated = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
done.store(true);
|
|
if (timeout_thread.joinable()) {
|
|
timeout_thread.join();
|
|
}
|
|
|
|
subprocess_join(&proc, &res.exit_code);
|
|
subprocess_destroy(&proc);
|
|
|
|
res.output = output;
|
|
res.timed_out = timed_out.load();
|
|
if (truncated) {
|
|
res.output += "\n[output truncated]";
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// simple glob: * matches non-/ chars, ** matches anything including /
|
|
static bool glob_match(const char * pattern, const char * str) {
|
|
if (*pattern == '\0') {
|
|
return *str == '\0';
|
|
}
|
|
if (pattern[0] == '*' && pattern[1] == '*') {
|
|
const char * p = pattern + 2;
|
|
if (*p == '/') p++;
|
|
if (glob_match(p, str)) return true;
|
|
if (*str != '\0') return glob_match(pattern, str + 1);
|
|
return false;
|
|
}
|
|
if (*pattern == '*') {
|
|
const char * p = pattern + 1;
|
|
for (; *str != '\0' && *str != '/'; str++) {
|
|
if (glob_match(p, str)) return true;
|
|
}
|
|
return glob_match(p, str);
|
|
}
|
|
if (*pattern == '?' && *str != '\0' && *str != '/') {
|
|
return glob_match(pattern + 1, str + 1);
|
|
}
|
|
if (*pattern == *str) {
|
|
return glob_match(pattern + 1, str + 1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool glob_match(const std::string & pattern, const std::string & str) {
|
|
return glob_match(pattern.c_str(), str.c_str());
|
|
}
|
|
|
|
json server_tool::to_json() {
|
|
return {
|
|
{"display_name", display_name},
|
|
{"tool", name},
|
|
{"type", "builtin"},
|
|
{"permissions", json{
|
|
{"write", permission_write}
|
|
}},
|
|
{"definition", get_definition()},
|
|
};
|
|
}
|
|
|
|
//
|
|
// read_file: read a file with optional line range and line-number prefix
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_READ_FILE_MAX_SIZE = 16 * 1024; // 16 KB
|
|
|
|
struct server_tool_read_file : server_tool {
|
|
server_tool_read_file() {
|
|
name = "read_file";
|
|
display_name = "Read file";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Read the contents of a file. Optionally specify a 1-based line range. "
|
|
"If append_loc is true, each line is prefixed with its line number (e.g. \"1\u2192 ...\")."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path to the file"}}},
|
|
{"start_line", {{"type", "integer"}, {"description", "First line to read, 1-based (default: 1)"}}},
|
|
{"end_line", {{"type", "integer"}, {"description", "Last line to read, 1-based inclusive (default: end of file)"}}},
|
|
{"append_loc", {{"type", "boolean"}, {"description", "Prefix each line with its line number"}}},
|
|
}},
|
|
{"required", json::array({"path"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
int start_line = json_value(params, "start_line", 1);
|
|
int end_line = json_value(params, "end_line", -1); // -1 = no limit
|
|
bool append_loc = json_value(params, "append_loc", false);
|
|
|
|
std::error_code ec;
|
|
uintmax_t file_size = fs::file_size(path, ec);
|
|
if (ec) {
|
|
return {{"error", "cannot stat file: " + ec.message()}};
|
|
}
|
|
if (file_size > SERVER_TOOL_READ_FILE_MAX_SIZE && end_line == -1) {
|
|
return {{"error", string_format(
|
|
"file too large (%zu bytes, max %zu). Use start_line/end_line to read a portion.",
|
|
(size_t)file_size, SERVER_TOOL_READ_FILE_MAX_SIZE)}};
|
|
}
|
|
|
|
std::ifstream f(path);
|
|
if (!f) {
|
|
return {{"error", "failed to open file: " + path}};
|
|
}
|
|
|
|
std::string result;
|
|
std::string line;
|
|
int lineno = 0;
|
|
|
|
while (std::getline(f, line)) {
|
|
lineno++;
|
|
if (lineno < start_line) continue;
|
|
if (end_line != -1 && lineno > end_line) break;
|
|
|
|
std::string out_line;
|
|
if (append_loc) {
|
|
out_line = std::to_string(lineno) + "\u2192 " + line + "\n";
|
|
} else {
|
|
out_line = line + "\n";
|
|
}
|
|
|
|
if (result.size() + out_line.size() > SERVER_TOOL_READ_FILE_MAX_SIZE) {
|
|
result += "[output truncated]";
|
|
break;
|
|
}
|
|
result += out_line;
|
|
}
|
|
|
|
return {{"plain_text_response", result}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// file_glob_search: find files matching a glob pattern under a base directory
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;
|
|
|
|
struct server_tool_file_glob_search : server_tool {
|
|
server_tool_file_glob_search() {
|
|
name = "file_glob_search";
|
|
display_name = "File search";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Recursively search for files matching a glob pattern under a directory."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Base directory to search in"}}},
|
|
{"include", {{"type", "string"}, {"description", "Glob pattern for files to include (e.g. \"**/*.cpp\"). Default: **"}}},
|
|
{"exclude", {{"type", "string"}, {"description", "Glob pattern for files to exclude"}}},
|
|
}},
|
|
{"required", json::array({"path"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string base = params.at("path").get<std::string>();
|
|
std::string include = json_value(params, "include", std::string("**"));
|
|
std::string exclude = json_value(params, "exclude", std::string(""));
|
|
|
|
std::ostringstream output_text;
|
|
size_t count = 0;
|
|
|
|
std::error_code ec;
|
|
for (const auto & entry : fs::recursive_directory_iterator(base,
|
|
fs::directory_options::skip_permission_denied, ec)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
|
|
std::string rel = fs::relative(entry.path(), base, ec).string();
|
|
if (ec) continue;
|
|
std::replace(rel.begin(), rel.end(), '\\', '/');
|
|
|
|
if (!glob_match(include, rel)) continue;
|
|
if (!exclude.empty() && glob_match(exclude, rel)) continue;
|
|
|
|
output_text << entry.path().string() << "\n";
|
|
if (++count >= SERVER_TOOL_FILE_SEARCH_MAX_RESULTS) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
output_text << "\n---\nTotal matches: " << count << "\n";
|
|
|
|
return {{"plain_text_response", output_text.str()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// grep_search: search for a regex pattern in files
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_GREP_SEARCH_MAX_RESULTS = 100;
|
|
|
|
struct server_tool_grep_search : server_tool {
|
|
server_tool_grep_search() {
|
|
name = "grep_search";
|
|
display_name = "Grep search";
|
|
permission_write = false;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Search for a regex pattern in files under a path. Returns matching lines."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "File or directory to search in"}}},
|
|
{"pattern", {{"type", "string"}, {"description", "Regular expression pattern to search for"}}},
|
|
{"include", {{"type", "string"}, {"description", "Glob pattern to filter files (default: **)"}}},
|
|
{"exclude", {{"type", "string"}, {"description", "Glob pattern to exclude files"}}},
|
|
{"return_line_numbers", {{"type", "boolean"}, {"description", "If true, include line numbers in results"}}},
|
|
}},
|
|
{"required", json::array({"path", "pattern"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
std::string pat_str = params.at("pattern").get<std::string>();
|
|
std::string include = json_value(params, "include", std::string("**"));
|
|
std::string exclude = json_value(params, "exclude", std::string(""));
|
|
bool show_lineno = json_value(params, "return_line_numbers", false);
|
|
|
|
std::regex pattern;
|
|
try {
|
|
pattern = std::regex(pat_str);
|
|
} catch (const std::regex_error & e) {
|
|
return {{"error", std::string("invalid regex: ") + e.what()}};
|
|
}
|
|
|
|
std::ostringstream output_text;
|
|
size_t total = 0;
|
|
|
|
auto search_file = [&](const fs::path & fpath) {
|
|
std::ifstream f(fpath);
|
|
if (!f) return;
|
|
std::string line;
|
|
int lineno = 0;
|
|
while (std::getline(f, line) && total < SERVER_TOOL_GREP_SEARCH_MAX_RESULTS) {
|
|
lineno++;
|
|
if (std::regex_search(line, pattern)) {
|
|
output_text << fpath.string() << ":";
|
|
if (show_lineno) {
|
|
output_text << lineno << ":";
|
|
}
|
|
output_text << line << "\n";
|
|
total++;
|
|
}
|
|
}
|
|
};
|
|
|
|
std::error_code ec;
|
|
if (fs::is_regular_file(path, ec)) {
|
|
search_file(path);
|
|
} else if (fs::is_directory(path, ec)) {
|
|
for (const auto & entry : fs::recursive_directory_iterator(path,
|
|
fs::directory_options::skip_permission_denied, ec)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
if (total >= SERVER_TOOL_GREP_SEARCH_MAX_RESULTS) break;
|
|
|
|
std::string rel = fs::relative(entry.path(), path, ec).string();
|
|
if (ec) continue;
|
|
std::replace(rel.begin(), rel.end(), '\\', '/');
|
|
|
|
if (!glob_match(include, rel)) continue;
|
|
if (!exclude.empty() && glob_match(exclude, rel)) continue;
|
|
|
|
search_file(entry.path());
|
|
}
|
|
} else {
|
|
return {{"error", "path does not exist: " + path}};
|
|
}
|
|
|
|
output_text << "\n\n---\nTotal matches: " << total << "\n";
|
|
|
|
return {{"plain_text_response", output_text.str()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// exec_shell_command: run an arbitrary shell command
|
|
//
|
|
|
|
static constexpr size_t SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE = 16 * 1024; // 16 KB
|
|
static constexpr int SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT = 60; // seconds
|
|
|
|
struct server_tool_exec_shell_command : server_tool {
|
|
server_tool_exec_shell_command() {
|
|
name = "exec_shell_command";
|
|
display_name = "Execute shell command";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Execute a shell command and return its output (stdout and stderr combined)."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"command", {{"type", "string"}, {"description", "Shell command to execute"}}},
|
|
{"timeout", {{"type", "integer"}, {"description", string_format("Timeout in seconds (default 10, max %d)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT)}}},
|
|
{"max_output_size", {{"type", "integer"}, {"description", string_format("Maximum output size in bytes (default %zu)", SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE)}}},
|
|
}},
|
|
{"required", json::array({"command"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string command = params.at("command").get<std::string>();
|
|
int timeout = json_value(params, "timeout", 10);
|
|
size_t max_output = (size_t) json_value(params, "max_output_size", (int) SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);
|
|
|
|
timeout = std::min(timeout, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_TIMEOUT);
|
|
max_output = std::min(max_output, SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE);
|
|
|
|
#ifdef _WIN32
|
|
std::vector<std::string> args = {"cmd", "/c", command};
|
|
#else
|
|
std::vector<std::string> args = {"sh", "-c", command};
|
|
#endif
|
|
|
|
auto res = run_process(args, max_output, timeout);
|
|
|
|
std::string text_output = res.output;
|
|
text_output += string_format("\n[exit code: %d]", res.exit_code);
|
|
if (res.timed_out) {
|
|
text_output += " [exit due to timed out]";
|
|
}
|
|
|
|
return {{"plain_text_response", text_output}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// write_file: create or overwrite a file
|
|
//
|
|
|
|
struct server_tool_write_file : server_tool {
|
|
server_tool_write_file() {
|
|
name = "write_file";
|
|
display_name = "Write file";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Write content to a file, creating it (including parent directories) if it does not exist. May use with edit_file for more complex edits."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path of the file to write"}}},
|
|
{"content", {{"type", "string"}, {"description", "Content to write"}}},
|
|
}},
|
|
{"required", json::array({"path", "content"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
std::string content = params.at("content").get<std::string>();
|
|
|
|
std::error_code ec;
|
|
fs::path fpath(path);
|
|
if (fpath.has_parent_path()) {
|
|
fs::create_directories(fpath.parent_path(), ec);
|
|
if (ec) {
|
|
return {{"error", "failed to create directories: " + ec.message()}};
|
|
}
|
|
}
|
|
|
|
std::ofstream f(path, std::ios::binary);
|
|
if (!f) {
|
|
return {{"error", "failed to open file for writing: " + path}};
|
|
}
|
|
f << content;
|
|
if (!f) {
|
|
return {{"error", "failed to write file: " + path}};
|
|
}
|
|
|
|
return {{"result", "file written successfully"}, {"path", path}, {"bytes", content.size()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// edit_file: edit file content via line-based changes
|
|
//
|
|
|
|
struct server_tool_edit_file : server_tool {
|
|
server_tool_edit_file() {
|
|
name = "edit_file";
|
|
display_name = "Edit file";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description",
|
|
"Edit a file by applying a list of line-based changes. "
|
|
"Each change targets a 1-based inclusive line range and has a mode: "
|
|
"\"replace\" (replace lines with content), "
|
|
"\"delete\" (remove lines, content must be empty string), "
|
|
"\"append\" (insert content after line_end). "
|
|
"Set line_start to -1 to target the end of file (line_end is ignored in that case). "
|
|
"Changes must not overlap. They are applied in reverse line order automatically."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {{"type", "string"}, {"description", "Path to the file to edit"}}},
|
|
{"changes", {
|
|
{"type", "array"},
|
|
{"description", "List of changes to apply"},
|
|
{"items", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"mode", {{"type", "string"}, {"description", "\"replace\", \"delete\", or \"append\""}}},
|
|
{"line_start", {{"type", "integer"}, {"description", "First line of the range (1-based); use -1 for end of file"}}},
|
|
{"line_end", {{"type", "integer"}, {"description", "Last line of the range (1-based, inclusive); ignored when line_start is -1"}}},
|
|
{"content", {{"type", "string"}, {"description", "Content to insert; must be empty string for delete mode"}}},
|
|
}},
|
|
{"required", json::array({"mode", "line_start", "line_end", "content"})},
|
|
}},
|
|
}},
|
|
}},
|
|
{"required", json::array({"path", "changes"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string path = params.at("path").get<std::string>();
|
|
const json & changes = params.at("changes");
|
|
|
|
if (!changes.is_array()) {
|
|
return {{"error", "\"changes\" must be an array"}};
|
|
}
|
|
|
|
// read file into lines
|
|
std::ifstream fin(path);
|
|
if (!fin) {
|
|
return {{"error", "failed to open file: " + path}};
|
|
}
|
|
std::vector<std::string> lines;
|
|
{
|
|
std::string line;
|
|
while (std::getline(fin, line)) {
|
|
lines.push_back(line);
|
|
}
|
|
}
|
|
fin.close();
|
|
|
|
// validate and collect changes, then sort descending by line_start
|
|
struct change_entry {
|
|
std::string mode;
|
|
int line_start; // 1-based
|
|
int line_end; // 1-based inclusive
|
|
std::string content;
|
|
};
|
|
std::vector<change_entry> entries;
|
|
entries.reserve(changes.size());
|
|
|
|
for (const auto & ch : changes) {
|
|
change_entry e;
|
|
e.mode = ch.at("mode").get<std::string>();
|
|
e.line_start = ch.at("line_start").get<int>();
|
|
e.line_end = ch.at("line_end").get<int>();
|
|
e.content = ch.at("content").get<std::string>();
|
|
|
|
if (e.mode != "replace" && e.mode != "delete" && e.mode != "append") {
|
|
return {{"error", "invalid mode \"" + e.mode + "\"; must be replace, delete, or append"}};
|
|
}
|
|
if (e.mode == "delete" && !e.content.empty()) {
|
|
return {{"error", "content must be empty string for delete mode"}};
|
|
}
|
|
int n = (int) lines.size();
|
|
if (e.line_start == -1) {
|
|
// -1 means end of file; line_end is ignored — normalize to point past last line
|
|
e.line_start = n + 1;
|
|
e.line_end = n + 1;
|
|
} else {
|
|
if (e.line_start < 1 || e.line_end < e.line_start) {
|
|
return {{"error", string_format("invalid line range [%d, %d]", e.line_start, e.line_end)}};
|
|
}
|
|
if (e.line_end > n) {
|
|
return {{"error", string_format("line_end %d exceeds file length %d", e.line_end, n)}};
|
|
}
|
|
}
|
|
entries.push_back(std::move(e));
|
|
}
|
|
|
|
// sort descending so earlier-indexed changes don't shift later ones
|
|
std::sort(entries.begin(), entries.end(), [](const change_entry & a, const change_entry & b) {
|
|
return a.line_start > b.line_start;
|
|
});
|
|
|
|
// apply changes (0-based indices internally)
|
|
for (const auto & e : entries) {
|
|
int idx_start = e.line_start - 1; // 0-based
|
|
int idx_end = e.line_end - 1; // 0-based inclusive
|
|
|
|
// split content into lines (preserve trailing newline awareness)
|
|
std::vector<std::string> new_lines;
|
|
if (!e.content.empty()) {
|
|
std::istringstream ss(e.content);
|
|
std::string ln;
|
|
while (std::getline(ss, ln)) {
|
|
new_lines.push_back(ln);
|
|
}
|
|
// if content ends with \n, getline consumed it — no extra empty line needed
|
|
// if content does NOT end with \n, last line is still captured correctly
|
|
}
|
|
|
|
if (e.mode == "replace") {
|
|
// erase [idx_start, idx_end] and insert new_lines
|
|
lines.erase(lines.begin() + idx_start, lines.begin() + idx_end + 1);
|
|
lines.insert(lines.begin() + idx_start, new_lines.begin(), new_lines.end());
|
|
} else if (e.mode == "delete") {
|
|
lines.erase(lines.begin() + idx_start, lines.begin() + idx_end + 1);
|
|
} else { // append
|
|
// idx_end + 1 may equal lines.size() when line_start == -1 (end of file)
|
|
lines.insert(lines.begin() + idx_end + 1, new_lines.begin(), new_lines.end());
|
|
}
|
|
}
|
|
|
|
// write file back
|
|
std::ofstream fout(path, std::ios::binary);
|
|
if (!fout) {
|
|
return {{"error", "failed to open file for writing: " + path}};
|
|
}
|
|
for (size_t i = 0; i < lines.size(); i++) {
|
|
fout << lines[i];
|
|
if (i + 1 < lines.size()) {
|
|
fout << "\n";
|
|
}
|
|
}
|
|
if (!lines.empty()) {
|
|
fout << "\n";
|
|
}
|
|
if (!fout) {
|
|
return {{"error", "failed to write file: " + path}};
|
|
}
|
|
|
|
return {{"result", "file edited successfully"}, {"path", path}, {"lines", (int) lines.size()}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// apply_diff: apply a unified diff via git apply
|
|
//
|
|
|
|
struct server_tool_apply_diff : server_tool {
|
|
server_tool_apply_diff() {
|
|
name = "apply_diff";
|
|
display_name = "Apply diff";
|
|
permission_write = true;
|
|
}
|
|
|
|
json get_definition() override {
|
|
return {
|
|
{"type", "function"},
|
|
{"function", {
|
|
{"name", name},
|
|
{"description", "Apply a unified diff to edit one or more files using git apply. Use this instead of edit_file when the changes are complex."},
|
|
{"parameters", {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"diff", {{"type", "string"}, {"description", "Unified diff content in git diff format"}}},
|
|
}},
|
|
{"required", json::array({"diff"})},
|
|
}},
|
|
}},
|
|
};
|
|
}
|
|
|
|
json invoke(json params) override {
|
|
std::string diff = params.at("diff").get<std::string>();
|
|
|
|
// write diff to a temporary file
|
|
static std::atomic<int> counter{0};
|
|
std::string tmp_path = (fs::temp_directory_path() /
|
|
("llama_patch_" + std::to_string(++counter) + ".patch")).string();
|
|
|
|
{
|
|
std::ofstream f(tmp_path, std::ios::binary);
|
|
if (!f) {
|
|
return {{"error", "failed to create temp patch file"}};
|
|
}
|
|
f << diff;
|
|
}
|
|
|
|
auto res = run_process({"git", "apply", tmp_path}, 4096, 10);
|
|
|
|
std::error_code ec;
|
|
fs::remove(tmp_path, ec);
|
|
|
|
if (res.exit_code != 0) {
|
|
return {{"error", "git apply failed (exit " + std::to_string(res.exit_code) + "): " + res.output}};
|
|
}
|
|
return {{"result", "patch applied successfully"}};
|
|
}
|
|
};
|
|
|
|
//
|
|
// public API
|
|
//
|
|
|
|
static std::vector<std::unique_ptr<server_tool>> build_tools() {
|
|
std::vector<std::unique_ptr<server_tool>> tools;
|
|
tools.push_back(std::make_unique<server_tool_read_file>());
|
|
tools.push_back(std::make_unique<server_tool_file_glob_search>());
|
|
tools.push_back(std::make_unique<server_tool_grep_search>());
|
|
tools.push_back(std::make_unique<server_tool_exec_shell_command>());
|
|
tools.push_back(std::make_unique<server_tool_write_file>());
|
|
tools.push_back(std::make_unique<server_tool_edit_file>());
|
|
tools.push_back(std::make_unique<server_tool_apply_diff>());
|
|
return tools;
|
|
}
|
|
|
|
void server_tools::setup(const std::vector<std::string> & enabled_tools) {
|
|
if (!enabled_tools.empty()) {
|
|
std::unordered_set<std::string> enabled_set(enabled_tools.begin(), enabled_tools.end());
|
|
auto all_tools = build_tools();
|
|
|
|
tools.clear();
|
|
for (auto & t : all_tools) {
|
|
if (enabled_set.count(t->name) > 0 || enabled_set.count("all") > 0) {
|
|
tools.push_back(std::move(t));
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_get = [this](const server_http_req &) -> server_http_res_ptr {
|
|
auto res = std::make_unique<server_http_res>();
|
|
try {
|
|
json result = json::array();
|
|
for (const auto & t : tools) {
|
|
result.push_back(t->to_json());
|
|
}
|
|
res->data = safe_json_to_str(result);
|
|
} catch (const std::exception & e) {
|
|
SRV_ERR("got exception: %s\n", e.what());
|
|
res->status = 500;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));
|
|
}
|
|
return res;
|
|
};
|
|
|
|
handle_post = [this](const server_http_req & req) -> server_http_res_ptr {
|
|
auto res = std::make_unique<server_http_res>();
|
|
try {
|
|
json body = json::parse(req.body);
|
|
std::string tool_name = body.at("tool").get<std::string>();
|
|
json params = body.value("params", json::object());
|
|
json result = invoke(tool_name, params);
|
|
res->data = safe_json_to_str(result);
|
|
} catch (const json::exception & e) {
|
|
res->status = 400;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST));
|
|
} catch (const std::exception & e) {
|
|
SRV_ERR("got exception: %s\n", e.what());
|
|
res->status = 500;
|
|
res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_SERVER));
|
|
}
|
|
return res;
|
|
};
|
|
}
|
|
|
|
json server_tools::invoke(const std::string & name, const json & params) {
|
|
for (auto & t : tools) {
|
|
if (t->name == name) {
|
|
return t->invoke(params);
|
|
}
|
|
}
|
|
return {{"error", "unknown tool: " + name}};
|
|
}
|