2026-01-28 19:34:39 +01:00
|
|
|
// retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
#include "db.h"
|
|
|
|
|
#include "r_config.h"
|
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
|
|
struct db_t {
|
|
|
|
|
sqlite3 *conn;
|
|
|
|
|
char *path;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static char *expand_home_directory(const char *path) {
|
|
|
|
|
if (!path) return NULL;
|
|
|
|
|
if (path[0] != '~') return strdup(path);
|
|
|
|
|
|
|
|
|
|
const char *home_dir = getenv("HOME");
|
|
|
|
|
if (!home_dir) home_dir = getenv("USERPROFILE");
|
|
|
|
|
if (!home_dir) return strdup(path);
|
|
|
|
|
|
|
|
|
|
size_t home_len = strlen(home_dir);
|
|
|
|
|
size_t path_len = strlen(path);
|
|
|
|
|
char *expanded = malloc(home_len + path_len);
|
|
|
|
|
if (!expanded) return NULL;
|
|
|
|
|
|
|
|
|
|
strcpy(expanded, home_dir);
|
|
|
|
|
strcat(expanded, path + 1);
|
|
|
|
|
return expanded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db_handle db_open(const char *path) {
|
|
|
|
|
struct db_t *db = calloc(1, sizeof(struct db_t));
|
|
|
|
|
if (!db) return NULL;
|
|
|
|
|
|
|
|
|
|
if (!path) {
|
|
|
|
|
r_config_handle cfg = r_config_get_instance();
|
|
|
|
|
path = r_config_get_db_path(cfg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db->path = expand_home_directory(path);
|
|
|
|
|
if (!db->path) {
|
|
|
|
|
free(db);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sqlite3_open(db->path, &db->conn) != SQLITE_OK) {
|
|
|
|
|
free(db->path);
|
|
|
|
|
free(db);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db_init(db);
|
|
|
|
|
return db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void db_close(db_handle db) {
|
|
|
|
|
if (!db) return;
|
|
|
|
|
if (db->conn) sqlite3_close(db->conn);
|
|
|
|
|
free(db->path);
|
|
|
|
|
free(db);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_init(db_handle db) {
|
|
|
|
|
if (!db || !db->conn) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
const char *sql =
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS kv ("
|
|
|
|
|
" key TEXT PRIMARY KEY,"
|
|
|
|
|
" value TEXT NOT NULL,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS file_versions ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" path TEXT NOT NULL,"
|
|
|
|
|
" content TEXT NOT NULL,"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
|
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS conversations ("
|
|
|
|
|
" session_key TEXT PRIMARY KEY,"
|
|
|
|
|
" data TEXT NOT NULL,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
chore: remove entire agent, auth, browse, chat, db_utils, and http_curl source trees
This commit deletes the complete implementation of the agent loop, authentication module, web browsing utilities, chat prompt builder, database helper, and HTTP client. All corresponding header files and test harnesses are also removed. The diff shows the deletion of 6 source files and 6 header files totaling over 1000 lines of code, including the agent state machine, API key resolution logic, curl-based HTTP functions, SQLite database initialization and CRUD operations, and the JSON chat prompt construction.
2026-01-29 02:21:11 +01:00
|
|
|
");"
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS blackboard ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" sender TEXT NOT NULL,"
|
|
|
|
|
" recipient TEXT,"
|
|
|
|
|
" topic TEXT NOT NULL,"
|
|
|
|
|
" message TEXT NOT NULL,"
|
|
|
|
|
" status TEXT DEFAULT 'pending',"
|
|
|
|
|
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
|
|
|
" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
2026-01-28 19:34:39 +01:00
|
|
|
");";
|
|
|
|
|
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
if (sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg) != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_kv_set(db_handle db, const char *key, const char *value) {
|
|
|
|
|
if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT OR REPLACE INTO kv (key, value, updated_at) VALUES (%Q, %Q, CURRENT_TIMESTAMP)",
|
|
|
|
|
key, value);
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_kv_get(db_handle db, const char *key, char **value) {
|
|
|
|
|
if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
*value = NULL;
|
|
|
|
|
const char *sql = "SELECT value FROM kv WHERE key = ?";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
|
|
|
|
|
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
|
|
|
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
|
|
|
|
*value = val ? strdup(val) : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_execute(db_handle db, const char *sql, struct json_object **result) {
|
|
|
|
|
if (!db || !db->conn || !sql || !result) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
*result = NULL;
|
|
|
|
|
|
|
|
|
|
const char *select_check = sql;
|
|
|
|
|
while (*select_check == ' ') select_check++;
|
|
|
|
|
|
|
|
|
|
if (strncasecmp(select_check, "SELECT", 6) != 0) {
|
|
|
|
|
char *err_msg = NULL;
|
|
|
|
|
int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg);
|
|
|
|
|
if (rc != SQLITE_OK) {
|
|
|
|
|
struct json_object *error_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(error_obj, "error",
|
|
|
|
|
json_object_new_string(err_msg ? err_msg : "Query failed"));
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
*result = error_obj;
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
struct json_object *success_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(success_obj, "success", json_object_new_boolean(1));
|
|
|
|
|
*result = success_obj;
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
struct json_object *error_obj = json_object_new_object();
|
|
|
|
|
json_object_object_add(error_obj, "error",
|
|
|
|
|
json_object_new_string(sqlite3_errmsg(db->conn)));
|
|
|
|
|
*result = error_obj;
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct json_object *array = json_object_new_array();
|
|
|
|
|
int col_count = sqlite3_column_count(stmt);
|
|
|
|
|
|
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
|
struct json_object *row = json_object_new_object();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < col_count; i++) {
|
|
|
|
|
const char *col_name = sqlite3_column_name(stmt, i);
|
|
|
|
|
int col_type = sqlite3_column_type(stmt, i);
|
|
|
|
|
|
|
|
|
|
switch (col_type) {
|
|
|
|
|
case SQLITE_INTEGER:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_int64(sqlite3_column_int64(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_FLOAT:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_double(sqlite3_column_double(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_TEXT:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_string((const char *)sqlite3_column_text(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
case SQLITE_NULL:
|
|
|
|
|
json_object_object_add(row, col_name, NULL);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
json_object_object_add(row, col_name,
|
|
|
|
|
json_object_new_string((const char *)sqlite3_column_text(stmt, i)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
json_object_array_add(array, row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
*result = array;
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *db_get_schema(db_handle db) {
|
|
|
|
|
if (!db || !db->conn) return strdup("Database not available");
|
|
|
|
|
|
|
|
|
|
struct json_object *result = NULL;
|
|
|
|
|
r_status_t status = db_execute(db,
|
|
|
|
|
"SELECT name, sql FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
|
|
|
&result);
|
|
|
|
|
|
|
|
|
|
if (status != R_SUCCESS || !result) {
|
|
|
|
|
return strdup("Failed to get schema");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *schema = strdup(json_object_to_json_string_ext(result, JSON_C_TO_STRING_PRETTY));
|
|
|
|
|
json_object_put(result);
|
|
|
|
|
return schema;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_store_file_version(db_handle db, const char *path) {
|
|
|
|
|
if (!db || !db->conn || !path) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
FILE *fp = fopen(path, "r");
|
|
|
|
|
if (!fp) return R_ERROR_FILE_NOT_FOUND;
|
|
|
|
|
|
|
|
|
|
fseek(fp, 0, SEEK_END);
|
|
|
|
|
long size = ftell(fp);
|
|
|
|
|
rewind(fp);
|
|
|
|
|
|
|
|
|
|
if (size <= 0 || size > 1000000) {
|
|
|
|
|
fclose(fp);
|
|
|
|
|
return R_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *content = malloc(size + 1);
|
|
|
|
|
if (!content) {
|
|
|
|
|
fclose(fp);
|
|
|
|
|
return R_ERROR_OUT_OF_MEMORY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t read_size = fread(content, 1, size, fp);
|
|
|
|
|
content[read_size] = '\0';
|
|
|
|
|
fclose(fp);
|
|
|
|
|
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT INTO file_versions (path, content) VALUES (%Q, %Q)",
|
|
|
|
|
path, content);
|
|
|
|
|
free(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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_save_conversation(db_handle db, const char *session_key, const char *data) {
|
|
|
|
|
if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
char *sql = sqlite3_mprintf(
|
|
|
|
|
"INSERT OR REPLACE INTO conversations (session_key, data, updated_at) "
|
|
|
|
|
"VALUES (%Q, %Q, CURRENT_TIMESTAMP)",
|
|
|
|
|
session_key, data);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r_status_t db_load_conversation(db_handle db, const char *session_key, char **data) {
|
|
|
|
|
if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
*data = NULL;
|
|
|
|
|
const char *sql = "SELECT data FROM conversations WHERE session_key = ?";
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
|
|
|
return R_ERROR_DB_QUERY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_bind_text(stmt, 1, session_key, -1, SQLITE_STATIC);
|
|
|
|
|
|
|
|
|
|
int rc = sqlite3_step(stmt);
|
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
|
|
|
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
|
|
|
|
*data = val ? strdup(val) : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND;
|
|
|
|
|
}
|