#include "server-tools.h" #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; // // internal helpers // static std::vector to_cstr_vec(const std::vector & v) { std::vector r; r.reserve(v.size() + 1); for (const auto & s : v) { r.push_back(const_cast(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 & 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 done{false}; std::atomic 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(); 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 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 pat_str = params.at("pattern").get(); 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(); 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 args = {"cmd", "/c", command}; #else std::vector 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 content = params.at("content").get(); 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(); 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 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 entries; entries.reserve(changes.size()); for (const auto & ch : changes) { change_entry e; e.mode = ch.at("mode").get(); e.line_start = ch.at("line_start").get(); e.line_end = ch.at("line_end").get(); e.content = ch.at("content").get(); 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 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(); // write diff to a temporary file static std::atomic 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> build_tools() { std::vector> tools; tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); tools.push_back(std::make_unique()); return tools; } void server_tools::setup(const std::vector & enabled_tools) { if (!enabled_tools.empty()) { std::unordered_set 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(); 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(); try { json body = json::parse(req.body); std::string tool_name = body.at("tool").get(); 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}}; }