From d83c93eb3281516cbbe8dca16d8fb4688d6869d8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 17:26:20 -0400 Subject: [PATCH] v0.0.3 - Add startup-event reconciliation and support structured content_fields encoding in startup config --- .roo/commands/push.md | 2 +- src/config.c | 68 +++++++++++++++ src/config.h | 8 ++ src/main.c | 35 +++----- src/main.h | 4 +- src/nostr_handler.c | 119 ++++++++++++++++++++++++++ src/nostr_handler.h | 2 + src/tools.c | 188 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 398 insertions(+), 28 deletions(-) diff --git a/.roo/commands/push.md b/.roo/commands/push.md index 869bfde..ade090b 100644 --- a/.roo/commands/push.md +++ b/.roo/commands/push.md @@ -2,6 +2,6 @@ description: "Increments and pushes the repo" --- -Then run increment_and_push.sh followed in the command line with a good description of the changes that were made. +Run increment_and_push.sh followed in the command line with a good description of the changes that were made. For example: ./increment_and_push.sh "Fixed that nasty bug" diff --git a/src/config.c b/src/config.c index e1b66da..4e0f36a 100644 --- a/src/config.c +++ b/src/config.c @@ -161,6 +161,62 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) { return 0; } +static int parse_startup_events(cJSON* root, didactyl_config_t* config) { + cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events"); + if (!arr || !cJSON_IsArray(arr)) { + return 0; + } + + int count = cJSON_GetArraySize(arr); + if (count <= 0) return 0; + + config->startup_events = (startup_event_t*)calloc((size_t)count, sizeof(startup_event_t)); + if (!config->startup_events) return -1; + config->startup_event_count = count; + + for (int i = 0; i < count; i++) { + cJSON* item = cJSON_GetArrayItem(arr, i); + if (!item || !cJSON_IsObject(item)) return -1; + + cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind"); + cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content"); + cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields"); + cJSON* tags = cJSON_GetObjectItemCaseSensitive(item, "tags"); + + if (!kind || !cJSON_IsNumber(kind)) { + return -1; + } + + config->startup_events[i].kind = (int)kind->valuedouble; + + if (content_fields) { + if (!cJSON_IsObject(content_fields)) { + return -1; + } + config->startup_events[i].content = cJSON_PrintUnformatted(content_fields); + if (!config->startup_events[i].content) { + return -1; + } + } else { + if (!content || !cJSON_IsString(content) || !content->valuestring) { + return -1; + } + config->startup_events[i].content = strdup(content->valuestring); + if (!config->startup_events[i].content) { + return -1; + } + } + + if (tags) { + if (!cJSON_IsArray(tags)) return -1; + config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags); + if (!config->startup_events[i].tags_json) return -1; + } + } + + return 0; +} + static int parse_relays(cJSON* root, didactyl_config_t* config) { cJSON* relays = cJSON_GetObjectItemCaseSensitive(root, "relays"); if (!relays || !cJSON_IsArray(relays)) { @@ -206,6 +262,14 @@ void config_free(didactyl_config_t* config) { free(config->relays); } + if (config->startup_events) { + for (int i = 0; i < config->startup_event_count; i++) { + free(config->startup_events[i].content); + free(config->startup_events[i].tags_json); + } + free(config->startup_events); + } + memset(config, 0, sizeof(*config)); } @@ -308,6 +372,10 @@ int config_load(const char* path, didactyl_config_t* config) { goto cleanup; } + if (parse_startup_events(root, config) != 0) { + goto cleanup; + } + if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) { goto cleanup; } diff --git a/src/config.h b/src/config.h index e034ce7..f0a5449 100644 --- a/src/config.h +++ b/src/config.h @@ -50,6 +50,12 @@ typedef struct { shell_tools_config_t shell; } tools_config_t; +typedef struct { + int kind; + char* content; + char* tags_json; // JSON array string for tags, optional +} startup_event_t; + typedef struct { agent_profile_t profile; agent_keys_t keys; @@ -58,6 +64,8 @@ typedef struct { int relay_count; llm_config_t llm; tools_config_t tools; + startup_event_t* startup_events; + int startup_event_count; } didactyl_config_t; int config_load(const char* path, didactyl_config_t* config); diff --git a/src/main.c b/src/main.c index 22c3dd4..0ff75da 100644 --- a/src/main.c +++ b/src/main.c @@ -9,7 +9,6 @@ #include "main.h" #include "agent.h" #include "config.h" -#include "context.h" #include "llm.h" #include "nostr_handler.h" #include "debug.h" @@ -23,19 +22,15 @@ static void signal_handler(int signum) { int main(int argc, char** argv) { const char* config_path = "./config.json"; - const char* context_path = "./SYSTEM.md"; - int debug_level = DEBUG_LEVEL_TRACE; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { config_path = argv[++i]; - } else if (strcmp(argv[i], "--context") == 0 && i + 1 < argc) { - context_path = argv[++i]; } else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) { debug_level = atoi(argv[++i]); } else { - fprintf(stderr, "Usage: %s [--config ] [--context ] [--debug <0-5>]\n", argv[0]); + fprintf(stderr, "Usage: %s [--config ] [--debug <0-5>]\n", argv[0]); return 1; } } @@ -56,17 +51,8 @@ int main(int argc, char** argv) { return 1; } - char* system_context = context_load(context_path); - if (!system_context) { - fprintf(stderr, "Failed to load context file: %s\n", context_path); - config_free(&cfg); - nostr_cleanup(); - return 1; - } - if (llm_init(&cfg.llm) != 0) { fprintf(stderr, "Failed to initialize llm client\n"); - context_free(system_context); config_free(&cfg); nostr_cleanup(); return 1; @@ -75,28 +61,29 @@ int main(int argc, char** argv) { if (nostr_handler_init(&cfg) != 0) { fprintf(stderr, "Failed to initialize nostr handler\n"); llm_cleanup(); - context_free(system_context); config_free(&cfg); nostr_cleanup(); return 1; } + if (nostr_handler_reconcile_startup_events() != 0) { + DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)"); + } + + const char* system_context = nostr_handler_get_system_context(); + if (!system_context || system_context[0] == '\0') { + system_context = "You are Didactyl, a sovereign AI agent living on Nostr."; + } + if (agent_init(&cfg, system_context) != 0) { fprintf(stderr, "Failed to initialize agent\n"); nostr_handler_cleanup(); llm_cleanup(); - context_free(system_context); config_free(&cfg); nostr_cleanup(); return 1; } - DEBUG_INFO("[didactyl] startup phase: publish profile begin"); - if (nostr_handler_publish_profile() != 0) { - DEBUG_WARN("[didactyl] publish profile deferred/failed (will continue startup)"); - } - DEBUG_INFO("[didactyl] startup phase: publish profile end"); - DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin"); if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) { DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed"); @@ -104,7 +91,6 @@ int main(int argc, char** argv) { agent_cleanup(); nostr_handler_cleanup(); llm_cleanup(); - context_free(system_context); config_free(&cfg); nostr_cleanup(); return 1; @@ -129,7 +115,6 @@ int main(int argc, char** argv) { agent_cleanup(); nostr_handler_cleanup(); llm_cleanup(); - context_free(system_context); config_free(&cfg); nostr_cleanup(); diff --git a/src/main.h b/src/main.h index 5a19715..789ab49 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 0 -#define DIDACTYL_VERSION_PATCH 2 -#define DIDACTYL_VERSION "v0.0.2" +#define DIDACTYL_VERSION_PATCH 3 +#define DIDACTYL_VERSION "v0.0.3" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index c4ddf10..a4250b9 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -20,6 +20,7 @@ static int g_poll_counter = 0; static time_t g_start_time = 0; static time_t g_last_status_log_time = 0; static nostr_pool_relay_status_t* g_last_relay_statuses = NULL; +static char* g_system_context = NULL; #define DM_DEDUP_CACHE_SIZE 256 @@ -197,6 +198,32 @@ static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) { return -1; } +static const char* extract_d_tag_value(cJSON* tags) { + if (!tags || !cJSON_IsArray(tags)) { + return NULL; + } + + int n = cJSON_GetArraySize(tags); + for (int i = 0; i < n; i++) { + cJSON* tag = cJSON_GetArrayItem(tags, i); + if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + cJSON* key = cJSON_GetArrayItem(tag, 0); + cJSON* val = cJSON_GetArrayItem(tag, 1); + if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val)) { + continue; + } + + if (strcmp(key->valuestring, "d") == 0 && val->valuestring && val->valuestring[0] != '\0') { + return val->valuestring; + } + } + + return NULL; +} + static void trace_event_json(const char* prefix, cJSON* event) { if (g_debug_level < DEBUG_LEVEL_TRACE || !event) { return; @@ -638,6 +665,96 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms) { return out; } +int nostr_handler_reconcile_startup_events(void) { + if (!g_cfg || !g_pool) { + return -1; + } + + free(g_system_context); + g_system_context = NULL; + + for (int i = 0; i < g_cfg->startup_event_count; i++) { + startup_event_t* se = &g_cfg->startup_events[i]; + if (!se->content) { + continue; + } + + cJSON* tags = NULL; + if (se->tags_json) { + tags = cJSON_Parse(se->tags_json); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(tags); + tags = NULL; + } + } + + cJSON* filter = cJSON_CreateObject(); + cJSON* kinds = cJSON_CreateArray(); + cJSON* authors = cJSON_CreateArray(); + if (!filter || !kinds || !authors) { + cJSON_Delete(filter); + cJSON_Delete(kinds); + cJSON_Delete(authors); + cJSON_Delete(tags); + return -1; + } + + cJSON_AddItemToArray(kinds, cJSON_CreateNumber(se->kind)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); + cJSON_AddItemToObject(filter, "authors", authors); + cJSON_AddNumberToObject(filter, "limit", 20); + + const char* d_tag = extract_d_tag_value(tags); + if (d_tag) { + cJSON* d_values = cJSON_CreateArray(); + cJSON_AddItemToArray(d_values, cJSON_CreateString(d_tag)); + cJSON_AddItemToObject(filter, "#d", d_values); + } + + char* events_json = nostr_handler_query_json(filter, 8000); + cJSON_Delete(filter); + + int found = 0; + if (events_json) { + cJSON* events = cJSON_Parse(events_json); + if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) { + found = 1; + if (se->kind == 31120) { + cJSON* ev = cJSON_GetArrayItem(events, 0); + cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; + if (content && cJSON_IsString(content) && content->valuestring) { + g_system_context = strdup(content->valuestring); + } + } + } + cJSON_Delete(events); + free(events_json); + } + + if (!found) { + if (nostr_handler_publish_kind_event(se->kind, se->content, tags) != 0) { + DEBUG_WARN("[didactyl] startup event publish failed for kind=%d", se->kind); + } + if (se->kind == 31120 && !g_system_context) { + g_system_context = strdup(se->content); + } + } + + cJSON_Delete(tags); + } + + if (!g_system_context) { + g_system_context = strdup("You are Didactyl, a sovereign AI agent living on Nostr."); + } + + return 0; +} + +const char* nostr_handler_get_system_context(void) { + return g_system_context; +} + int nostr_handler_poll(int timeout_ms) { if (!g_pool) { return -1; @@ -670,6 +787,8 @@ void nostr_handler_cleanup(void) { g_cfg = NULL; g_dm_callback = NULL; g_dm_user_data = NULL; + free(g_system_context); + g_system_context = NULL; memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids)); g_seen_dm_count = 0; g_seen_dm_next = 0; diff --git a/src/nostr_handler.h b/src/nostr_handler.h index 9e70260..e6db733 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -13,6 +13,8 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags); char* nostr_handler_query_json(cJSON* filter, int timeout_ms); int nostr_handler_poll(int timeout_ms); +int nostr_handler_reconcile_startup_events(void); +const char* nostr_handler_get_system_context(void); void nostr_handler_cleanup(void); #endif \ No newline at end of file diff --git a/src/tools.c b/src/tools.c index 2961818..7358eff 100644 --- a/src/tools.c +++ b/src/tools.c @@ -5,6 +5,7 @@ #include #include #include +#include #include "cjson/cJSON.h" #include "nostr_handler.h" @@ -29,6 +30,33 @@ static char* json_success_with_message(const char* msg) { return out; } +static int is_safe_relative_path(const char* path) { + if (!path || path[0] == '\0') return 0; + if (path[0] == '/') return 0; + if (strstr(path, "..") != NULL) return 0; + if (strchr(path, '\\') != NULL) return 0; + return 1; +} + +static int build_tool_path(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) { + if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1; + if (!is_safe_relative_path(rel_path)) return -1; + + const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0' + ? ctx->cfg->tools.shell.working_directory + : "."; + + int n = 0; + if (strcmp(cwd, ".") == 0) { + n = snprintf(out, out_size, "%s", rel_path); + } else { + n = snprintf(out, out_size, "%s/%s", cwd, rel_path); + } + + if (n < 0 || (size_t)n >= out_size) return -1; + return 0; +} + int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) { if (!ctx || !cfg) return -1; memset(ctx, 0, sizeof(*ctx)); @@ -120,6 +148,60 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) { cJSON_AddItemToObject(t3, "function", t3_fn); cJSON_AddItemToArray(tools, t3); + cJSON* t4 = cJSON_CreateObject(); + cJSON* t4_fn = cJSON_CreateObject(); + cJSON* t4_params = cJSON_CreateObject(); + cJSON* t4_props = cJSON_CreateObject(); + cJSON* t4_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t4, "type", "function"); + cJSON_AddStringToObject(t4_fn, "name", "file_read"); + cJSON_AddStringToObject(t4_fn, "description", "Read a local file as text from the configured working directory"); + cJSON_AddStringToObject(t4_params, "type", "object"); + cJSON_AddItemToObject(t4_params, "properties", t4_props); + cJSON_AddItemToObject(t4_params, "required", t4_required); + + cJSON* p_fr_path = cJSON_CreateObject(); + cJSON_AddStringToObject(p_fr_path, "type", "string"); + cJSON_AddItemToObject(t4_props, "path", p_fr_path); + cJSON* p_fr_max = cJSON_CreateObject(); + cJSON_AddStringToObject(p_fr_max, "type", "integer"); + cJSON_AddItemToObject(t4_props, "max_bytes", p_fr_max); + cJSON_AddItemToArray(t4_required, cJSON_CreateString("path")); + + cJSON_AddItemToObject(t4_fn, "parameters", t4_params); + cJSON_AddItemToObject(t4, "function", t4_fn); + cJSON_AddItemToArray(tools, t4); + + cJSON* t5 = cJSON_CreateObject(); + cJSON* t5_fn = cJSON_CreateObject(); + cJSON* t5_params = cJSON_CreateObject(); + cJSON* t5_props = cJSON_CreateObject(); + cJSON* t5_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t5, "type", "function"); + cJSON_AddStringToObject(t5_fn, "name", "file_write"); + cJSON_AddStringToObject(t5_fn, "description", "Write text content to a local file in the configured working directory"); + cJSON_AddStringToObject(t5_params, "type", "object"); + cJSON_AddItemToObject(t5_params, "properties", t5_props); + cJSON_AddItemToObject(t5_params, "required", t5_required); + + cJSON* p_fw_path = cJSON_CreateObject(); + cJSON_AddStringToObject(p_fw_path, "type", "string"); + cJSON_AddItemToObject(t5_props, "path", p_fw_path); + cJSON* p_fw_content = cJSON_CreateObject(); + cJSON_AddStringToObject(p_fw_content, "type", "string"); + cJSON_AddItemToObject(t5_props, "content", p_fw_content); + cJSON* p_fw_append = cJSON_CreateObject(); + cJSON_AddStringToObject(p_fw_append, "type", "boolean"); + cJSON_AddItemToObject(t5_props, "append", p_fw_append); + cJSON_AddItemToArray(t5_required, cJSON_CreateString("path")); + cJSON_AddItemToArray(t5_required, cJSON_CreateString("content")); + + cJSON_AddItemToObject(t5_fn, "parameters", t5_params); + cJSON_AddItemToObject(t5, "function", t5_fn); + cJSON_AddItemToArray(tools, t5); + char* out = cJSON_PrintUnformatted(tools); cJSON_Delete(tools); return out; @@ -249,6 +331,106 @@ static char* execute_shell_exec(tools_context_t* ctx, const char* args_json) { return json; } +static char* execute_file_read(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path"); + cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes"); + if (!path || !cJSON_IsString(path) || !path->valuestring) { + cJSON_Delete(args); + return json_error("file_read requires string path"); + } + + int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536; + int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max; + if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max; + + char file_path[PATH_MAX]; + if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) { + cJSON_Delete(args); + return json_error("file_read path is not allowed"); + } + + FILE* fp = fopen(file_path, "rb"); + cJSON_Delete(args); + if (!fp) return json_error("file_read failed to open file"); + + char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U); + if (!buf) { + fclose(fp); + return json_error("allocation failure"); + } + + size_t n = fread(buf, 1, (size_t)max_bytes, fp); + int truncated = !feof(fp) ? 1 : 0; + fclose(fp); + buf[n] = '\0'; + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(buf); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "path", file_path); + cJSON_AddNumberToObject(out, "bytes_read", (double)n); + cJSON_AddBoolToObject(out, "truncated", truncated); + cJSON_AddStringToObject(out, "content", buf); + free(buf); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + +static char* execute_file_write(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path"); + cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content"); + cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append"); + if (!path || !cJSON_IsString(path) || !path->valuestring || + !content || !cJSON_IsString(content) || !content->valuestring) { + cJSON_Delete(args); + return json_error("file_write requires string path and content"); + } + + char file_path[PATH_MAX]; + if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) { + cJSON_Delete(args); + return json_error("file_write path is not allowed"); + } + + const char* content_str = content->valuestring; + size_t len = strlen(content_str); + int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0; + + FILE* fp = fopen(file_path, do_append ? "ab" : "wb"); + cJSON_Delete(args); + if (!fp) return json_error("file_write failed to open file"); + + size_t n = fwrite(content_str, 1, len, fp); + fclose(fp); + if (n != len) return json_error("file_write failed to write all bytes"); + + cJSON* out = cJSON_CreateObject(); + if (!out) return NULL; + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "path", file_path); + cJSON_AddNumberToObject(out, "bytes_written", (double)n); + cJSON_AddBoolToObject(out, "append", do_append); + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json) { if (!tool_name) return json_error("missing tool name"); @@ -261,6 +443,12 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg if (strcmp(tool_name, "shell_exec") == 0) { return execute_shell_exec(ctx, args_json); } + if (strcmp(tool_name, "file_read") == 0) { + return execute_file_read(ctx, args_json); + } + if (strcmp(tool_name, "file_write") == 0) { + return execute_file_write(ctx, args_json); + } return json_error("unknown tool"); }