This commit is contained in:
retoor 2026-02-14 08:07:05 +01:00
parent 33af547b5d
commit bd9b1b929e
16 changed files with 2131 additions and 68 deletions

View File

@ -40,7 +40,8 @@ SRC_TOOLS = $(TOOLSDIR)/tools_init.c \
$(TOOLSDIR)/tool_automation.c \ $(TOOLSDIR)/tool_automation.c \
$(TOOLSDIR)/tool_csv.c \ $(TOOLSDIR)/tool_csv.c \
$(TOOLSDIR)/tool_agent.c \ $(TOOLSDIR)/tool_agent.c \
$(TOOLSDIR)/tool_deepsearch.c $(TOOLSDIR)/tool_deepsearch.c \
$(TOOLSDIR)/tool_snapshot.c
SRC = $(SRC_CORE) $(SRC_TOOLS) SRC = $(SRC_CORE) $(SRC_TOOLS)

1354
README.md

File diff suppressed because it is too large Load Diff

View File

@ -23,4 +23,15 @@ r_status_t db_load_conversation(db_handle db, const char *session_key, char **da
long long db_get_conversation_age(db_handle db, const char *session_key); long long db_get_conversation_age(db_handle db, const char *session_key);
r_status_t db_delete_conversation(db_handle db, const char *session_key); r_status_t db_delete_conversation(db_handle db, const char *session_key);
r_status_t db_snapshot_create(db_handle db, const char *session_id, const char *description,
const char **paths, const char **contents, int file_count,
long long *snapshot_id_out);
r_status_t db_snapshot_list(db_handle db, const char *session_id, struct json_object **result);
r_status_t db_snapshot_get_files(db_handle db, long long snapshot_id, struct json_object **result);
r_status_t db_snapshot_ensure_live(db_handle db, const char *session_id,
const char *description, long long *snapshot_id_out);
r_status_t db_snapshot_upsert_file(db_handle db, long long snapshot_id,
const char *path, const char *content);
#endif #endif

View File

@ -35,4 +35,6 @@ char *messages_to_string(messages_handle msgs);
char *messages_to_json_string(messages_handle msgs); char *messages_to_json_string(messages_handle msgs);
int messages_count(messages_handle msgs); int messages_count(messages_handle msgs);
char *messages_generate_session_id(void);
#endif #endif

View File

@ -36,4 +36,7 @@ int r_config_get_max_total_spawns(r_config_handle cfg);
const char *r_config_get_deepsearch_system_message(void); const char *r_config_get_deepsearch_system_message(void);
void r_config_set_current_prompt(r_config_handle cfg, const char *prompt);
const char *r_config_get_current_prompt(r_config_handle cfg);
#endif #endif

View File

@ -48,4 +48,6 @@ typedef enum {
tool_registry_t *tool_registry_get_specialized(tool_registry_type_t type); tool_registry_t *tool_registry_get_specialized(tool_registry_type_t type);
void snapshot_live_record(const char *path, const char *content);
#endif #endif

View File

@ -393,6 +393,7 @@ char *agent_run(agent_handle agent, const char *user_message) {
agent_set_error(agent, "Empty user message"); agent_set_error(agent, "Empty user message");
return NULL; return NULL;
} }
r_config_set_current_prompt(r_config_get_instance(), user_message);
messages_load(agent->messages); messages_load(agent->messages);
char *json_data = agent_build_request(agent, "user", user_message); char *json_data = agent_build_request(agent, "user", user_message);
if (!json_data) { if (!json_data) {

140
src/db.c
View File

@ -117,7 +117,22 @@ r_status_t db_init(db_handle db) {
" batch_id TEXT," " batch_id TEXT,"
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
");"; ");"
"CREATE TABLE IF NOT EXISTS snapshots ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" session_id TEXT NOT NULL,"
" description TEXT,"
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
");"
"CREATE TABLE IF NOT EXISTS snapshot_files ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" snapshot_id INTEGER NOT NULL,"
" path TEXT NOT NULL,"
" content TEXT NOT NULL,"
" FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE"
");"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshot_files_unique "
"ON snapshot_files(snapshot_id, path);";
char *err_msg = NULL; char *err_msg = NULL;
if (sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg) != SQLITE_OK) { if (sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg) != SQLITE_OK) {
sqlite3_free(err_msg); sqlite3_free(err_msg);
@ -319,3 +334,126 @@ r_status_t db_load_conversation(db_handle db, const char *session_key, char **da
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND; return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND;
} }
r_status_t db_snapshot_create(db_handle db, const char *session_id, const char *description,
const char **paths, const char **contents, int file_count,
long long *snapshot_id_out) {
if (!db || !db->conn || !session_id || !paths || !contents || file_count <= 0 || !snapshot_id_out)
return R_ERROR_INVALID_ARG;
char *err_msg = NULL;
if (sqlite3_exec(db->conn, "BEGIN TRANSACTION", NULL, NULL, &err_msg) != SQLITE_OK) {
sqlite3_free(err_msg);
return R_ERROR_DB_QUERY;
}
char *sql = sqlite3_mprintf(
"INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)",
session_id, description ? description : "");
if (!sql) {
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
return R_ERROR_OUT_OF_MEMORY;
}
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
sqlite3_free(err_msg);
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
return R_ERROR_DB_QUERY;
}
long long sid = sqlite3_last_insert_rowid(db->conn);
for (int i = 0; i < file_count; i++) {
sql = sqlite3_mprintf(
"INSERT INTO snapshot_files (snapshot_id, path, content) VALUES (%lld, %Q, %Q)",
sid, paths[i], contents[i]);
if (!sql) {
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
return R_ERROR_OUT_OF_MEMORY;
}
rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
sqlite3_free(err_msg);
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
return R_ERROR_DB_QUERY;
}
}
if (sqlite3_exec(db->conn, "COMMIT", NULL, NULL, &err_msg) != SQLITE_OK) {
sqlite3_free(err_msg);
sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL);
return R_ERROR_DB_QUERY;
}
*snapshot_id_out = sid;
return R_SUCCESS;
}
r_status_t db_snapshot_list(db_handle db, const char *session_id, struct json_object **result) {
if (!db || !db->conn || !session_id || !result) return R_ERROR_INVALID_ARG;
char *sql = sqlite3_mprintf(
"SELECT s.id, s.session_id, s.description, s.created_at, "
"(SELECT COUNT(*) FROM snapshot_files sf WHERE sf.snapshot_id = s.id) AS file_count "
"FROM snapshots s WHERE s.session_id = %Q ORDER BY s.created_at DESC",
session_id);
if (!sql) return R_ERROR_OUT_OF_MEMORY;
r_status_t status = db_execute(db, sql, result);
sqlite3_free(sql);
return status;
}
r_status_t db_snapshot_get_files(db_handle db, long long snapshot_id, struct json_object **result) {
if (!db || !db->conn || !result) return R_ERROR_INVALID_ARG;
char *sql = sqlite3_mprintf(
"SELECT path, content FROM snapshot_files WHERE snapshot_id = %lld",
snapshot_id);
if (!sql) return R_ERROR_OUT_OF_MEMORY;
r_status_t status = db_execute(db, sql, result);
sqlite3_free(sql);
return status;
}
r_status_t db_snapshot_ensure_live(db_handle db, const char *session_id,
const char *description, long long *snapshot_id_out) {
if (!db || !db->conn || !session_id || !snapshot_id_out) return R_ERROR_INVALID_ARG;
const char *select_sql = "SELECT id FROM snapshots WHERE session_id = ? LIMIT 1";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(db->conn, select_sql, -1, &stmt, NULL) != SQLITE_OK) {
return R_ERROR_DB_QUERY;
}
sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_STATIC);
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
*snapshot_id_out = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
char *update_sql = sqlite3_mprintf(
"UPDATE snapshots SET description = %Q WHERE id = %lld",
description ? description : "", *snapshot_id_out);
if (!update_sql) return R_ERROR_OUT_OF_MEMORY;
char *err_msg = NULL;
rc = sqlite3_exec(db->conn, update_sql, NULL, NULL, &err_msg);
sqlite3_free(update_sql);
sqlite3_free(err_msg);
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
}
sqlite3_finalize(stmt);
char *insert_sql = sqlite3_mprintf(
"INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)",
session_id, description ? description : "");
if (!insert_sql) return R_ERROR_OUT_OF_MEMORY;
char *err_msg = NULL;
rc = sqlite3_exec(db->conn, insert_sql, NULL, NULL, &err_msg);
sqlite3_free(insert_sql);
if (rc != SQLITE_OK) {
sqlite3_free(err_msg);
return R_ERROR_DB_QUERY;
}
*snapshot_id_out = sqlite3_last_insert_rowid(db->conn);
return R_SUCCESS;
}
r_status_t db_snapshot_upsert_file(db_handle db, long long snapshot_id,
const char *path, const char *content) {
if (!db || !db->conn || !path || !content) return R_ERROR_INVALID_ARG;
char *sql = sqlite3_mprintf(
"INSERT OR REPLACE INTO snapshot_files (snapshot_id, path, content) "
"VALUES (%lld, %Q, %Q)",
snapshot_id, path, content);
if (!sql) return R_ERROR_OUT_OF_MEMORY;
char *err_msg = NULL;
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
sqlite3_free(sql);
sqlite3_free(err_msg);
return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY;
}

