| | |
| | #include <iostream> |
| | #include <string> |
| | #include <vector> |
| | #include <map> |
| | #include <fstream> |
| | #include <sstream> |
| | #include <cstdlib> |
| | #include <memory> |
| | #include <cstring> |
| | #include <curl/curl.h> |
| | #include <filesystem> |
| | #include <chrono> |
| | #include <thread> |
| | #include <ctime> |
| | #include <iomanip> |
| | #include <cstdio> |
| | #include <algorithm> |
| | #include <cctype> |
| | #include <optional> |
| | #include <unistd.h> |
| | #include <sys/wait.h> |
| | #include <nlohmann/json.hpp> |
| |
|
| | #include "pencil_utils.hpp" |
| |
|
| | using json = nlohmann::json; |
| |
|
| | |
| | static bool debug_enabled = false; |
| |
|
| | |
| | |
| | static time_t last_ollama_time = 0; |
| | const int KEEP_ALIVE_INTERVAL = 120; |
| | const int HEARTBEAT_INTERVAL = 120; |
| |
|
| | |
| | |
| | static std::string last_ai_output; |
| | static std::string last_ai_type; |
| |
|
| | |
| | |
| | class CurlRequest { |
| | CURL* curl; |
| | struct curl_slist* headers; |
| | std::string response; |
| | void cleanup() { |
| | if (headers) curl_slist_free_all(headers); |
| | if (curl) curl_easy_cleanup(curl); |
| | } |
| | public: |
| | CurlRequest() : curl(curl_easy_init()), headers(nullptr) {} |
| | ~CurlRequest() { cleanup(); } |
| | CurlRequest(const CurlRequest&) = delete; |
| | CurlRequest& operator=(const CurlRequest&) = delete; |
| | CurlRequest(CurlRequest&& other) noexcept |
| | : curl(std::exchange(other.curl, nullptr)), |
| | headers(std::exchange(other.headers, nullptr)), |
| | response(std::move(other.response)) {} |
| | CurlRequest& operator=(CurlRequest&& other) noexcept { |
| | if (this != &other) { |
| | cleanup(); |
| | curl = std::exchange(other.curl, nullptr); |
| | headers = std::exchange(other.headers, nullptr); |
| | response = std::move(other.response); |
| | } |
| | return *this; |
| | } |
| |
|
| | bool perform(const std::string& url, const std::string& postdata) { |
| | if (!curl) return false; |
| | headers = curl_slist_append(headers, "Content-Type: application/json"); |
| | curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
| | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str()); |
| | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); |
| | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); |
| | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); |
| | curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); |
| | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L); |
| | CURLcode res = curl_easy_perform(curl); |
| | if (res != CURLE_OK) { |
| | response = "[Error] curl failed: " + std::string(curl_easy_strerror(res)); |
| | return false; |
| | } |
| | long http_code = 0; |
| | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
| | if (http_code != 200) { |
| | response = "[Error] HTTP " + std::to_string(http_code); |
| | return false; |
| | } |
| | return true; |
| | } |
| | const std::string& get_response() const { return response; } |
| | static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *output) { |
| | size_t total = size * nmemb; |
| | output->append((char*)contents, total); |
| | return total; |
| | } |
| | }; |
| |
|
| | |
| | |
| | std::string ask_ollama(const std::string &prompt); |
| | std::string ask_ollama_with_retry(const std::string& prompt, int max_retries = 3); |
| | void check_and_keep_alive(time_t now); |
| | void warm_up_ollama(); |
| |
|
| | |
| | |
| | std::string get_model_name() { |
| | const char* env = std::getenv("OLLAMA_MODEL"); |
| | return env ? env : "qwen2.5:0.5b"; |
| | } |
| |
|
| | |
| | |
| | std::string ask_ollama(const std::string &prompt) { |
| | json request = { |
| | {"model", get_model_name()}, |
| | {"prompt", prompt}, |
| | {"stream", false} |
| | }; |
| | std::string request_str = request.dump(); |
| |
|
| | if (debug_enabled) { |
| | std::cerr << "\n[DEBUG] Request JSON: " << request_str << std::endl; |
| | } |
| |
|
| | CurlRequest req; |
| | if (!req.perform("http://localhost:11434/api/generate", request_str)) { |
| | return req.get_response(); |
| | } |
| |
|
| | std::string response_string = req.get_response(); |
| | if (debug_enabled) { |
| | std::cerr << "[DEBUG] Raw response: " << response_string << std::endl; |
| | } |
| |
|
| | try { |
| | auto response = json::parse(response_string); |
| | if (response.contains("response")) { |
| | return response["response"].get<std::string>(); |
| | } else if (response.contains("error")) { |
| | return "[Error from Ollama] " + response["error"].get<std::string>(); |
| | } else { |
| | return "[Error] No 'response' field in Ollama output."; |
| | } |
| | } catch (const json::parse_error& e) { |
| | return "[Error] Failed to parse Ollama JSON: " + std::string(e.what()); |
| | } |
| | } |
| |
|
| | |
| | |
| | std::string ask_ollama_with_retry(const std::string& prompt, int max_retries) { |
| | int attempt = 0; |
| | int base_delay = 2; |
| | while (attempt < max_retries) { |
| | std::string result = ask_ollama(prompt); |
| | if (result.compare(0, 9, "[Timeout]") == 0) { |
| | attempt++; |
| | if (attempt < max_retries) { |
| | int delay = base_delay * (1 << (attempt - 1)); |
| | std::cerr << "Timeout, retrying in " << delay << " seconds...\n"; |
| | std::this_thread::sleep_for(std::chrono::seconds(delay)); |
| | continue; |
| | } else { |
| | return "[Error] Maximum retries reached, giving up."; |
| | } |
| | } |
| | return result; |
| | } |
| | return "[Error] Maximum retries reached, giving up."; |
| | } |
| |
|
| | |
| | |
| | void check_and_keep_alive(time_t now) { |
| | if (now - last_ollama_time > KEEP_ALIVE_INTERVAL) { |
| | if (debug_enabled) std::cout << "[Keep alive] Sending ping to Ollama.\n"; |
| | ask_ollama("Hello"); |
| | last_ollama_time = now; |
| | } |
| | } |
| |
|
| | |
| | |
| | void warm_up_ollama() { |
| | std::cout << "Warming up Ollama model..." << std::endl; |
| | std::string result = ask_ollama("Hello"); |
| | if (result.compare(0, 7, "[Error]") == 0 || result.compare(0, 9, "[Timeout]") == 0) { |
| | std::cerr << "Warning: Warm-up failed: " << result << std::endl; |
| | std::cerr << "Check that Ollama is running and the model is available.\n"; |
| | } else { |
| | std::cout << "Model ready.\n"; |
| | } |
| | } |
| |
|
| | |
| | |
| | struct CommandResult { |
| | std::string output; |
| | int exit_status; |
| | }; |
| | CommandResult run_command(const std::vector<std::string>& args) { |
| | if (args.empty()) return {"", -1}; |
| | std::vector<char*> argv; |
| | for (const auto& a : args) argv.push_back(const_cast<char*>(a.c_str())); |
| | argv.push_back(nullptr); |
| |
|
| | int pipefd[2]; |
| | if (pipe(pipefd) == -1) return {"pipe() failed", -1}; |
| |
|
| | pid_t pid = fork(); |
| | if (pid == -1) { |
| | close(pipefd[0]); |
| | close(pipefd[1]); |
| | return {"fork() failed", -1}; |
| | } |
| |
|
| | if (pid == 0) { |
| | close(pipefd[0]); |
| | dup2(pipefd[1], STDOUT_FILENO); |
| | dup2(pipefd[1], STDERR_FILENO); |
| | close(pipefd[1]); |
| | execvp(argv[0], argv.data()); |
| | perror("execvp"); |
| | _exit(127); |
| | } |
| |
|
| | close(pipefd[1]); |
| | std::string output; |
| | char buffer[4096]; |
| | ssize_t n; |
| | while ((n = read(pipefd[0], buffer, sizeof(buffer)-1)) > 0) { |
| | buffer[n] = '\0'; |
| | output += buffer; |
| | } |
| | close(pipefd[0]); |
| |
|
| | int status; |
| | waitpid(pid, &status, 0); |
| | int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; |
| | return {output, exit_status}; |
| | } |
| |
|
| | |
| | |
| | bool is_git_repo() { |
| | return std::filesystem::exists(pencil::get_pencil_dir() + ".git"); |
| | } |
| |
|
| | bool init_git_repo() { |
| | if (is_git_repo()) return true; |
| | auto res = run_command({"git", "-C", pencil::get_pencil_dir(), "init"}); |
| | if (res.exit_status != 0) return false; |
| | |
| | run_command({"git", "-C", pencil::get_pencil_dir(), "config", "user.email", "pencilclaw@local"}); |
| | run_command({"git", "-C", pencil::get_pencil_dir(), "config", "user.name", "PencilClaw"}); |
| | return true; |
| | } |
| |
|
| | |
| | CommandResult git_command(const std::vector<std::string>& args) { |
| | std::vector<std::string> cmd = {"git", "-C", pencil::get_pencil_dir()}; |
| | cmd.insert(cmd.end(), args.begin(), args.end()); |
| | return run_command(cmd); |
| | } |
| |
|
| | bool git_commit_file(const std::string& file_path, const std::string& commit_message) { |
| | std::filesystem::path full_path(file_path); |
| | std::string rel_path = std::filesystem::relative(full_path, pencil::get_pencil_dir()).string(); |
| |
|
| | |
| | auto add_res = git_command({"add", rel_path}); |
| | if (add_res.exit_status != 0) { |
| | std::cerr << "Git add failed: " << add_res.output << std::endl; |
| | return false; |
| | } |
| |
|
| | |
| | auto commit_res = git_command({"commit", "-m", commit_message}); |
| | if (commit_res.exit_status != 0) { |
| | |
| | if (commit_res.output.find("nothing to commit") == std::string::npos && |
| | commit_res.output.find("no changes added") == std::string::npos) { |
| | std::cerr << "Git commit failed: " << commit_res.output << std::endl; |
| | return false; |
| | } |
| | } |
| | if (debug_enabled) std::cerr << "[Git] " << commit_res.output << std::endl; |
| | return true; |
| | } |
| |
|
| | |
| | |
| | std::vector<std::string> extract_code_blocks(const std::string &text) { |
| | std::vector<std::string> blocks; |
| | size_t pos = 0; |
| | while (true) { |
| | size_t start = text.find("```", pos); |
| | if (start == std::string::npos) break; |
| | size_t end = text.find("```", start + 3); |
| | if (end == std::string::npos) break; |
| |
|
| | size_t nl = text.find('\n', start); |
| | size_t content_start; |
| | if (nl != std::string::npos && nl < end) { |
| | content_start = nl + 1; |
| | } else { |
| | content_start = start + 3; |
| | } |
| |
|
| | std::string block = text.substr(content_start, end - content_start); |
| | blocks.push_back(block); |
| | pos = end + 3; |
| | } |
| | return blocks; |
| | } |
| |
|
| | |
| | |
| | bool execute_code(const std::string &code) { |
| | std::string tmp_cpp = pencil::get_pencil_dir() + "temp_code.cpp"; |
| | std::string tmp_exe = pencil::get_pencil_dir() + "temp_code"; |
| |
|
| | if (!pencil::save_text(tmp_cpp, code)) { |
| | std::cerr << "Failed to write code to temporary file." << std::endl; |
| | return false; |
| | } |
| |
|
| | auto compile_res = run_command({"g++", "-o", tmp_exe, tmp_cpp}); |
| | if (compile_res.exit_status != 0) { |
| | std::cerr << "Compilation failed:\n" << compile_res.output << std::endl; |
| | std::filesystem::remove(tmp_cpp); |
| | return false; |
| | } |
| |
|
| | auto run_res = run_command({tmp_exe}); |
| | std::cout << "\n[Program exited with code " << run_res.exit_status << "]\n"; |
| | std::cout << run_res.output << std::endl; |
| |
|
| | std::filesystem::remove(tmp_cpp); |
| | std::filesystem::remove(tmp_exe); |
| | return true; |
| | } |
| |
|
| | |
| | |
| | std::string sanitize_and_secure_path(const std::string &input, const std::string &subdir = "") { |
| | std::error_code ec; |
| | std::filesystem::path base = std::filesystem::canonical(pencil::get_pencil_dir(), ec); |
| | if (ec) { |
| | std::cerr << "Error: Cannot resolve base directory.\n"; |
| | return ""; |
| | } |
| | if (!subdir.empty()) base /= subdir; |
| |
|
| | |
| | std::string safe_name; |
| | for (char c : input) { |
| | if (isalnum(c) || c == '.' || c == '-' || c == '_') |
| | safe_name += c; |
| | else |
| | safe_name += '_'; |
| | } |
| | if (safe_name.empty() || safe_name == "." || safe_name == "..") |
| | safe_name = "unnamed"; |
| |
|
| | std::filesystem::path full = base / safe_name; |
| | std::filesystem::path resolved = std::filesystem::canonical(full, ec); |
| | if (ec) { |
| | |
| | std::string abs_full = std::filesystem::absolute(full).string(); |
| | std::string base_str = base.string(); |
| | if (abs_full.compare(0, base_str.size(), base_str) != 0 || |
| | (abs_full.size() > base_str.size() && abs_full[base_str.size()] != '/')) { |
| | return ""; |
| | } |
| | return abs_full; |
| | } |
| |
|
| | std::string resolved_str = resolved.string(); |
| | std::string base_str = base.string(); |
| | if (resolved_str.compare(0, base_str.size(), base_str) != 0 || |
| | (resolved_str.size() > base_str.size() && resolved_str[base_str.size()] != '/')) { |
| | return ""; |
| | } |
| | return resolved_str; |
| | } |
| |
|
| | |
| | |
| | bool save_content_to_file(const std::string& content, const std::string& filename, const std::string& description) { |
| | std::string safe_path = sanitize_and_secure_path(filename); |
| | if (safe_path.empty()) { |
| | std::cerr << "Error: Invalid or insecure filename." << std::endl; |
| | return false; |
| | } |
| |
|
| | std::error_code ec; |
| | std::filesystem::create_directories(std::filesystem::path(safe_path).parent_path(), ec); |
| | if (ec) { |
| | std::cerr << "Error creating directory: " << ec.message() << std::endl; |
| | return false; |
| | } |
| |
|
| | if (!pencil::save_text(safe_path, content)) { |
| | std::cerr << "Error: Failed to write file " << safe_path << std::endl; |
| | return false; |
| | } |
| |
|
| | if (!std::filesystem::exists(safe_path)) { |
| | std::cerr << "Error: File " << safe_path << " does not exist after save." << std::endl; |
| | return false; |
| | } |
| | auto size = std::filesystem::file_size(safe_path); |
| | if (size == 0) { |
| | std::cerr << "Error: File " << safe_path << " is empty." << std::endl; |
| | return false; |
| | } |
| |
|
| | std::cout << "✅ Saved " << description << " to: " << safe_path << " (" << size << " bytes)" << std::endl; |
| |
|
| | |
| | if (is_git_repo()) { |
| | std::string commit_msg = description; |
| | if (commit_msg.length() > 100) commit_msg = commit_msg.substr(0, 100) + "..."; |
| | if (!git_commit_file(safe_path, commit_msg)) { |
| | std::cerr << "Warning: Git commit failed (check your Git configuration).\n"; |
| | } |
| | } |
| | return true; |
| | } |
| |
|
| | |
| | |
| | std::string get_active_task_folder() { |
| | std::ifstream f(pencil::get_active_task_file()); |
| | std::string folder; |
| | std::getline(f, folder); |
| | if (folder.empty()) return ""; |
| |
|
| | std::error_code ec; |
| | std::filesystem::path p = std::filesystem::weakly_canonical(folder, ec); |
| | if (ec) return ""; |
| |
|
| | std::string tasks_dir_canon = std::filesystem::weakly_canonical(pencil::get_tasks_dir()).string(); |
| | std::string p_str = p.string(); |
| | if (p_str.compare(0, tasks_dir_canon.size(), tasks_dir_canon) != 0 || |
| | (p_str.size() > tasks_dir_canon.size() && p_str[tasks_dir_canon.size()] != '/')) { |
| | return ""; |
| | } |
| | return p_str; |
| | } |
| |
|
| | bool set_active_task_folder(const std::string& folder) { |
| | std::ofstream f(pencil::get_active_task_file()); |
| | if (!f) return false; |
| | f << folder; |
| | return !f.fail(); |
| | } |
| |
|
| | void clear_active_task() { |
| | std::filesystem::remove(pencil::get_active_task_file()); |
| | } |
| |
|
| | bool start_new_task(const std::string& description) { |
| | |
| | std::string safe_desc; |
| | for (char c : description) { |
| | if (isalnum(c) || c == ' ' || c == '-') safe_desc += c; |
| | else safe_desc += '_'; |
| | } |
| | if (safe_desc.length() > 30) safe_desc = safe_desc.substr(0, 30); |
| | std::string folder_name = pencil::timestamp() + "_" + safe_desc; |
| | std::string task_folder = pencil::get_tasks_dir() + folder_name + "/"; |
| |
|
| | std::error_code ec; |
| | if (!std::filesystem::create_directories(task_folder, ec) && ec) { |
| | std::cerr << "Failed to create task folder: " << ec.message() << std::endl; |
| | return false; |
| | } |
| |
|
| | |
| | if (!pencil::save_text(task_folder + "description.txt", description)) { |
| | std::cerr << "Failed to save task description.\n"; |
| | return false; |
| | } |
| |
|
| | |
| | std::string log_entry = "Task started at " + pencil::timestamp() + "\nDescription: " + description + "\n\n"; |
| | if (!pencil::save_text(task_folder + "log.txt", log_entry)) { |
| | std::cerr << "Failed to create log file.\n"; |
| | return false; |
| | } |
| |
|
| | if (!set_active_task_folder(task_folder)) { |
| | std::cerr << "Warning: Could not set active task.\n"; |
| | } else { |
| | std::cout << "✅ New task started: \"" << description << "\"\n"; |
| | std::cout << "Task folder: " << task_folder << "\n"; |
| | } |
| |
|
| | pencil::append_to_session("Started new task: " + description); |
| | return true; |
| | } |
| |
|
| | bool continue_task(const std::string& task_folder) { |
| | |
| | auto desc_opt = pencil::read_file(task_folder + "description.txt"); |
| | if (!desc_opt.has_value()) { |
| | std::cerr << "Task description missing.\n"; |
| | return false; |
| | } |
| | std::string description = desc_opt.value(); |
| |
|
| | auto log_opt = pencil::read_file(task_folder + "log.txt"); |
| | std::string log = log_opt.value_or(""); |
| |
|
| | |
| | int iteration = 1; |
| | size_t pos = 0; |
| | while ((pos = log.find("Iteration", pos)) != std::string::npos) { |
| | iteration++; |
| | pos += 9; |
| | } |
| |
|
| | |
| | std::string prompt = "You are a C++ coding agent working on the following task:\n\n" + |
| | description + "\n\n" + |
| | "Previous work log:\n" + log + "\n\n" + |
| | "Generate the next iteration of code or progress. If the task is not yet complete, " |
| | "produce a new C++ code snippet that advances the work. If the task is complete, " |
| | "output a message indicating completion and include no code.\n\n" |
| | "Provide your response with optional explanation, but include any code inside ```cpp ... ``` blocks."; |
| |
|
| | std::cout << "Continuing task (iteration " << iteration << ")...\n"; |
| | std::string response = ask_ollama_with_retry(prompt); |
| | if (response.compare(0, 7, "[Error]") == 0) { |
| | std::cerr << "Failed to generate continuation: " << response << std::endl; |
| | return false; |
| | } |
| |
|
| | |
| | std::string iter_file = task_folder + "iteration_" + std::to_string(iteration) + ".txt"; |
| | if (!pencil::save_text(iter_file, response)) { |
| | std::cerr << "Failed to save iteration.\n"; |
| | return false; |
| | } |
| |
|
| | |
| | std::ofstream log_file(task_folder + "log.txt", std::ios::app); |
| | if (log_file) { |
| | log_file << "\n--- Iteration " << iteration << " (" << pencil::timestamp() << ") ---\n"; |
| | log_file << response << "\n"; |
| | } |
| |
|
| | std::cout << "✅ Iteration " << iteration << " saved to: " << iter_file << "\n"; |
| | last_ai_output = response; |
| | last_ai_type = "task_iteration"; |
| | pencil::append_to_session("Task continued: iteration " + std::to_string(iteration)); |
| |
|
| | |
| | if (is_git_repo()) { |
| | std::string commit_msg = "Task iteration " + std::to_string(iteration) + ": " + description; |
| | if (commit_msg.length() > 100) commit_msg = commit_msg.substr(0, 100) + "..."; |
| | if (!git_commit_file(iter_file, commit_msg)) { |
| | std::cerr << "Warning: Git commit failed.\n"; |
| | } |
| | } |
| | return true; |
| | } |
| |
|
| | |
| | |
| | void run_heartbeat(time_t now) { |
| | check_and_keep_alive(now); |
| | std::string active_task = get_active_task_folder(); |
| | if (!active_task.empty()) { |
| | if (debug_enabled) std::cout << "[Heartbeat] Continuing active task.\n"; |
| | continue_task(active_task); |
| | } |
| | } |
| |
|
| | |
| | |
| | std::string to_lowercase(std::string s) { |
| | std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); |
| | return s; |
| | } |
| |
|
| | bool contains_phrase(const std::string& text, const std::string& phrase) { |
| | std::string lower = to_lowercase(text); |
| | std::string lower_phrase = to_lowercase(phrase); |
| | size_t pos = lower.find(lower_phrase); |
| | while (pos != std::string::npos) { |
| | if ((pos == 0 || !isalnum(lower[pos-1])) && |
| | (pos + lower_phrase.length() == lower.length() || !isalnum(lower[pos + lower_phrase.length()]))) { |
| | return true; |
| | } |
| | pos = lower.find(lower_phrase, pos + 1); |
| | } |
| | return false; |
| | } |
| |
|
| | std::string extract_after(const std::string& input, const std::string& phrase) { |
| | std::string lower_input = to_lowercase(input); |
| | std::string lower_phrase = to_lowercase(phrase); |
| | size_t pos = lower_input.find(lower_phrase); |
| | if (pos == std::string::npos) return ""; |
| | if (pos > 0 && isalnum(lower_input[pos-1])) return ""; |
| | size_t after = pos + phrase.length(); |
| | if (after < lower_input.length() && isalnum(lower_input[after])) return ""; |
| | size_t start = after; |
| | while (start < input.length() && isspace(input[start])) ++start; |
| | std::string result = input.substr(start); |
| | while (!result.empty() && isspace(result.back())) result.pop_back(); |
| | return result; |
| | } |
| |
|
| | std::string extract_quoted(const std::string& input) { |
| | size_t start = input.find('"'); |
| | if (start == std::string::npos) start = input.find('\''); |
| | if (start == std::string::npos) return ""; |
| | size_t end = input.find(input[start], start + 1); |
| | if (end == std::string::npos) return ""; |
| | return input.substr(start + 1, end - start - 1); |
| | } |
| |
|
| | std::string extract_filename(const std::string& line) { |
| | std::string quoted = extract_quoted(line); |
| | if (!quoted.empty()) return quoted; |
| |
|
| | std::string lower = to_lowercase(line); |
| | size_t as_pos = lower.find(" as "); |
| | if (as_pos != std::string::npos) { |
| | std::string after = line.substr(as_pos + 4); |
| | size_t start = after.find_first_not_of(" \t"); |
| | if (start != std::string::npos) { |
| | after = after.substr(start); |
| | size_t end = after.find_first_of(" \t\n\r,;"); |
| | if (end != std::string::npos) after = after.substr(0, end); |
| | return after; |
| | } |
| | } |
| | return ""; |
| | } |
| |
|
| | |
| | |
| | void handle_code(const std::string& idea) { |
| | std::string prompt = "Write C++ code to accomplish the following task. Provide only the code without explanations unless requested. Include necessary headers and a main function if appropriate.\n\n" + idea; |
| | std::cout << "Asking Ollama...\n"; |
| | std::string response = ask_ollama_with_retry(prompt); |
| | std::cout << "\n" << response << "\n"; |
| |
|
| | last_ai_output = response; |
| | last_ai_type = "code"; |
| |
|
| | std::string base = idea; |
| | if (base.length() > 50) base = base.substr(0, 50); |
| | |
| | save_content_to_file(response, base + ".txt", "code for \"" + idea + "\""); |
| | pencil::append_to_session("User asked for code: " + idea); |
| | pencil::append_to_session("Assistant: " + response); |
| | } |
| |
|
| | |
| | |
| | bool handle_natural_language(const std::string& line) { |
| | |
| | if (contains_phrase(line, "save it") || contains_phrase(line, "save the code") || |
| | contains_phrase(line, "write it to a file") || contains_phrase(line, "save as")) { |
| |
|
| | if (debug_enabled) std::cout << "[NLU] Matched save request.\n"; |
| |
|
| | if (last_ai_output.empty()) { |
| | std::cout << "I don't have any recent code to save.\n"; |
| | return true; |
| | } |
| |
|
| | std::string default_name = "code.txt"; |
| | std::string filename = extract_filename(line); |
| | if (filename.empty()) { |
| | std::cout << "What filename would you like to save it as? (default: " << default_name << ")\n> "; |
| | std::getline(std::cin, filename); |
| | if (filename.empty()) filename = default_name; |
| | } |
| |
|
| | if (filename.find('.') == std::string::npos) filename += ".txt"; |
| |
|
| | save_content_to_file(last_ai_output, filename, "code"); |
| | return true; |
| | } |
| |
|
| | |
| | std::vector<std::pair<std::string, std::string>> code_triggers = { |
| | {"write code for", "for"}, |
| | {"write a program that", "that"}, |
| | {"generate code for", "for"}, |
| | {"generate a program that", "that"}, |
| | {"create code for", "for"}, |
| | {"create a program that", "that"}, |
| | {"write a function that", "that"}, |
| | {"code for", "for"} |
| | }; |
| | for (const auto& [trigger, _] : code_triggers) { |
| | if (contains_phrase(line, trigger)) { |
| | if (debug_enabled) std::cout << "[NLU] Matched code trigger: " << trigger << "\n"; |
| | std::string idea = extract_after(line, trigger); |
| | if (idea.empty()) idea = extract_quoted(line); |
| | if (idea.empty()) { |
| | std::cout << "What should the code do?\n> "; |
| | std::getline(std::cin, idea); |
| | } |
| | if (!idea.empty()) handle_code(idea); |
| | return true; |
| | } |
| | } |
| | |
| | std::vector<std::string> generic_code = { |
| | "write code", "generate code", "create code", "write a program", "generate a program" |
| | }; |
| | for (const auto& trigger : generic_code) { |
| | if (contains_phrase(line, trigger)) { |
| | if (debug_enabled) std::cout << "[NLU] Matched generic code trigger: " << trigger << "\n"; |
| | std::cout << "What should the code do?\n> "; |
| | std::string idea; |
| | std::getline(std::cin, idea); |
| | if (!idea.empty()) handle_code(idea); |
| | return true; |
| | } |
| | } |
| |
|
| | |
| | std::vector<std::pair<std::string, std::string>> task_triggers = { |
| | {"start a task to", "to"}, |
| | {"begin a task to", "to"}, |
| | {"create a task to", "to"}, |
| | {"start a task that", "that"}, |
| | {"begin a task that", "that"}, |
| | {"create a task that", "that"} |
| | }; |
| | for (const auto& [trigger, _] : task_triggers) { |
| | if (contains_phrase(line, trigger)) { |
| | if (debug_enabled) std::cout << "[NLU] Matched task trigger: " << trigger << "\n"; |
| | std::string desc = extract_after(line, trigger); |
| | if (desc.empty()) desc = extract_quoted(line); |
| | if (desc.empty()) { |
| | std::cout << "Describe the task:\n> "; |
| | std::getline(std::cin, desc); |
| | } |
| | if (!desc.empty()) start_new_task(desc); |
| | return true; |
| | } |
| | } |
| | |
| | std::vector<std::string> generic_task = { |
| | "start a task", "begin a task", "create a task", "new task" |
| | }; |
| | for (const auto& trigger : generic_task) { |
| | if (contains_phrase(line, trigger)) { |
| | if (debug_enabled) std::cout << "[NLU] Matched generic task trigger: " << trigger << "\n"; |
| | std::cout << "Describe the task:\n> "; |
| | std::string desc; |
| | std::getline(std::cin, desc); |
| | if (!desc.empty()) start_new_task(desc); |
| | return true; |
| | } |
| | } |
| |
|
| | return false; |
| | } |
| |
|
| | |
| | |
| | void list_files() { |
| | std::cout << "\n📁 Files in " << std::filesystem::absolute(pencil::get_pencil_dir()).string() << ":\n"; |
| | try { |
| | for (const auto& entry : std::filesystem::directory_iterator(pencil::get_pencil_dir())) { |
| | if (entry.is_regular_file() && entry.path().extension() == ".txt") { |
| | std::cout << " " << entry.path().filename().string() |
| | << " (" << entry.file_size() << " bytes)\n"; |
| | } |
| | } |
| | if (std::filesystem::exists(pencil::get_tasks_dir())) { |
| | std::cout << "\n📂 Tasks:\n"; |
| | for (const auto& entry : std::filesystem::directory_iterator(pencil::get_tasks_dir())) { |
| | if (entry.is_directory()) { |
| | std::cout << " " << entry.path().filename().string() << "/\n"; |
| | |
| | } |
| | } |
| | } |
| | } catch (const std::filesystem::filesystem_error& e) { |
| | std::cerr << "Error listing files: " << e.what() << std::endl; |
| | } |
| | } |
| |
|
| | |
| | int main() { |
| | if (!pencil::init_workspace()) { |
| | std::cerr << "Fatal error: cannot create workspace directory." << std::endl; |
| | return 1; |
| | } |
| |
|
| | std::cout << "📁 Workspace: " << std::filesystem::absolute(pencil::get_pencil_dir()).string() << "\n"; |
| |
|
| | |
| | if (!init_git_repo()) { |
| | std::cerr << "Warning: Could not initialise Git repository. Git features disabled.\n"; |
| | } else { |
| | std::cout << "Git repository initialised (or already exists).\n"; |
| | } |
| |
|
| | warm_up_ollama(); |
| | if (last_ollama_time == 0) last_ollama_time = time(nullptr); |
| |
|
| | std::cout << "PENCILCLAW – C++ Coding Agent with Git integration\n"; |
| | std::cout << "Heartbeat interval: " << HEARTBEAT_INTERVAL << " seconds\n"; |
| | std::cout << "Type /HELP for commands.\n"; |
| |
|
| | std::string last_response; |
| | time_t last_heartbeat_run = time(nullptr); |
| |
|
| | while (true) { |
| | time_t now = time(nullptr); |
| | check_and_keep_alive(now); |
| |
|
| | std::cout << "\n> "; |
| | std::string line; |
| | std::getline(std::cin, line); |
| | if (line.empty()) continue; |
| |
|
| | if (line[0] != '/') { |
| | if (handle_natural_language(line)) { |
| | if (now - last_heartbeat_run >= HEARTBEAT_INTERVAL) { |
| | run_heartbeat(now); |
| | last_heartbeat_run = now; |
| | } |
| | continue; |
| | } |
| | } |
| |
|
| | if (line[0] == '/') { |
| | std::string cmd; |
| | std::string arg; |
| | size_t sp = line.find(' '); |
| | if (sp == std::string::npos) { |
| | cmd = line; |
| | } else { |
| | cmd = line.substr(0, sp); |
| | arg = line.substr(sp + 1); |
| | } |
| |
|
| | if (cmd == "/EXIT") { |
| | break; |
| | } |
| | else if (cmd == "/HELP") { |
| | std::cout << "Available commands:\n"; |
| | std::cout << " /HELP – this help\n"; |
| | std::cout << " /CODE <idea> – generate C++ code for a task\n"; |
| | std::cout << " /TASK <description> – start a new autonomous coding task\n"; |
| | std::cout << " /TASK_STATUS – show current active task\n"; |
| | std::cout << " /STOP_TASK – clear active task\n"; |
| | std::cout << " /EXECUTE – compile & run code from last output\n"; |
| | std::cout << " /FILES – list all saved files and tasks\n"; |
| | std::cout << " /DEBUG – toggle debug output\n"; |
| | std::cout << " /EXIT – quit\n"; |
| | std::cout << "\nNatural language examples:\n"; |
| | std::cout << " 'write code for a fibonacci function'\n"; |
| | std::cout << " 'start a task to build a calculator'\n"; |
| | std::cout << " 'save it as mycode.txt' (after code generation)\n"; |
| | } |
| | else if (cmd == "/DEBUG") { |
| | debug_enabled = !debug_enabled; |
| | std::cout << "Debug mode " << (debug_enabled ? "enabled" : "disabled") << ".\n"; |
| | } |
| | else if (cmd == "/CODE") { |
| | if (arg.empty()) { |
| | std::cout << "Please provide a description of the code.\n"; |
| | continue; |
| | } |
| | handle_code(arg); |
| | } |
| | else if (cmd == "/TASK") { |
| | if (arg.empty()) { |
| | std::cout << "Please provide a task description.\n"; |
| | continue; |
| | } |
| | start_new_task(arg); |
| | } |
| | else if (cmd == "/TASK_STATUS") { |
| | std::string folder = get_active_task_folder(); |
| | if (folder.empty()) { |
| | std::cout << "No active task.\n"; |
| | } else { |
| | auto desc_opt = pencil::read_file(folder + "description.txt"); |
| | std::string desc = desc_opt.value_or("unknown"); |
| | std::cout << "Active task: " << desc << "\n"; |
| | std::cout << "Folder: " << folder << "\n"; |
| | |
| | int count = 0; |
| | for (const auto& entry : std::filesystem::directory_iterator(folder)) { |
| | if (entry.path().filename().string().rfind("iteration_", 0) == 0) |
| | count++; |
| | } |
| | std::cout << "Iterations so far: " << count << "\n"; |
| | } |
| | } |
| | else if (cmd == "/STOP_TASK") { |
| | clear_active_task(); |
| | std::cout << "Active task cleared.\n"; |
| | } |
| | else if (cmd == "/FILES") { |
| | list_files(); |
| | } |
| | else if (cmd == "/EXECUTE") { |
| | if (last_ai_output.empty()) { |
| | std::cout << "No previous AI output to execute from.\n"; |
| | continue; |
| | } |
| | auto blocks = extract_code_blocks(last_ai_output); |
| | if (blocks.empty()) { |
| | std::cout << "No code blocks found in last output.\n"; |
| | continue; |
| | } |
| | std::cout << "--- Code to execute ---\n"; |
| | std::cout << blocks[0] << "\n"; |
| | std::cout << "------------------------\n"; |
| | std::cout << "WARNING: This code was generated by an AI and may be unsafe.\n"; |
| | std::cout << "Type 'yes' to confirm execution (any other input cancels): "; |
| | std::string confirm; |
| | std::getline(std::cin, confirm); |
| | if (confirm != "yes") { |
| | std::cout << "Execution cancelled.\n"; |
| | continue; |
| | } |
| | std::cout << "Executing code block...\n"; |
| | if (execute_code(blocks[0])) { |
| | std::cout << "Execution finished.\n"; |
| | } else { |
| | std::cout << "Execution failed.\n"; |
| | } |
| | } |
| | else { |
| | std::cout << "Unknown command. Type /HELP for list.\n"; |
| | } |
| | } else { |
| | |
| | std::cout << "Sending to Ollama...\n"; |
| | last_response = ask_ollama_with_retry(line); |
| | last_ai_output = last_response; |
| | last_ai_type = "free"; |
| | std::cout << last_response << "\n"; |
| | pencil::append_to_session("User: " + line); |
| | pencil::append_to_session("Assistant: " + last_response); |
| | } |
| |
|
| | time_t now2 = time(nullptr); |
| | if (now2 - last_heartbeat_run >= HEARTBEAT_INTERVAL) { |
| | run_heartbeat(now2); |
| | last_heartbeat_run = now2; |
| | } |
| | } |
| |
|
| | return 0; |
| | } |
| |
|