webxos commited on
Commit
fc8a1d1
Β·
verified Β·
1 Parent(s): d24d3af

Upload 3 files

Browse files
Files changed (3) hide show
  1. Makefile +16 -0
  2. pencil_utils.hpp +84 -0
  3. pencilclaw.cpp +971 -0
Makefile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Makefile for PencilClaw
2
+ CXX = g++
3
+ CXXFLAGS = -std=c++17 -Wall -Wextra -O2
4
+ LDFLAGS = -lcurl
5
+
6
+ TARGET = pencilclaw
7
+ SOURCES = pencilclaw.cpp
8
+ HEADERS = pencil_utils.hpp
9
+
10
+ $(TARGET): $(SOURCES) $(HEADERS)
11
+ $(CXX) $(CXXFLAGS) -o $@ $(SOURCES) $(LDFLAGS)
12
+
13
+ clean:
14
+ rm -f $(TARGET)
15
+
16
+ .PHONY: clean
pencil_utils.hpp ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pencil_utils.hpp – Core file utilities for PencilClaw coding agent
2
+ #ifndef PENCIL_UTILS_HPP
3
+ #define PENCIL_UTILS_HPP
4
+
5
+ #include <iostream>
6
+ #include <fstream>
7
+ #include <string>
8
+ #include <filesystem>
9
+ #include <optional>
10
+ #include <chrono>
11
+ #include <iomanip>
12
+ #include <sstream>
13
+
14
+ namespace pencil {
15
+ // Base directory – can be overridden by environment variable PENCIL_DATA
16
+ inline std::string get_pencil_dir() {
17
+ const char* env = std::getenv("PENCIL_DATA");
18
+ return env ? env : "./pencil_data/";
19
+ }
20
+
21
+ inline std::string get_session_log() {
22
+ return get_pencil_dir() + "session.log";
23
+ }
24
+
25
+ inline std::string get_tasks_dir() {
26
+ return get_pencil_dir() + "tasks/";
27
+ }
28
+
29
+ inline std::string get_active_task_file() {
30
+ return get_pencil_dir() + "active_task.txt";
31
+ }
32
+
33
+ // Ensure the working directory exists. Returns true on success or if already exists.
34
+ inline bool init_workspace() {
35
+ std::error_code ec;
36
+ bool created = std::filesystem::create_directory(get_pencil_dir(), ec);
37
+ if (ec) {
38
+ std::cerr << "Error creating directory " << get_pencil_dir() << ": " << ec.message() << std::endl;
39
+ return false;
40
+ }
41
+ // Also create tasks directory
42
+ std::filesystem::create_directory(get_tasks_dir(), ec);
43
+ return true;
44
+ }
45
+
46
+ // Append a line to the session log. Returns true on success.
47
+ inline bool append_to_session(const std::string& text) {
48
+ std::ofstream log(get_session_log(), std::ios::app);
49
+ if (!log) return false;
50
+ log << text << std::endl;
51
+ return !log.fail();
52
+ }
53
+
54
+ // Read entire file content. Returns std::nullopt if file cannot be opened.
55
+ inline std::optional<std::string> read_file(const std::string& path) {
56
+ std::ifstream f(path);
57
+ if (!f) {
58
+ std::cerr << "Warning: Could not open file: " << path << std::endl;
59
+ return std::nullopt;
60
+ }
61
+ std::string content((std::istreambuf_iterator<char>(f)),
62
+ std::istreambuf_iterator<char>());
63
+ return content;
64
+ }
65
+
66
+ // Save text to a file (overwrite). Returns true on success.
67
+ inline bool save_text(const std::string& path, const std::string& text) {
68
+ std::ofstream f(path);
69
+ if (!f) return false;
70
+ f << text;
71
+ return !f.fail();
72
+ }
73
+
74
+ // Get a timestamp string for folder/file names.
75
+ inline std::string timestamp() {
76
+ auto now = std::chrono::system_clock::now();
77
+ auto in_time_t = std::chrono::system_clock::to_time_t(now);
78
+ std::stringstream ss;
79
+ ss << std::put_time(std::localtime(&in_time_t), "%Y%m%d_%H%M%S");
80
+ return ss.str();
81
+ }
82
+ }
83
+
84
+ #endif // PENCIL_UTILS_HPP
pencilclaw.cpp ADDED
@@ -0,0 +1,971 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pencilclaw.cpp – C++ coding agent with autonomous task mode and Git integration
2
+ #include <iostream>
3
+ #include <string>
4
+ #include <vector>
5
+ #include <map>
6
+ #include <fstream>
7
+ #include <sstream>
8
+ #include <cstdlib>
9
+ #include <memory>
10
+ #include <cstring>
11
+ #include <curl/curl.h>
12
+ #include <filesystem>
13
+ #include <chrono>
14
+ #include <thread>
15
+ #include <ctime>
16
+ #include <iomanip>
17
+ #include <cstdio>
18
+ #include <algorithm>
19
+ #include <cctype>
20
+ #include <optional>
21
+ #include <unistd.h>
22
+ #include <sys/wait.h>
23
+ #include <nlohmann/json.hpp>
24
+
25
+ #include "pencil_utils.hpp"
26
+
27
+ using json = nlohmann::json;
28
+
29
+ // Global debug flag
30
+ static bool debug_enabled = false;
31
+
32
+ // ----------------------------------------------------------------------
33
+ // Keep‑alive and heartbeat timing
34
+ static time_t last_ollama_time = 0;
35
+ const int KEEP_ALIVE_INTERVAL = 120;
36
+ const int HEARTBEAT_INTERVAL = 120;
37
+
38
+ // ----------------------------------------------------------------------
39
+ // Last AI output (for saving, executing, etc.)
40
+ static std::string last_ai_output;
41
+ static std::string last_ai_type; // "code", "task_iteration", "free"
42
+
43
+ // ----------------------------------------------------------------------
44
+ // RAII wrapper for libcurl (with move semantics)
45
+ class CurlRequest {
46
+ CURL* curl;
47
+ struct curl_slist* headers;
48
+ std::string response;
49
+ void cleanup() {
50
+ if (headers) curl_slist_free_all(headers);
51
+ if (curl) curl_easy_cleanup(curl);
52
+ }
53
+ public:
54
+ CurlRequest() : curl(curl_easy_init()), headers(nullptr) {}
55
+ ~CurlRequest() { cleanup(); }
56
+ CurlRequest(const CurlRequest&) = delete;
57
+ CurlRequest& operator=(const CurlRequest&) = delete;
58
+ CurlRequest(CurlRequest&& other) noexcept
59
+ : curl(std::exchange(other.curl, nullptr)),
60
+ headers(std::exchange(other.headers, nullptr)),
61
+ response(std::move(other.response)) {}
62
+ CurlRequest& operator=(CurlRequest&& other) noexcept {
63
+ if (this != &other) {
64
+ cleanup();
65
+ curl = std::exchange(other.curl, nullptr);
66
+ headers = std::exchange(other.headers, nullptr);
67
+ response = std::move(other.response);
68
+ }
69
+ return *this;
70
+ }
71
+
72
+ bool perform(const std::string& url, const std::string& postdata) {
73
+ if (!curl) return false;
74
+ headers = curl_slist_append(headers, "Content-Type: application/json");
75
+ curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
76
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str());
77
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
78
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
79
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
80
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
81
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
82
+ CURLcode res = curl_easy_perform(curl);
83
+ if (res != CURLE_OK) {
84
+ response = "[Error] curl failed: " + std::string(curl_easy_strerror(res));
85
+ return false;
86
+ }
87
+ long http_code = 0;
88
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
89
+ if (http_code != 200) {
90
+ response = "[Error] HTTP " + std::to_string(http_code);
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ const std::string& get_response() const { return response; }
96
+ static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *output) {
97
+ size_t total = size * nmemb;
98
+ output->append((char*)contents, total);
99
+ return total;
100
+ }
101
+ };
102
+
103
+ // ----------------------------------------------------------------------
104
+ // Forward declarations
105
+ std::string ask_ollama(const std::string &prompt);
106
+ std::string ask_ollama_with_retry(const std::string& prompt, int max_retries = 3);
107
+ void check_and_keep_alive(time_t now);
108
+ void warm_up_ollama();
109
+
110
+ // ----------------------------------------------------------------------
111
+ // Get model name from environment or default
112
+ std::string get_model_name() {
113
+ const char* env = std::getenv("OLLAMA_MODEL");
114
+ return env ? env : "qwen2.5:0.5b";
115
+ }
116
+
117
+ // ----------------------------------------------------------------------
118
+ // Send prompt to Ollama, return the generated text or an error string.
119
+ std::string ask_ollama(const std::string &prompt) {
120
+ json request = {
121
+ {"model", get_model_name()},
122
+ {"prompt", prompt},
123
+ {"stream", false}
124
+ };
125
+ std::string request_str = request.dump();
126
+
127
+ if (debug_enabled) {
128
+ std::cerr << "\n[DEBUG] Request JSON: " << request_str << std::endl;
129
+ }
130
+
131
+ CurlRequest req;
132
+ if (!req.perform("http://localhost:11434/api/generate", request_str)) {
133
+ return req.get_response(); // contains error message
134
+ }
135
+
136
+ std::string response_string = req.get_response();
137
+ if (debug_enabled) {
138
+ std::cerr << "[DEBUG] Raw response: " << response_string << std::endl;
139
+ }
140
+
141
+ try {
142
+ auto response = json::parse(response_string);
143
+ if (response.contains("response")) {
144
+ return response["response"].get<std::string>();
145
+ } else if (response.contains("error")) {
146
+ return "[Error from Ollama] " + response["error"].get<std::string>();
147
+ } else {
148
+ return "[Error] No 'response' field in Ollama output.";
149
+ }
150
+ } catch (const json::parse_error& e) {
151
+ return "[Error] Failed to parse Ollama JSON: " + std::string(e.what());
152
+ }
153
+ }
154
+
155
+ // ----------------------------------------------------------------------
156
+ // Wrapper with retry logic for timeouts
157
+ std::string ask_ollama_with_retry(const std::string& prompt, int max_retries) {
158
+ int attempt = 0;
159
+ int base_delay = 2;
160
+ while (attempt < max_retries) {
161
+ std::string result = ask_ollama(prompt);
162
+ if (result.compare(0, 9, "[Timeout]") == 0) {
163
+ attempt++;
164
+ if (attempt < max_retries) {
165
+ int delay = base_delay * (1 << (attempt - 1));
166
+ std::cerr << "Timeout, retrying in " << delay << " seconds...\n";
167
+ std::this_thread::sleep_for(std::chrono::seconds(delay));
168
+ continue;
169
+ } else {
170
+ return "[Error] Maximum retries reached, giving up.";
171
+ }
172
+ }
173
+ return result;
174
+ }
175
+ return "[Error] Maximum retries reached, giving up.";
176
+ }
177
+
178
+ // ----------------------------------------------------------------------
179
+ // Keep‑alive
180
+ void check_and_keep_alive(time_t now) {
181
+ if (now - last_ollama_time > KEEP_ALIVE_INTERVAL) {
182
+ if (debug_enabled) std::cout << "[Keep alive] Sending ping to Ollama.\n";
183
+ ask_ollama("Hello");
184
+ last_ollama_time = now;
185
+ }
186
+ }
187
+
188
+ // ----------------------------------------------------------------------
189
+ // Warm up
190
+ void warm_up_ollama() {
191
+ std::cout << "Warming up Ollama model..." << std::endl;
192
+ std::string result = ask_ollama("Hello");
193
+ if (result.compare(0, 7, "[Error]") == 0 || result.compare(0, 9, "[Timeout]") == 0) {
194
+ std::cerr << "Warning: Warm-up failed: " << result << std::endl;
195
+ std::cerr << "Check that Ollama is running and the model is available.\n";
196
+ } else {
197
+ std::cout << "Model ready.\n";
198
+ }
199
+ }
200
+
201
+ // ----------------------------------------------------------------------
202
+ // Safe command execution (no shell) – returns stdout + status
203
+ struct CommandResult {
204
+ std::string output;
205
+ int exit_status;
206
+ };
207
+ CommandResult run_command(const std::vector<std::string>& args) {
208
+ if (args.empty()) return {"", -1};
209
+ std::vector<char*> argv;
210
+ for (const auto& a : args) argv.push_back(const_cast<char*>(a.c_str()));
211
+ argv.push_back(nullptr);
212
+
213
+ int pipefd[2];
214
+ if (pipe(pipefd) == -1) return {"pipe() failed", -1};
215
+
216
+ pid_t pid = fork();
217
+ if (pid == -1) {
218
+ close(pipefd[0]);
219
+ close(pipefd[1]);
220
+ return {"fork() failed", -1};
221
+ }
222
+
223
+ if (pid == 0) {
224
+ close(pipefd[0]);
225
+ dup2(pipefd[1], STDOUT_FILENO);
226
+ dup2(pipefd[1], STDERR_FILENO);
227
+ close(pipefd[1]);
228
+ execvp(argv[0], argv.data());
229
+ perror("execvp");
230
+ _exit(127);
231
+ }
232
+
233
+ close(pipefd[1]);
234
+ std::string output;
235
+ char buffer[4096];
236
+ ssize_t n;
237
+ while ((n = read(pipefd[0], buffer, sizeof(buffer)-1)) > 0) {
238
+ buffer[n] = '\0';
239
+ output += buffer;
240
+ }
241
+ close(pipefd[0]);
242
+
243
+ int status;
244
+ waitpid(pid, &status, 0);
245
+ int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
246
+ return {output, exit_status};
247
+ }
248
+
249
+ // ----------------------------------------------------------------------
250
+ // Git helper functions (safe, no shell)
251
+ bool is_git_repo() {
252
+ return std::filesystem::exists(pencil::get_pencil_dir() + ".git");
253
+ }
254
+
255
+ bool init_git_repo() {
256
+ if (is_git_repo()) return true;
257
+ auto res = run_command({"git", "-C", pencil::get_pencil_dir(), "init"});
258
+ if (res.exit_status != 0) return false;
259
+ // Set local identity so commits don't fail
260
+ run_command({"git", "-C", pencil::get_pencil_dir(), "config", "user.email", "pencilclaw@local"});
261
+ run_command({"git", "-C", pencil::get_pencil_dir(), "config", "user.name", "PencilClaw"});
262
+ return true;
263
+ }
264
+
265
+ // Run a git command with arguments, return output and status
266
+ CommandResult git_command(const std::vector<std::string>& args) {
267
+ std::vector<std::string> cmd = {"git", "-C", pencil::get_pencil_dir()};
268
+ cmd.insert(cmd.end(), args.begin(), args.end());
269
+ return run_command(cmd);
270
+ }
271
+
272
+ bool git_commit_file(const std::string& file_path, const std::string& commit_message) {
273
+ std::filesystem::path full_path(file_path);
274
+ std::string rel_path = std::filesystem::relative(full_path, pencil::get_pencil_dir()).string();
275
+
276
+ // git add
277
+ auto add_res = git_command({"add", rel_path});
278
+ if (add_res.exit_status != 0) {
279
+ std::cerr << "Git add failed: " << add_res.output << std::endl;
280
+ return false;
281
+ }
282
+
283
+ // git commit
284
+ auto commit_res = git_command({"commit", "-m", commit_message});
285
+ if (commit_res.exit_status != 0) {
286
+ // It's okay if "nothing to commit" – check output
287
+ if (commit_res.output.find("nothing to commit") == std::string::npos &&
288
+ commit_res.output.find("no changes added") == std::string::npos) {
289
+ std::cerr << "Git commit failed: " << commit_res.output << std::endl;
290
+ return false;
291
+ }
292
+ }
293
+ if (debug_enabled) std::cerr << "[Git] " << commit_res.output << std::endl;
294
+ return true;
295
+ }
296
+
297
+ // ----------------------------------------------------------------------
298
+ // Extract code blocks
299
+ std::vector<std::string> extract_code_blocks(const std::string &text) {
300
+ std::vector<std::string> blocks;
301
+ size_t pos = 0;
302
+ while (true) {
303
+ size_t start = text.find("```", pos);
304
+ if (start == std::string::npos) break;
305
+ size_t end = text.find("```", start + 3);
306
+ if (end == std::string::npos) break;
307
+
308
+ size_t nl = text.find('\n', start);
309
+ size_t content_start;
310
+ if (nl != std::string::npos && nl < end) {
311
+ content_start = nl + 1;
312
+ } else {
313
+ content_start = start + 3;
314
+ }
315
+
316
+ std::string block = text.substr(content_start, end - content_start);
317
+ blocks.push_back(block);
318
+ pos = end + 3;
319
+ }
320
+ return blocks;
321
+ }
322
+
323
+ // ----------------------------------------------------------------------
324
+ // Execute code (compiles and runs C++)
325
+ bool execute_code(const std::string &code) {
326
+ std::string tmp_cpp = pencil::get_pencil_dir() + "temp_code.cpp";
327
+ std::string tmp_exe = pencil::get_pencil_dir() + "temp_code";
328
+
329
+ if (!pencil::save_text(tmp_cpp, code)) {
330
+ std::cerr << "Failed to write code to temporary file." << std::endl;
331
+ return false;
332
+ }
333
+
334
+ auto compile_res = run_command({"g++", "-o", tmp_exe, tmp_cpp});
335
+ if (compile_res.exit_status != 0) {
336
+ std::cerr << "Compilation failed:\n" << compile_res.output << std::endl;
337
+ std::filesystem::remove(tmp_cpp);
338
+ return false;
339
+ }
340
+
341
+ auto run_res = run_command({tmp_exe});
342
+ std::cout << "\n[Program exited with code " << run_res.exit_status << "]\n";
343
+ std::cout << run_res.output << std::endl;
344
+
345
+ std::filesystem::remove(tmp_cpp);
346
+ std::filesystem::remove(tmp_exe);
347
+ return true;
348
+ }
349
+
350
+ // ----------------------------------------------------------------------
351
+ // Secure filename sanitization (using canonical)
352
+ std::string sanitize_and_secure_path(const std::string &input, const std::string &subdir = "") {
353
+ std::error_code ec;
354
+ std::filesystem::path base = std::filesystem::canonical(pencil::get_pencil_dir(), ec);
355
+ if (ec) {
356
+ std::cerr << "Error: Cannot resolve base directory.\n";
357
+ return "";
358
+ }
359
+ if (!subdir.empty()) base /= subdir;
360
+
361
+ // Construct a safe filename: keep only alphanumeric, dot, dash, underscore
362
+ std::string safe_name;
363
+ for (char c : input) {
364
+ if (isalnum(c) || c == '.' || c == '-' || c == '_')
365
+ safe_name += c;
366
+ else
367
+ safe_name += '_';
368
+ }
369
+ if (safe_name.empty() || safe_name == "." || safe_name == "..")
370
+ safe_name = "unnamed";
371
+
372
+ std::filesystem::path full = base / safe_name;
373
+ std::filesystem::path resolved = std::filesystem::canonical(full, ec);
374
+ if (ec) {
375
+ // Path may not exist yet; use absolute and check prefix manually
376
+ std::string abs_full = std::filesystem::absolute(full).string();
377
+ std::string base_str = base.string();
378
+ if (abs_full.compare(0, base_str.size(), base_str) != 0 ||
379
+ (abs_full.size() > base_str.size() && abs_full[base_str.size()] != '/')) {
380
+ return "";
381
+ }
382
+ return abs_full;
383
+ }
384
+
385
+ std::string resolved_str = resolved.string();
386
+ std::string base_str = base.string();
387
+ if (resolved_str.compare(0, base_str.size(), base_str) != 0 ||
388
+ (resolved_str.size() > base_str.size() && resolved_str[base_str.size()] != '/')) {
389
+ return "";
390
+ }
391
+ return resolved_str;
392
+ }
393
+
394
+ // ----------------------------------------------------------------------
395
+ // Save content with verification and Git commit
396
+ bool save_content_to_file(const std::string& content, const std::string& filename, const std::string& description) {
397
+ std::string safe_path = sanitize_and_secure_path(filename);
398
+ if (safe_path.empty()) {
399
+ std::cerr << "Error: Invalid or insecure filename." << std::endl;
400
+ return false;
401
+ }
402
+
403
+ std::error_code ec;
404
+ std::filesystem::create_directories(std::filesystem::path(safe_path).parent_path(), ec);
405
+ if (ec) {
406
+ std::cerr << "Error creating directory: " << ec.message() << std::endl;
407
+ return false;
408
+ }
409
+
410
+ if (!pencil::save_text(safe_path, content)) {
411
+ std::cerr << "Error: Failed to write file " << safe_path << std::endl;
412
+ return false;
413
+ }
414
+
415
+ if (!std::filesystem::exists(safe_path)) {
416
+ std::cerr << "Error: File " << safe_path << " does not exist after save." << std::endl;
417
+ return false;
418
+ }
419
+ auto size = std::filesystem::file_size(safe_path);
420
+ if (size == 0) {
421
+ std::cerr << "Error: File " << safe_path << " is empty." << std::endl;
422
+ return false;
423
+ }
424
+
425
+ std::cout << "βœ… Saved " << description << " to: " << safe_path << " (" << size << " bytes)" << std::endl;
426
+
427
+ // Git commit if repository is active
428
+ if (is_git_repo()) {
429
+ std::string commit_msg = description;
430
+ if (commit_msg.length() > 100) commit_msg = commit_msg.substr(0, 100) + "...";
431
+ if (!git_commit_file(safe_path, commit_msg)) {
432
+ std::cerr << "Warning: Git commit failed (check your Git configuration).\n";
433
+ }
434
+ }
435
+ return true;
436
+ }
437
+
438
+ // ----------------------------------------------------------------------
439
+ // Task management
440
+ std::string get_active_task_folder() {
441
+ std::ifstream f(pencil::get_active_task_file());
442
+ std::string folder;
443
+ std::getline(f, folder);
444
+ if (folder.empty()) return "";
445
+
446
+ std::error_code ec;
447
+ std::filesystem::path p = std::filesystem::weakly_canonical(folder, ec);
448
+ if (ec) return "";
449
+
450
+ std::string tasks_dir_canon = std::filesystem::weakly_canonical(pencil::get_tasks_dir()).string();
451
+ std::string p_str = p.string();
452
+ if (p_str.compare(0, tasks_dir_canon.size(), tasks_dir_canon) != 0 ||
453
+ (p_str.size() > tasks_dir_canon.size() && p_str[tasks_dir_canon.size()] != '/')) {
454
+ return "";
455
+ }
456
+ return p_str;
457
+ }
458
+
459
+ bool set_active_task_folder(const std::string& folder) {
460
+ std::ofstream f(pencil::get_active_task_file());
461
+ if (!f) return false;
462
+ f << folder;
463
+ return !f.fail();
464
+ }
465
+
466
+ void clear_active_task() {
467
+ std::filesystem::remove(pencil::get_active_task_file());
468
+ }
469
+
470
+ bool start_new_task(const std::string& description) {
471
+ // Create a folder with timestamp and sanitized description prefix
472
+ std::string safe_desc;
473
+ for (char c : description) {
474
+ if (isalnum(c) || c == ' ' || c == '-') safe_desc += c;
475
+ else safe_desc += '_';
476
+ }
477
+ if (safe_desc.length() > 30) safe_desc = safe_desc.substr(0, 30);
478
+ std::string folder_name = pencil::timestamp() + "_" + safe_desc;
479
+ std::string task_folder = pencil::get_tasks_dir() + folder_name + "/";
480
+
481
+ std::error_code ec;
482
+ if (!std::filesystem::create_directories(task_folder, ec) && ec) {
483
+ std::cerr << "Failed to create task folder: " << ec.message() << std::endl;
484
+ return false;
485
+ }
486
+
487
+ // Save description
488
+ if (!pencil::save_text(task_folder + "description.txt", description)) {
489
+ std::cerr << "Failed to save task description.\n";
490
+ return false;
491
+ }
492
+
493
+ // Create log file with initial entry
494
+ std::string log_entry = "Task started at " + pencil::timestamp() + "\nDescription: " + description + "\n\n";
495
+ if (!pencil::save_text(task_folder + "log.txt", log_entry)) {
496
+ std::cerr << "Failed to create log file.\n";
497
+ return false;
498
+ }
499
+
500
+ if (!set_active_task_folder(task_folder)) {
501
+ std::cerr << "Warning: Could not set active task.\n";
502
+ } else {
503
+ std::cout << "βœ… New task started: \"" << description << "\"\n";
504
+ std::cout << "Task folder: " << task_folder << "\n";
505
+ }
506
+
507
+ pencil::append_to_session("Started new task: " + description);
508
+ return true;
509
+ }
510
+
511
+ bool continue_task(const std::string& task_folder) {
512
+ // Read description and log
513
+ auto desc_opt = pencil::read_file(task_folder + "description.txt");
514
+ if (!desc_opt.has_value()) {
515
+ std::cerr << "Task description missing.\n";
516
+ return false;
517
+ }
518
+ std::string description = desc_opt.value();
519
+
520
+ auto log_opt = pencil::read_file(task_folder + "log.txt");
521
+ std::string log = log_opt.value_or("");
522
+
523
+ // Determine iteration number: count occurrences of "Iteration" in log
524
+ int iteration = 1;
525
+ size_t pos = 0;
526
+ while ((pos = log.find("Iteration", pos)) != std::string::npos) {
527
+ iteration++;
528
+ pos += 9;
529
+ }
530
+
531
+ // Build prompt for next step
532
+ std::string prompt = "You are a C++ coding agent working on the following task:\n\n" +
533
+ description + "\n\n" +
534
+ "Previous work log:\n" + log + "\n\n" +
535
+ "Generate the next iteration of code or progress. If the task is not yet complete, "
536
+ "produce a new C++ code snippet that advances the work. If the task is complete, "
537
+ "output a message indicating completion and include no code.\n\n"
538
+ "Provide your response with optional explanation, but include any code inside ```cpp ... ``` blocks.";
539
+
540
+ std::cout << "Continuing task (iteration " << iteration << ")...\n";
541
+ std::string response = ask_ollama_with_retry(prompt);
542
+ if (response.compare(0, 7, "[Error]") == 0) {
543
+ std::cerr << "Failed to generate continuation: " << response << std::endl;
544
+ return false;
545
+ }
546
+
547
+ // Save this iteration
548
+ std::string iter_file = task_folder + "iteration_" + std::to_string(iteration) + ".txt";
549
+ if (!pencil::save_text(iter_file, response)) {
550
+ std::cerr << "Failed to save iteration.\n";
551
+ return false;
552
+ }
553
+
554
+ // Append to log
555
+ std::ofstream log_file(task_folder + "log.txt", std::ios::app);
556
+ if (log_file) {
557
+ log_file << "\n--- Iteration " << iteration << " (" << pencil::timestamp() << ") ---\n";
558
+ log_file << response << "\n";
559
+ }
560
+
561
+ std::cout << "βœ… Iteration " << iteration << " saved to: " << iter_file << "\n";
562
+ last_ai_output = response;
563
+ last_ai_type = "task_iteration";
564
+ pencil::append_to_session("Task continued: iteration " + std::to_string(iteration));
565
+
566
+ // Git commit if repository is active
567
+ if (is_git_repo()) {
568
+ std::string commit_msg = "Task iteration " + std::to_string(iteration) + ": " + description;
569
+ if (commit_msg.length() > 100) commit_msg = commit_msg.substr(0, 100) + "...";
570
+ if (!git_commit_file(iter_file, commit_msg)) {
571
+ std::cerr << "Warning: Git commit failed.\n";
572
+ }
573
+ }
574
+ return true;
575
+ }
576
+
577
+ // ----------------------------------------------------------------------
578
+ // Heartbeat
579
+ void run_heartbeat(time_t now) {
580
+ check_and_keep_alive(now);
581
+ std::string active_task = get_active_task_folder();
582
+ if (!active_task.empty()) {
583
+ if (debug_enabled) std::cout << "[Heartbeat] Continuing active task.\n";
584
+ continue_task(active_task);
585
+ }
586
+ }
587
+
588
+ // ----------------------------------------------------------------------
589
+ // Natural language helpers
590
+ std::string to_lowercase(std::string s) {
591
+ std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
592
+ return s;
593
+ }
594
+
595
+ bool contains_phrase(const std::string& text, const std::string& phrase) {
596
+ std::string lower = to_lowercase(text);
597
+ std::string lower_phrase = to_lowercase(phrase);
598
+ size_t pos = lower.find(lower_phrase);
599
+ while (pos != std::string::npos) {
600
+ if ((pos == 0 || !isalnum(lower[pos-1])) &&
601
+ (pos + lower_phrase.length() == lower.length() || !isalnum(lower[pos + lower_phrase.length()]))) {
602
+ return true;
603
+ }
604
+ pos = lower.find(lower_phrase, pos + 1);
605
+ }
606
+ return false;
607
+ }
608
+
609
+ std::string extract_after(const std::string& input, const std::string& phrase) {
610
+ std::string lower_input = to_lowercase(input);
611
+ std::string lower_phrase = to_lowercase(phrase);
612
+ size_t pos = lower_input.find(lower_phrase);
613
+ if (pos == std::string::npos) return "";
614
+ if (pos > 0 && isalnum(lower_input[pos-1])) return "";
615
+ size_t after = pos + phrase.length();
616
+ if (after < lower_input.length() && isalnum(lower_input[after])) return "";
617
+ size_t start = after;
618
+ while (start < input.length() && isspace(input[start])) ++start;
619
+ std::string result = input.substr(start);
620
+ while (!result.empty() && isspace(result.back())) result.pop_back();
621
+ return result;
622
+ }
623
+
624
+ std::string extract_quoted(const std::string& input) {
625
+ size_t start = input.find('"');
626
+ if (start == std::string::npos) start = input.find('\'');
627
+ if (start == std::string::npos) return "";
628
+ size_t end = input.find(input[start], start + 1);
629
+ if (end == std::string::npos) return "";
630
+ return input.substr(start + 1, end - start - 1);
631
+ }
632
+
633
+ std::string extract_filename(const std::string& line) {
634
+ std::string quoted = extract_quoted(line);
635
+ if (!quoted.empty()) return quoted;
636
+
637
+ std::string lower = to_lowercase(line);
638
+ size_t as_pos = lower.find(" as ");
639
+ if (as_pos != std::string::npos) {
640
+ std::string after = line.substr(as_pos + 4);
641
+ size_t start = after.find_first_not_of(" \t");
642
+ if (start != std::string::npos) {
643
+ after = after.substr(start);
644
+ size_t end = after.find_first_of(" \t\n\r,;");
645
+ if (end != std::string::npos) after = after.substr(0, end);
646
+ return after;
647
+ }
648
+ }
649
+ return "";
650
+ }
651
+
652
+ // ----------------------------------------------------------------------
653
+ // Code generation handler
654
+ void handle_code(const std::string& idea) {
655
+ 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;
656
+ std::cout << "Asking Ollama...\n";
657
+ std::string response = ask_ollama_with_retry(prompt);
658
+ std::cout << "\n" << response << "\n";
659
+
660
+ last_ai_output = response;
661
+ last_ai_type = "code";
662
+
663
+ std::string base = idea;
664
+ if (base.length() > 50) base = base.substr(0, 50);
665
+ // Pass raw filename; save_content_to_file will sanitize it.
666
+ save_content_to_file(response, base + ".txt", "code for \"" + idea + "\"");
667
+ pencil::append_to_session("User asked for code: " + idea);
668
+ pencil::append_to_session("Assistant: " + response);
669
+ }
670
+
671
+ // ----------------------------------------------------------------------
672
+ // NLU dispatcher
673
+ bool handle_natural_language(const std::string& line) {
674
+ // Save requests
675
+ if (contains_phrase(line, "save it") || contains_phrase(line, "save the code") ||
676
+ contains_phrase(line, "write it to a file") || contains_phrase(line, "save as")) {
677
+
678
+ if (debug_enabled) std::cout << "[NLU] Matched save request.\n";
679
+
680
+ if (last_ai_output.empty()) {
681
+ std::cout << "I don't have any recent code to save.\n";
682
+ return true;
683
+ }
684
+
685
+ std::string default_name = "code.txt";
686
+ std::string filename = extract_filename(line);
687
+ if (filename.empty()) {
688
+ std::cout << "What filename would you like to save it as? (default: " << default_name << ")\n> ";
689
+ std::getline(std::cin, filename);
690
+ if (filename.empty()) filename = default_name;
691
+ }
692
+
693
+ if (filename.find('.') == std::string::npos) filename += ".txt";
694
+
695
+ save_content_to_file(last_ai_output, filename, "code");
696
+ return true;
697
+ }
698
+
699
+ // Code triggers
700
+ std::vector<std::pair<std::string, std::string>> code_triggers = {
701
+ {"write code for", "for"},
702
+ {"write a program that", "that"},
703
+ {"generate code for", "for"},
704
+ {"generate a program that", "that"},
705
+ {"create code for", "for"},
706
+ {"create a program that", "that"},
707
+ {"write a function that", "that"},
708
+ {"code for", "for"}
709
+ };
710
+ for (const auto& [trigger, _] : code_triggers) {
711
+ if (contains_phrase(line, trigger)) {
712
+ if (debug_enabled) std::cout << "[NLU] Matched code trigger: " << trigger << "\n";
713
+ std::string idea = extract_after(line, trigger);
714
+ if (idea.empty()) idea = extract_quoted(line);
715
+ if (idea.empty()) {
716
+ std::cout << "What should the code do?\n> ";
717
+ std::getline(std::cin, idea);
718
+ }
719
+ if (!idea.empty()) handle_code(idea);
720
+ return true;
721
+ }
722
+ }
723
+ // Generic code
724
+ std::vector<std::string> generic_code = {
725
+ "write code", "generate code", "create code", "write a program", "generate a program"
726
+ };
727
+ for (const auto& trigger : generic_code) {
728
+ if (contains_phrase(line, trigger)) {
729
+ if (debug_enabled) std::cout << "[NLU] Matched generic code trigger: " << trigger << "\n";
730
+ std::cout << "What should the code do?\n> ";
731
+ std::string idea;
732
+ std::getline(std::cin, idea);
733
+ if (!idea.empty()) handle_code(idea);
734
+ return true;
735
+ }
736
+ }
737
+
738
+ // Task triggers
739
+ std::vector<std::pair<std::string, std::string>> task_triggers = {
740
+ {"start a task to", "to"},
741
+ {"begin a task to", "to"},
742
+ {"create a task to", "to"},
743
+ {"start a task that", "that"},
744
+ {"begin a task that", "that"},
745
+ {"create a task that", "that"}
746
+ };
747
+ for (const auto& [trigger, _] : task_triggers) {
748
+ if (contains_phrase(line, trigger)) {
749
+ if (debug_enabled) std::cout << "[NLU] Matched task trigger: " << trigger << "\n";
750
+ std::string desc = extract_after(line, trigger);
751
+ if (desc.empty()) desc = extract_quoted(line);
752
+ if (desc.empty()) {
753
+ std::cout << "Describe the task:\n> ";
754
+ std::getline(std::cin, desc);
755
+ }
756
+ if (!desc.empty()) start_new_task(desc);
757
+ return true;
758
+ }
759
+ }
760
+ // Generic task
761
+ std::vector<std::string> generic_task = {
762
+ "start a task", "begin a task", "create a task", "new task"
763
+ };
764
+ for (const auto& trigger : generic_task) {
765
+ if (contains_phrase(line, trigger)) {
766
+ if (debug_enabled) std::cout << "[NLU] Matched generic task trigger: " << trigger << "\n";
767
+ std::cout << "Describe the task:\n> ";
768
+ std::string desc;
769
+ std::getline(std::cin, desc);
770
+ if (!desc.empty()) start_new_task(desc);
771
+ return true;
772
+ }
773
+ }
774
+
775
+ return false;
776
+ }
777
+
778
+ // ----------------------------------------------------------------------
779
+ // List files
780
+ void list_files() {
781
+ std::cout << "\nπŸ“ Files in " << std::filesystem::absolute(pencil::get_pencil_dir()).string() << ":\n";
782
+ try {
783
+ for (const auto& entry : std::filesystem::directory_iterator(pencil::get_pencil_dir())) {
784
+ if (entry.is_regular_file() && entry.path().extension() == ".txt") {
785
+ std::cout << " " << entry.path().filename().string()
786
+ << " (" << entry.file_size() << " bytes)\n";
787
+ }
788
+ }
789
+ if (std::filesystem::exists(pencil::get_tasks_dir())) {
790
+ std::cout << "\nπŸ“‚ Tasks:\n";
791
+ for (const auto& entry : std::filesystem::directory_iterator(pencil::get_tasks_dir())) {
792
+ if (entry.is_directory()) {
793
+ std::cout << " " << entry.path().filename().string() << "/\n";
794
+ // Optionally list iteration files
795
+ }
796
+ }
797
+ }
798
+ } catch (const std::filesystem::filesystem_error& e) {
799
+ std::cerr << "Error listing files: " << e.what() << std::endl;
800
+ }
801
+ }
802
+
803
+ // ----------------------------------------------------------------------
804
+ int main() {
805
+ if (!pencil::init_workspace()) {
806
+ std::cerr << "Fatal error: cannot create workspace directory." << std::endl;
807
+ return 1;
808
+ }
809
+
810
+ std::cout << "πŸ“ Workspace: " << std::filesystem::absolute(pencil::get_pencil_dir()).string() << "\n";
811
+
812
+ // Initialize Git repository if possible
813
+ if (!init_git_repo()) {
814
+ std::cerr << "Warning: Could not initialise Git repository. Git features disabled.\n";
815
+ } else {
816
+ std::cout << "Git repository initialised (or already exists).\n";
817
+ }
818
+
819
+ warm_up_ollama();
820
+ if (last_ollama_time == 0) last_ollama_time = time(nullptr);
821
+
822
+ std::cout << "PENCILCLAW – C++ Coding Agent with Git integration\n";
823
+ std::cout << "Heartbeat interval: " << HEARTBEAT_INTERVAL << " seconds\n";
824
+ std::cout << "Type /HELP for commands.\n";
825
+
826
+ std::string last_response;
827
+ time_t last_heartbeat_run = time(nullptr);
828
+
829
+ while (true) {
830
+ time_t now = time(nullptr);
831
+ check_and_keep_alive(now);
832
+
833
+ std::cout << "\n> ";
834
+ std::string line;
835
+ std::getline(std::cin, line);
836
+ if (line.empty()) continue;
837
+
838
+ if (line[0] != '/') {
839
+ if (handle_natural_language(line)) {
840
+ if (now - last_heartbeat_run >= HEARTBEAT_INTERVAL) {
841
+ run_heartbeat(now);
842
+ last_heartbeat_run = now;
843
+ }
844
+ continue;
845
+ }
846
+ }
847
+
848
+ if (line[0] == '/') {
849
+ std::string cmd;
850
+ std::string arg;
851
+ size_t sp = line.find(' ');
852
+ if (sp == std::string::npos) {
853
+ cmd = line;
854
+ } else {
855
+ cmd = line.substr(0, sp);
856
+ arg = line.substr(sp + 1);
857
+ }
858
+
859
+ if (cmd == "/EXIT") {
860
+ break;
861
+ }
862
+ else if (cmd == "/HELP") {
863
+ std::cout << "Available commands:\n";
864
+ std::cout << " /HELP – this help\n";
865
+ std::cout << " /CODE <idea> – generate C++ code for a task\n";
866
+ std::cout << " /TASK <description> – start a new autonomous coding task\n";
867
+ std::cout << " /TASK_STATUS – show current active task\n";
868
+ std::cout << " /STOP_TASK – clear active task\n";
869
+ std::cout << " /EXECUTE – compile & run code from last output\n";
870
+ std::cout << " /FILES – list all saved files and tasks\n";
871
+ std::cout << " /DEBUG – toggle debug output\n";
872
+ std::cout << " /EXIT – quit\n";
873
+ std::cout << "\nNatural language examples:\n";
874
+ std::cout << " 'write code for a fibonacci function'\n";
875
+ std::cout << " 'start a task to build a calculator'\n";
876
+ std::cout << " 'save it as mycode.txt' (after code generation)\n";
877
+ }
878
+ else if (cmd == "/DEBUG") {
879
+ debug_enabled = !debug_enabled;
880
+ std::cout << "Debug mode " << (debug_enabled ? "enabled" : "disabled") << ".\n";
881
+ }
882
+ else if (cmd == "/CODE") {
883
+ if (arg.empty()) {
884
+ std::cout << "Please provide a description of the code.\n";
885
+ continue;
886
+ }
887
+ handle_code(arg);
888
+ }
889
+ else if (cmd == "/TASK") {
890
+ if (arg.empty()) {
891
+ std::cout << "Please provide a task description.\n";
892
+ continue;
893
+ }
894
+ start_new_task(arg);
895
+ }
896
+ else if (cmd == "/TASK_STATUS") {
897
+ std::string folder = get_active_task_folder();
898
+ if (folder.empty()) {
899
+ std::cout << "No active task.\n";
900
+ } else {
901
+ auto desc_opt = pencil::read_file(folder + "description.txt");
902
+ std::string desc = desc_opt.value_or("unknown");
903
+ std::cout << "Active task: " << desc << "\n";
904
+ std::cout << "Folder: " << folder << "\n";
905
+ // Count iterations
906
+ int count = 0;
907
+ for (const auto& entry : std::filesystem::directory_iterator(folder)) {
908
+ if (entry.path().filename().string().rfind("iteration_", 0) == 0)
909
+ count++;
910
+ }
911
+ std::cout << "Iterations so far: " << count << "\n";
912
+ }
913
+ }
914
+ else if (cmd == "/STOP_TASK") {
915
+ clear_active_task();
916
+ std::cout << "Active task cleared.\n";
917
+ }
918
+ else if (cmd == "/FILES") {
919
+ list_files();
920
+ }
921
+ else if (cmd == "/EXECUTE") {
922
+ if (last_ai_output.empty()) {
923
+ std::cout << "No previous AI output to execute from.\n";
924
+ continue;
925
+ }
926
+ auto blocks = extract_code_blocks(last_ai_output);
927
+ if (blocks.empty()) {
928
+ std::cout << "No code blocks found in last output.\n";
929
+ continue;
930
+ }
931
+ std::cout << "--- Code to execute ---\n";
932
+ std::cout << blocks[0] << "\n";
933
+ std::cout << "------------------------\n";
934
+ std::cout << "WARNING: This code was generated by an AI and may be unsafe.\n";
935
+ std::cout << "Type 'yes' to confirm execution (any other input cancels): ";
936
+ std::string confirm;
937
+ std::getline(std::cin, confirm);
938
+ if (confirm != "yes") {
939
+ std::cout << "Execution cancelled.\n";
940
+ continue;
941
+ }
942
+ std::cout << "Executing code block...\n";
943
+ if (execute_code(blocks[0])) {
944
+ std::cout << "Execution finished.\n";
945
+ } else {
946
+ std::cout << "Execution failed.\n";
947
+ }
948
+ }
949
+ else {
950
+ std::cout << "Unknown command. Type /HELP for list.\n";
951
+ }
952
+ } else {
953
+ // Free prompt (not handled by NLU)
954
+ std::cout << "Sending to Ollama...\n";
955
+ last_response = ask_ollama_with_retry(line);
956
+ last_ai_output = last_response;
957
+ last_ai_type = "free";
958
+ std::cout << last_response << "\n";
959
+ pencil::append_to_session("User: " + line);
960
+ pencil::append_to_session("Assistant: " + last_response);
961
+ }
962
+
963
+ time_t now2 = time(nullptr);
964
+ if (now2 - last_heartbeat_run >= HEARTBEAT_INTERVAL) {
965
+ run_heartbeat(now2);
966
+ last_heartbeat_run = now2;
967
+ }
968
+ }
969
+
970
+ return 0;
971
+ }