View File

@ -1,27 +1,17 @@
// Written by retoor@molodetz.nl // retoor <retoor@molodetz.nl>
// This source code provides command-line input functionalities with
// autocomplete and history features using the readline library. It allows users
// to complete commands and manage input history.
// External includes:
// - <readline/readline.h>
// - <readline/history.h>
// - <glob.h>
// MIT License: Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction.
#include "utils.h" #include "utils.h"
#include <glob.h> #include <glob.h>
#include <readline/history.h> #include <readline/history.h>
#include <readline/readline.h> #include <readline/readline.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#define HISTORY_FILE "~/.r_history" #define HISTORY_FILE "~/.r_history"
#define LINE_BUFFER_INITIAL 4096
bool line_initialized = false; static bool line_initialized = false;
static int line_continuation_requested = 0;
char *get_history_file() { char *get_history_file() {
static char result[4096]; static char result[4096];
@ -41,7 +31,8 @@ char *line_command_generator(const char *text, int state) {
static int list_index, len = 0; static int list_index, len = 0;
const char *commands[] = {"help", "exit", "list", "review", const char *commands[] = {"help", "exit", "list", "review",
"refactor", "obfuscate", "!verbose", "!dump", "refactor", "obfuscate", "!verbose", "!dump",
"!model", "!debug", NULL}; "!model", "!debug", "!vi", "!emacs",
"!new", "!clear", "!session", NULL};
if (!state) { if (!state) {
list_index = 0; list_index = 0;
@ -100,21 +91,163 @@ char **line_command_completion(const char *text, int start, int end) {
return rl_completion_matches(text, line_command_generator); return rl_completion_matches(text, line_command_generator);
} }
static int line_toggle_editing_mode(int count, int key) {
(void)count;
(void)key;
if (rl_editing_mode == 1) {
rl_variable_bind("editing-mode", "vi");
} else {
rl_variable_bind("editing-mode", "emacs");
}
rl_set_keymap_from_edit_mode();
return 0;
}
static int line_request_continuation(int count, int key) {
(void)count;
(void)key;
line_continuation_requested = 1;
rl_crlf();
rl_done = 1;
return 0;
}
const char *line_build_prompt(void) {
if (rl_editing_mode == 0) {
return "\001\033[38;5;208m\002>\001\033[0m\002 ";
}
return "> ";
}
static const char *line_build_numbered_prompt(int line_number) {
static char prompt[64];
if (rl_editing_mode == 0) {
snprintf(prompt, sizeof(prompt),
"\001\033[38;5;208m\002%d>\001\033[0m\002 ", line_number);
} else {
snprintf(prompt, sizeof(prompt), "%d> ", line_number);
}
return prompt;
}
void line_init() { void line_init() {
if (!line_initialized) { if (!line_initialized) {
rl_attempted_completion_function = line_command_completion; rl_attempted_completion_function = line_command_completion;
rl_variable_bind("editing-mode", "vi");
rl_variable_bind("enable-bracketed-paste", "on");
rl_add_defun("toggle-editing-mode", line_toggle_editing_mode, -1);
rl_bind_key_in_map(CTRL('T'), line_toggle_editing_mode,
vi_insertion_keymap);
rl_bind_key_in_map(CTRL('T'), line_toggle_editing_mode,
vi_movement_keymap);
rl_bind_key_in_map(CTRL('T'), line_toggle_editing_mode,
emacs_standard_keymap);
rl_add_defun("request-continuation", line_request_continuation, -1);
rl_bind_key_in_map(CTRL('J'), line_request_continuation,
vi_insertion_keymap);
rl_bind_key_in_map(CTRL('J'), line_request_continuation,
vi_movement_keymap);
rl_bind_key_in_map(CTRL('J'), line_request_continuation,
emacs_standard_keymap);
rl_bind_keyseq_in_map("\e\r", line_request_continuation,
vi_insertion_keymap);
rl_bind_keyseq_in_map("\e\r", line_request_continuation,
vi_movement_keymap);
rl_bind_keyseq_in_map("\e\r", line_request_continuation,
emacs_standard_keymap);
rl_bind_keyseq_in_map("\e\n", line_request_continuation,
vi_insertion_keymap);
rl_bind_keyseq_in_map("\e\n", line_request_continuation,
vi_movement_keymap);
rl_bind_keyseq_in_map("\e\n", line_request_continuation,
emacs_standard_keymap);
rl_bind_keyseq_in_map("\033[13;2u", line_request_continuation,
vi_insertion_keymap);
rl_bind_keyseq_in_map("\033[13;2u", line_request_continuation,
vi_movement_keymap);
rl_bind_keyseq_in_map("\033[13;2u", line_request_continuation,
emacs_standard_keymap);
rl_bind_keyseq_in_map("\033[13;5u", line_request_continuation,
vi_insertion_keymap);
rl_bind_keyseq_in_map("\033[13;5u", line_request_continuation,
vi_movement_keymap);
rl_bind_keyseq_in_map("\033[13;5u", line_request_continuation,
emacs_standard_keymap);
rl_bind_keyseq_in_map("\033[27;5;13~", line_request_continuation,
vi_insertion_keymap);
rl_bind_keyseq_in_map("\033[27;5;13~", line_request_continuation,
vi_movement_keymap);
rl_bind_keyseq_in_map("\033[27;5;13~", line_request_continuation,
emacs_standard_keymap);
line_initialized = true; line_initialized = true;
read_history(get_history_file()); read_history(get_history_file());
} }
} }
char *line_read(char *prefix) { char *line_read(const char *prompt) {
char *data = readline(prefix); size_t capacity = LINE_BUFFER_INITIAL;
if (!(data && *data)) { size_t length = 0;
free(data); char *buffer = (char *)malloc(capacity);
if (!buffer) {
return NULL; return NULL;
} }
return data; buffer[0] = '\0';
const char *active_prompt = prompt;
int line_number = 1;
while (1) {
line_continuation_requested = 0;
char *segment = readline(active_prompt);
if (!segment) {
if (length > 0) {
break;
}
free(buffer);
return NULL;
}
size_t seg_len = strlen(segment);
size_t needed = length + seg_len + 2;
if (needed > capacity) {
capacity = needed * 2;
char *grown = (char *)realloc(buffer, capacity);
if (!grown) {
free(segment);
free(buffer);
return NULL;
}
buffer = grown;
}
if (length > 0) {
buffer[length++] = '\n';
}
memcpy(buffer + length, segment, seg_len);
length += seg_len;
buffer[length] = '\0';
free(segment);
segment = NULL;
if (!line_continuation_requested) {
break;
}
line_number++;
active_prompt = line_build_numbered_prompt(line_number);
}
if (length == 0) {
free(buffer);
return NULL;
}
return buffer;
} }
void line_add_history(char *data) { void line_add_history(char *data) {

View File

@ -164,7 +164,7 @@ static void repl(void) {
line_init(); line_init();
char *line = NULL; char *line = NULL;
while (true) { while (true) {
line = line_read("> "); line = line_read(line_build_prompt());
if (!line || !*line) if (!line || !*line)
continue; continue;
if (!strncmp(line, "!dump", 5)) { if (!strncmp(line, "!dump", 5)) {
@ -192,6 +192,16 @@ static void repl(void) {
fprintf(stderr, "New session: %s\n", session_id); fprintf(stderr, "New session: %s\n", session_id);
continue; continue;
} }
if (!strncmp(line, "!vi", 3)) {
rl_variable_bind("editing-mode", "vi");
rl_set_keymap_from_edit_mode();
continue;
}
if (!strncmp(line, "!emacs", 6)) {
rl_variable_bind("editing-mode", "emacs");
rl_set_keymap_from_edit_mode();
continue;
}
if (!strncmp(line, "!verbose", 8)) { if (!strncmp(line, "!verbose", 8)) {
bool verbose = !r_config_is_verbose(cfg); bool verbose = !r_config_is_verbose(cfg);
r_config_set_verbose(cfg, verbose); r_config_set_verbose(cfg, verbose);
@ -375,6 +385,11 @@ static void init(void) {
"Copy relevant data from tool results into your response.\n" "Copy relevant data from tool results into your response.\n"
"## Backup\n" "## Backup\n"
"Make a .bak backup before editing files you did not create.\n" "Make a .bak backup before editing files you did not create.\n"
"## Snapshots\n"
"File modifications through write_file, file_line_replace, and file_apply_patch are "
"automatically recorded in a live snapshot for this session. Use list_snapshots to see "
"snapshots and restore_snapshot to restore files. You can also use create_snapshot to "
"manually capture additional files before risky changes.\n"
"## Terminal\n" "## Terminal\n"
"You have bash access. Prefer commands that do not require root.\n" "You have bash access. Prefer commands that do not require root.\n"
"## RULE #2: DELIVERABLE FIRST\n" "## RULE #2: DELIVERABLE FIRST\n"

View File

@ -25,7 +25,7 @@ static bool is_valid_session_id(const char *session_id) {
} }
return true; return true;
} }
static long long get_ppid_starttime(pid_t ppid) { long long messages_get_ppid_starttime(pid_t ppid) {
char proc_path[64]; char proc_path[64];
snprintf(proc_path, sizeof(proc_path), "/proc/%d/stat", ppid); snprintf(proc_path, sizeof(proc_path), "/proc/%d/stat", ppid);
FILE *fp = fopen(proc_path, "r"); FILE *fp = fopen(proc_path, "r");
@ -46,11 +46,11 @@ static long long get_ppid_starttime(pid_t ppid) {
if (field < 19) return -1; if (field < 19) return -1;
return strtoll(p, NULL, 10); return strtoll(p, NULL, 10);
} }
static char *generate_default_session_id(void) { char *messages_generate_session_id(void) {
char *session_id = malloc(64); char *session_id = malloc(64);
if (!session_id) return NULL; if (!session_id) return NULL;
pid_t ppid = getppid(); pid_t ppid = getppid();
long long starttime = get_ppid_starttime(ppid); long long starttime = messages_get_ppid_starttime(ppid);
if (starttime >= 0) { if (starttime >= 0) {
snprintf(session_id, 64, "session-%d-%lld", ppid, starttime); snprintf(session_id, 64, "session-%d-%lld", ppid, starttime);
} else { } else {
@ -69,7 +69,7 @@ messages_handle messages_create(const char *session_id) {
if (session_id && is_valid_session_id(session_id)) { if (session_id && is_valid_session_id(session_id)) {
msgs->session_id = strdup(session_id); msgs->session_id = strdup(session_id);
} else { } else {
msgs->session_id = generate_default_session_id(); msgs->session_id = messages_generate_session_id();
} }
if (!msgs->session_id) { if (!msgs->session_id) {
json_object_put(msgs->array); json_object_put(msgs->array);

View File

@ -12,6 +12,7 @@ struct r_config_t {
char *db_path; char *db_path;
char *session_id; char *session_id;
char *system_message; char *system_message;
char *current_prompt;
double temperature; double temperature;
int max_tokens; int max_tokens;
int max_spawn_depth; int max_spawn_depth;
@ -99,6 +100,7 @@ void r_config_destroy(void) {
free(instance->db_path); free(instance->db_path);
free(instance->session_id); free(instance->session_id);
free(instance->system_message); free(instance->system_message);
free(instance->current_prompt);
free(instance); free(instance);
instance = NULL; instance = NULL;
} }
@ -158,6 +160,14 @@ int r_config_get_max_spawn_depth(r_config_handle cfg) {
int r_config_get_max_total_spawns(r_config_handle cfg) { int r_config_get_max_total_spawns(r_config_handle cfg) {
return cfg ? cfg->max_total_spawns : 20; return cfg ? cfg->max_total_spawns : 20;
} }
void r_config_set_current_prompt(r_config_handle cfg, const char *prompt) {
if (!cfg) return;
free(cfg->current_prompt);
cfg->current_prompt = prompt ? strdup(prompt) : NULL;
}
const char *r_config_get_current_prompt(r_config_handle cfg) {
return cfg ? cfg->current_prompt : NULL;
}
/* /*
* Deepsearch Algorithm System Instructions * Deepsearch Algorithm System Instructions

View File

@ -293,6 +293,7 @@ static char *write_file_execute(tool_t *self, struct json_object *args) {
fwrite(new_content, 1, strlen(new_content), fp); fwrite(new_content, 1, strlen(new_content), fp);
fclose(fp); fclose(fp);
snapshot_live_record(path, new_content);
return strdup("File successfully written."); return strdup("File successfully written.");
} }

View File

@ -127,6 +127,7 @@ static char *file_line_replace_execute(tool_t *self, struct json_object *args) {
if (fp) { if (fp) {
fputs(new_content, fp); fputs(new_content, fp);
fclose(fp); fclose(fp);
snapshot_live_record(path, new_content);
} }
for (int i = 0; i < count; i++) free(lines[i]); for (int i = 0; i < count; i++) free(lines[i]);
@ -183,6 +184,9 @@ static char *file_apply_patch_execute(tool_t *self, struct json_object *args) {
if (old_content && new_content) { if (old_content && new_content) {
r_diff_print(path, old_content, new_content); r_diff_print(path, old_content, new_content);
} }
if (new_content) {
snapshot_live_record(path, new_content);
}
free(old_content); free(old_content);
free(new_content); free(new_content);

459
src/tools/tool_snapshot.c Normal file
View File

@ -0,0 +1,459 @@
// retoor <retoor@molodetz.nl>
#include "tool.h"
#include "db.h"
#include "messages.h"
#include "r_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <libgen.h>
#include <errno.h>
#include <unistd.h>
#define SNAPSHOT_MAX_FILE_SIZE 1048576
static char *snapshot_resolve_session_id(void) {
r_config_handle cfg = r_config_get_instance();
const char *session_id = r_config_get_session_id(cfg);
if (session_id && *session_id) return strdup(session_id);
return messages_generate_session_id();
}
static int snapshot_mkdirs(const char *filepath) {
char *path_copy = strdup(filepath);
if (!path_copy) return -1;
char *dir = dirname(path_copy);
if (!dir || strcmp(dir, ".") == 0 || strcmp(dir, "/") == 0) {
free(path_copy);
return 0;
}
char mkdir_cmd[4096];
snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p '%s'", dir);
int rc = system(mkdir_cmd);
free(path_copy);
return rc;
}
static struct json_object *create_snapshot_get_description(void);
static char *create_snapshot_execute(tool_t *self, struct json_object *args);
static void create_snapshot_print_action(const char *name, struct json_object *args);
static struct json_object *list_snapshots_get_description(void);
static char *list_snapshots_execute(tool_t *self, struct json_object *args);
static void list_snapshots_print_action(const char *name, struct json_object *args);
static struct json_object *restore_snapshot_get_description(void);
static char *restore_snapshot_execute(tool_t *self, struct json_object *args);
static void restore_snapshot_print_action(const char *name, struct json_object *args);
static const tool_vtable_t create_snapshot_vtable = {
.get_description = create_snapshot_get_description,
.execute = create_snapshot_execute,
.print_action = create_snapshot_print_action
};
static const tool_vtable_t list_snapshots_vtable = {
.get_description = list_snapshots_get_description,
.execute = list_snapshots_execute,
.print_action = list_snapshots_print_action
};
static const tool_vtable_t restore_snapshot_vtable = {
.get_description = restore_snapshot_get_description,
.execute = restore_snapshot_execute,
.print_action = restore_snapshot_print_action
};
static tool_t create_snapshot_tool = { .vtable = &create_snapshot_vtable, .name = "create_snapshot" };
static tool_t list_snapshots_tool = { .vtable = &list_snapshots_vtable, .name = "list_snapshots" };
static tool_t restore_snapshot_tool = { .vtable = &restore_snapshot_vtable, .name = "restore_snapshot" };
tool_t *tool_create_snapshot_create(void) { return &create_snapshot_tool; }
tool_t *tool_list_snapshots_create(void) { return &list_snapshots_tool; }
tool_t *tool_restore_snapshot_create(void) { return &restore_snapshot_tool; }
void snapshot_live_record(const char *path, const char *content) {
if (!path || !content) return;
char *session_id = snapshot_resolve_session_id();
if (!session_id) return;
r_config_handle cfg = r_config_get_instance();
const char *prompt = r_config_get_current_prompt(cfg);
if (!prompt || !*prompt) prompt = "auto";
db_handle db = db_open(NULL);
if (!db) {
free(session_id);
return;
}
long long snapshot_id = 0;
if (db_snapshot_ensure_live(db, session_id, prompt, &snapshot_id) == R_SUCCESS) {
db_snapshot_upsert_file(db, snapshot_id, path, content);
}
db_close(db);
free(session_id);
}
static void create_snapshot_print_action(const char *name, struct json_object *args) {
(void)name;
if (!args) return;
struct json_object *desc;
if (json_object_object_get_ex(args, "description", &desc)) {
fprintf(stderr, " -> Creating snapshot: %s\n", json_object_get_string(desc));
}
}
static char *create_snapshot_execute(tool_t *self, struct json_object *args) {
(void)self;
struct json_object *paths_obj = NULL;
struct json_object *desc_obj = NULL;
if (!json_object_object_get_ex(args, "paths", &paths_obj) ||
!json_object_is_type(paths_obj, json_type_array)) {
return strdup("Error: missing or invalid 'paths' array argument");
}
if (!json_object_object_get_ex(args, "description", &desc_obj)) {
return strdup("Error: missing 'description' argument");
}
int path_count = json_object_array_length(paths_obj);
if (path_count <= 0) {
return strdup("Error: 'paths' array is empty");
}
const char **paths = calloc((size_t)path_count, sizeof(char *));
const char **contents = calloc((size_t)path_count, sizeof(char *));
char **allocated_contents = calloc((size_t)path_count, sizeof(char *));
struct json_object *skipped = json_object_new_array();
if (!paths || !contents || !allocated_contents || !skipped) {
free(paths);
free(contents);
free(allocated_contents);
json_object_put(skipped);
return strdup("Error: out of memory");
}
int captured = 0;
for (int i = 0; i < path_count; i++) {
const char *path = json_object_get_string(json_object_array_get_idx(paths_obj, i));
if (!path) continue;
FILE *fp = fopen(path, "r");
if (!fp) {
json_object_array_add(skipped, json_object_new_string(path));
continue;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp);
if (size < 0 || size > SNAPSHOT_MAX_FILE_SIZE) {
fclose(fp);
json_object_array_add(skipped, json_object_new_string(path));
continue;
}
char *buf = malloc((size_t)size + 1);
if (!buf) {
fclose(fp);
json_object_array_add(skipped, json_object_new_string(path));
continue;
}
size_t read_bytes = fread(buf, 1, (size_t)size, fp);
buf[read_bytes] = '\0';
fclose(fp);
paths[captured] = path;
contents[captured] = buf;
allocated_contents[captured] = buf;
captured++;
}
char *response = NULL;
if (captured == 0) {
response = strdup("Error: no files could be read");
} else {
char *session_id = snapshot_resolve_session_id();
if (!session_id) {
response = strdup("Error: could not determine session ID");
} else {
db_handle db = db_open(NULL);
if (!db) {
response = strdup("Error: failed to open database");
} else {
long long snapshot_id = 0;
r_status_t status = db_snapshot_create(db, session_id,
json_object_get_string(desc_obj),
paths, contents, captured, &snapshot_id);
db_close(db);
if (status != R_SUCCESS) {
response = strdup("Error: failed to create snapshot in database");
} else {
struct json_object *result = json_object_new_object();
json_object_object_add(result, "snapshot_id", json_object_new_int64(snapshot_id));
json_object_object_add(result, "files_captured", json_object_new_int(captured));
json_object_object_add(result, "description",
json_object_new_string(json_object_get_string(desc_obj)));
json_object_object_add(result, "skipped", json_object_get(skipped));
response = strdup(json_object_to_json_string(result));
json_object_put(result);
}
}
free(session_id);
}
}
for (int i = 0; i < captured; i++) {
free(allocated_contents[i]);
}
free(paths);
free(contents);
free(allocated_contents);
json_object_put(skipped);
return response;
}
static struct json_object *create_snapshot_get_description(void) {
struct json_object *root = json_object_new_object();
json_object_object_add(root, "type", json_object_new_string("function"));
struct json_object *function = json_object_new_object();
json_object_object_add(function, "name", json_object_new_string("create_snapshot"));
json_object_object_add(function, "description",
json_object_new_string("Creates a snapshot of specified files, storing their contents "
"in the database for later restoration. Use before making significant changes."));
struct json_object *parameters = json_object_new_object();
json_object_object_add(parameters, "type", json_object_new_string("object"));
struct json_object *properties = json_object_new_object();
struct json_object *paths_prop = json_object_new_object();
json_object_object_add(paths_prop, "type", json_object_new_string("array"));
struct json_object *items = json_object_new_object();
json_object_object_add(items, "type", json_object_new_string("string"));
json_object_object_add(paths_prop, "items", items);
json_object_object_add(paths_prop, "description",
json_object_new_string("Array of absolute file paths to snapshot."));
json_object_object_add(properties, "paths", paths_prop);
struct json_object *desc_prop = json_object_new_object();
json_object_object_add(desc_prop, "type", json_object_new_string("string"));
json_object_object_add(desc_prop, "description",
json_object_new_string("Description of what this snapshot captures and why."));
json_object_object_add(properties, "description", desc_prop);
json_object_object_add(parameters, "properties", properties);
struct json_object *required = json_object_new_array();
json_object_array_add(required, json_object_new_string("paths"));
json_object_array_add(required, json_object_new_string("description"));
json_object_object_add(parameters, "required", required);
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
json_object_object_add(function, "parameters", parameters);
r_config_handle cfg = r_config_get_instance();
if (r_config_use_strict(cfg)) {
json_object_object_add(function, "strict", json_object_new_boolean(1));
}
json_object_object_add(root, "function", function);
return root;
}
static void list_snapshots_print_action(const char *name, struct json_object *args) {
(void)name;
(void)args;
fprintf(stderr, " -> Listing snapshots\n");
}
static char *list_snapshots_execute(tool_t *self, struct json_object *args) {
(void)self;
(void)args;
char *session_id = snapshot_resolve_session_id();
if (!session_id) return strdup("Error: could not determine session ID");
db_handle db = db_open(NULL);
if (!db) {
free(session_id);
return strdup("Error: failed to open database");
}
struct json_object *result = NULL;
r_status_t status = db_snapshot_list(db, session_id, &result);
db_close(db);
free(session_id);
if (status != R_SUCCESS || !result) {
return strdup("Error: failed to list snapshots");
}
char *response = strdup(json_object_to_json_string(result));
json_object_put(result);
return response;
}
static struct json_object *list_snapshots_get_description(void) {
struct json_object *root = json_object_new_object();
json_object_object_add(root, "type", json_object_new_string("function"));
struct json_object *function = json_object_new_object();
json_object_object_add(function, "name", json_object_new_string("list_snapshots"));
json_object_object_add(function, "description",
json_object_new_string("Lists all file snapshots for the current session, "
"showing ID, description, creation time, and file count."));
struct json_object *parameters = json_object_new_object();
json_object_object_add(parameters, "type", json_object_new_string("object"));
json_object_object_add(parameters, "properties", json_object_new_object());
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
json_object_object_add(function, "parameters", parameters);
r_config_handle cfg = r_config_get_instance();
if (r_config_use_strict(cfg)) {
json_object_object_add(function, "strict", json_object_new_boolean(1));
}
json_object_object_add(root, "function", function);
return root;
}
static void restore_snapshot_print_action(const char *name, struct json_object *args) {
(void)name;
if (!args) return;
struct json_object *id_obj;
if (json_object_object_get_ex(args, "snapshot_id", &id_obj)) {
fprintf(stderr, " -> Restoring snapshot #%lld\n",
(long long)json_object_get_int64(id_obj));
}
}
static char *restore_snapshot_execute(tool_t *self, struct json_object *args) {
(void)self;
struct json_object *id_obj = NULL;
if (!json_object_object_get_ex(args, "snapshot_id", &id_obj)) {
return strdup("Error: missing 'snapshot_id' argument");
}
long long snapshot_id = json_object_get_int64(id_obj);
db_handle db = db_open(NULL);
if (!db) return strdup("Error: failed to open database");
struct json_object *files = NULL;
r_status_t status = db_snapshot_get_files(db, snapshot_id, &files);
db_close(db);
if (status != R_SUCCESS || !files) {
return strdup("Error: failed to retrieve snapshot files");
}
int file_count = json_object_array_length(files);
if (file_count == 0) {
json_object_put(files);
return strdup("Error: snapshot contains no files");
}
int restored = 0;
struct json_object *failed = json_object_new_array();
for (int i = 0; i < file_count; i++) {
struct json_object *entry = json_object_array_get_idx(files, i);
struct json_object *path_obj = NULL;
struct json_object *content_obj = NULL;
if (!json_object_object_get_ex(entry, "path", &path_obj) ||
!json_object_object_get_ex(entry, "content", &content_obj)) {
continue;
}
const char *path = json_object_get_string(path_obj);
const char *content = json_object_get_string(content_obj);
if (!path || !content) continue;
snapshot_mkdirs(path);
char tmp_path[4096];
snprintf(tmp_path, sizeof(tmp_path), "%s.snapshot_tmp", path);
FILE *fp = fopen(tmp_path, "w");
if (!fp) {
json_object_array_add(failed, json_object_new_string(path));
continue;
}
size_t content_len = strlen(content);
size_t written = fwrite(content, 1, content_len, fp);
fclose(fp);
if (written != content_len) {
unlink(tmp_path);
json_object_array_add(failed, json_object_new_string(path));
continue;
}
if (rename(tmp_path, path) != 0) {
unlink(tmp_path);
json_object_array_add(failed, json_object_new_string(path));
continue;
}
restored++;
}
struct json_object *result = json_object_new_object();
json_object_object_add(result, "snapshot_id", json_object_new_int64(snapshot_id));
json_object_object_add(result, "restored", json_object_new_int(restored));
json_object_object_add(result, "failed", failed);
char *response = strdup(json_object_to_json_string(result));
json_object_put(result);
json_object_put(files);
return response;
}
static struct json_object *restore_snapshot_get_description(void) {
struct json_object *root = json_object_new_object();
json_object_object_add(root, "type", json_object_new_string("function"));
struct json_object *function = json_object_new_object();
json_object_object_add(function, "name", json_object_new_string("restore_snapshot"));
json_object_object_add(function, "description",
json_object_new_string("Restores files from a previously created snapshot. "
"Writes each file back to its original path atomically."));
struct json_object *parameters = json_object_new_object();
json_object_object_add(parameters, "type", json_object_new_string("object"));
struct json_object *properties = json_object_new_object();
struct json_object *id_prop = json_object_new_object();
json_object_object_add(id_prop, "type", json_object_new_string("integer"));
json_object_object_add(id_prop, "description",
json_object_new_string("The snapshot ID to restore. Use list_snapshots to find available IDs."));
json_object_object_add(properties, "snapshot_id", id_prop);
json_object_object_add(parameters, "properties", properties);
struct json_object *required = json_object_new_array();
json_object_array_add(required, json_object_new_string("snapshot_id"));
json_object_object_add(parameters, "required", required);
json_object_object_add(parameters, "additionalProperties", json_object_new_boolean(0));
json_object_object_add(function, "parameters", parameters);
r_config_handle cfg = r_config_get_instance();
if (r_config_use_strict(cfg)) {
json_object_object_add(function, "strict", json_object_new_boolean(1));
}
json_object_object_add(root, "function", function);
return root;
}

View File

@ -41,6 +41,9 @@ extern tool_t *tool_automation_exploit_gen_create(void);
extern tool_t *tool_csv_export_create(void); extern tool_t *tool_csv_export_create(void);
extern tool_t *tool_spawn_agent_create(void); extern tool_t *tool_spawn_agent_create(void);
extern tool_t *tool_deepsearch_create(void); extern tool_t *tool_deepsearch_create(void);
extern tool_t *tool_create_snapshot_create(void);
extern tool_t *tool_list_snapshots_create(void);
extern tool_t *tool_restore_snapshot_create(void);
static tool_registry_t *global_registry = NULL; static tool_registry_t *global_registry = NULL;
@ -90,6 +93,9 @@ tool_registry_t *tools_get_registry(void) {
tool_registry_register(global_registry, tool_csv_export_create()); tool_registry_register(global_registry, tool_csv_export_create());
tool_registry_register(global_registry, tool_spawn_agent_create()); tool_registry_register(global_registry, tool_spawn_agent_create());
tool_registry_register(global_registry, tool_deepsearch_create()); tool_registry_register(global_registry, tool_deepsearch_create());
tool_registry_register(global_registry, tool_create_snapshot_create());
tool_registry_register(global_registry, tool_list_snapshots_create());
tool_registry_register(global_registry, tool_restore_snapshot_create());
return global_registry; return global_registry;
} }
@ -127,6 +133,9 @@ tool_registry_t *tool_registry_get_specialized(tool_registry_type_t type) {
tool_registry_register(reg, tool_process_get_status_create()); tool_registry_register(reg, tool_process_get_status_create());
tool_registry_register(reg, tool_process_terminate_create()); tool_registry_register(reg, tool_process_terminate_create());
tool_registry_register(reg, tool_spawn_agent_create()); tool_registry_register(reg, tool_spawn_agent_create());
tool_registry_register(reg, tool_create_snapshot_create());
tool_registry_register(reg, tool_list_snapshots_create());
tool_registry_register(reg, tool_restore_snapshot_create());
} else if (type == TOOL_TYPE_SECURITY) { } else if (type == TOOL_TYPE_SECURITY) {
tool_registry_register(reg, tool_terminal_create()); tool_registry_register(reg, tool_terminal_create());
tool_registry_register(reg, tool_network_check_create()); tool_registry_register(reg, tool_network_check_create());