#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "main.h" #include "agent.h" #include "config.h" #include "llm.h" #include "nostr_handler.h" #include "debug.h" #include "trigger_manager.h" #include "tools/tools.h" #include "cjson/cJSON.h" #include "http_api.h" #include "setup_wizard.h" #include "cashu_wallet.h" #include "nostr_block_list.h" #include "mongoose.h" static volatile sig_atomic_t g_running = 1; typedef struct { trigger_manager_t* trigger_manager; int loaded_once; } deferred_trigger_load_ctx_t; static void on_self_skill_eose_load_triggers(int event_count, void* user_data) { deferred_trigger_load_ctx_t* ctx = (deferred_trigger_load_ctx_t*)user_data; if (!ctx || !ctx->trigger_manager) { return; } if (ctx->loaded_once) { DEBUG_TRACE("[didactyl] deferred trigger load skipped (already loaded once) event_count=%d", event_count); return; } DEBUG_INFO("[didactyl] deferred trigger load begin after self-skill EOSE (event_count=%d)", event_count); if (trigger_manager_load_from_skills(ctx->trigger_manager) != 0) { DEBUG_WARN("[didactyl] deferred trigger load failed after self-skill EOSE"); return; } ctx->loaded_once = 1; char* status = trigger_manager_status_json(ctx->trigger_manager); if (status) { DEBUG_INFO("[didactyl] deferred trigger load status: %s", status); free(status); } } static volatile sig_atomic_t g_sigint_count = 0; static void signal_handler(int signum) { if (signum == SIGINT) { g_sigint_count++; if (g_sigint_count >= 2) { _exit(130); } } g_running = 0; } static int systemd_notify_send(const char* state) { if (!state || state[0] == '\0') { return 0; } const char* notify_socket = getenv("NOTIFY_SOCKET"); if (!notify_socket || notify_socket[0] == '\0') { return 0; } if (notify_socket[0] != '/' && notify_socket[0] != '@') { return -1; } int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); if (fd < 0) { return -1; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; size_t path_len = strlen(notify_socket); if (path_len >= sizeof(addr.sun_path)) { close(fd); return -1; } memcpy(addr.sun_path, notify_socket, path_len + 1); socklen_t addr_len = (socklen_t)(sizeof(sa_family_t) + path_len + 1U); if (notify_socket[0] == '@') { addr.sun_path[0] = '\0'; addr_len = (socklen_t)(sizeof(sa_family_t) + path_len); } ssize_t sent = sendto(fd, state, strlen(state), MSG_NOSIGNAL, (const struct sockaddr*)&addr, addr_len); int saved_errno = errno; close(fd); errno = saved_errno; if (sent < 0) { return -1; } return 1; } static void configure_mongoose_log_level_from_debug(void) { if (g_debug_level >= DEBUG_LEVEL_TRACE) { mg_log_set(MG_LL_INFO); } else if (g_debug_level >= DEBUG_LEVEL_DEBUG) { mg_log_set(MG_LL_INFO); } else if (g_debug_level >= DEBUG_LEVEL_INFO) { mg_log_set(MG_LL_INFO); } else if (g_debug_level >= DEBUG_LEVEL_WARN) { mg_log_set(MG_LL_ERROR); } else { mg_log_set(MG_LL_NONE); } } static void print_usage(const char* prog) { const char* p = prog ? prog : "didactyl"; fprintf(stderr, "Didactyl - sovereign Nostr agent\n\n" "Usage:\n" " %s [options]\n\n" "Options:\n" " -h, --help\n" " Show this help text and exit.\n" " -v, --version\n" " Show version and exit.\n" " --config \n" " Path to config/genesis JSONC file (default: ./genesis.jsonc).\n" " If file is missing, startup falls back to minimal nsec-only mode.\n" " --nsec \n" " Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n" " --admin \n" " Administrator pubkey override; accepts npub1... or 64-char hex.\n" " --api-port \n" " Enable HTTP API and bind to this port (1-65535).\n" " --api-bind \n" " Enable HTTP API and bind address (example: 127.0.0.1).\n" " --debug <0-5>\n" " Log level (0=fatal, 1=error, 2=warn, 3=info, 4=debug, 5=trace).\n" " --dump-schemas\n" " Print tool schemas JSON and exit.\n" " --test-tool \n" " Execute one tool call locally and print JSON result, then exit.\n" "\n" "Environment:\n" " DIDACTYL_NSEC\n" " Fallback private key when --nsec is not provided.\n" "\n" "Examples:\n" " 1) Genesis-based startup\n" " %s --config genesis.jsonc\n" "\n" " 2) nsec-only startup (no config file)\n" " %s --nsec nsec1...\n" "\n" " 3) nsec via environment variable\n" " DIDACTYL_NSEC=nsec1... %s\n" "\n" " 4) Override API bind/port\n" " %s --config genesis.jsonc --api-bind 127.0.0.1 --api-port 8484\n" "\n" " 5) Dump tool schemas\n" " %s --dump-schemas\n" "\n" " 6) Test a tool invocation\n" " %s --test-tool config_recall '{\"config_name\":\"llm_config\"}'\n", p, p, p, p, p, p, p); } static int derive_keys_from_nsec(const char* nsec_or_hex, didactyl_config_t* cfg) { if (!nsec_or_hex || !cfg) return -1; size_t n = strlen(nsec_or_hex); if (n == 0 || n >= sizeof(cfg->keys.nsec)) { return -1; } snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec_or_hex); int dec_rc = -1; if (strncmp(nsec_or_hex, "nsec1", 5) == 0) { dec_rc = (nostr_decode_nsec(nsec_or_hex, cfg->keys.private_key) == 0) ? 0 : -1; } else if (strlen(nsec_or_hex) == 64U) { dec_rc = (nostr_hex_to_bytes(nsec_or_hex, cfg->keys.private_key, 32) == 0) ? 0 : -1; } if (dec_rc != 0) { return -1; } if (nostr_ec_public_key_from_private_key(cfg->keys.private_key, cfg->keys.public_key) != 0) { return -1; } nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex); return 0; } static int is_first_run_from_kind10002(const didactyl_config_t* cfg) { if (!cfg || cfg->keys.public_key_hex[0] == '\0') { return 1; } 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); return 1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 3000); cJSON_Delete(filter); if (!events_json) { return 1; } int first_run = 1; cJSON* arr = cJSON_Parse(events_json); free(events_json); if (arr && cJSON_IsArray(arr) && cJSON_GetArraySize(arr) > 0) { first_run = 0; } cJSON_Delete(arr); return first_run; } static int tool_result_success(const char* json) { if (!json) { return 0; } cJSON* root = cJSON_Parse(json); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return 0; } cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success"); int ok = (success && cJSON_IsBool(success) && cJSON_IsTrue(success)) ? 1 : 0; cJSON_Delete(root); return ok; } static int wait_for_connected_relays(int timeout_ms) { if (timeout_ms <= 0) { return nostr_handler_connected_relay_count() > 0 ? 1 : 0; } const int step_ms = 100; int elapsed_ms = 0; while (elapsed_ms < timeout_ms) { if (nostr_handler_connected_relay_count() > 0) { return 1; } (void)nostr_handler_poll(step_ms); elapsed_ms += step_ms; } return nostr_handler_connected_relay_count() > 0 ? 1 : 0; } static void startup_step_begin(int step, const char* label) { DEBUG_INFO("[didactyl] startup checklist [%02d] %s: begin", step, label); fprintf(stdout, "[startup %02d] %s ...\n", step, label); fflush(stdout); } static void startup_step_ok(int step, const char* label, const char* detail) { if (detail && detail[0] != '\0') { DEBUG_INFO("[didactyl] startup checklist [%02d] %s: ok (%s)", step, label, detail); fprintf(stdout, "[startup %02d] %s: OK (%s)\n", step, label, detail); } else { DEBUG_INFO("[didactyl] startup checklist [%02d] %s: ok", step, label); fprintf(stdout, "[startup %02d] %s: OK\n", step, label); } fflush(stdout); } static void startup_step_fail(int step, const char* label, const char* detail) { if (detail && detail[0] != '\0') { DEBUG_ERROR("[didactyl] startup checklist [%02d] %s: failed (%s)", step, label, detail); fprintf(stderr, "[startup %02d] %s: FAILED (%s)\n", step, label, detail); } else { DEBUG_ERROR("[didactyl] startup checklist [%02d] %s: failed", step, label); fprintf(stderr, "[startup %02d] %s: FAILED\n", step, label); } fflush(stderr); } static int relay_list_contains(char** relays, int relay_count, const char* relay_url) { if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') { return 0; } for (int i = 0; i < relay_count; i++) { if (relays[i] && strcmp(relays[i], relay_url) == 0) { return 1; } } return 0; } static void free_relay_list(char** relays, int relay_count) { if (!relays) { return; } for (int i = 0; i < relay_count; i++) { free(relays[i]); } free(relays); } static int query_self_kind10002_relays(const didactyl_config_t* cfg, int timeout_ms, char*** out_relays, int* out_relay_count) { if (!cfg || !out_relays || !out_relay_count || cfg->keys.public_key_hex[0] == '\0') { return -1; } *out_relays = NULL; *out_relay_count = 0; 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); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 16); DEBUG_INFO("[didactyl] kind10002 query begin: timeout_ms=%d author=%.16s... relay_count=%d", timeout_ms, cfg->keys.public_key_hex, cfg->relay_count); char* relay_status_snapshot = nostr_handler_relay_status_json(); if (relay_status_snapshot) { DEBUG_INFO("[didactyl] kind10002 query relay snapshot: %s", relay_status_snapshot); free(relay_status_snapshot); } char* events_json = nostr_handler_query_json(filter, timeout_ms); cJSON_Delete(filter); if (!events_json) { DEBUG_WARN("[didactyl] kind10002 query returned NULL JSON payload"); return 0; } size_t events_json_len = strlen(events_json); cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { DEBUG_WARN("[didactyl] kind10002 query parse failed or non-array payload (len=%zu)", events_json_len); cJSON_Delete(events); return 0; } DEBUG_INFO("[didactyl] kind10002 query received event_count=%d", cJSON_GetArraySize(events)); cJSON* best_tags = NULL; double best_created_at = -1.0; int event_count = cJSON_GetArraySize(events); for (int i = 0; i < event_count; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); if (!ev || !cJSON_IsObject(ev)) { continue; } cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); if (!tags || !cJSON_IsArray(tags)) { continue; } double ts = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0; if (!best_tags || ts >= best_created_at) { best_tags = tags; best_created_at = ts; } } if (!best_tags) { if (event_count == 0) { DEBUG_WARN("[didactyl] kind10002 query returned zero events"); } else { DEBUG_WARN("[didactyl] kind10002 query found events but none had valid tags array"); } cJSON_Delete(events); return 0; } char** relays = NULL; int relay_count = 0; int tag_count = cJSON_GetArraySize(best_tags); for (int i = 0; i < tag_count; i++) { cJSON* tag = cJSON_GetArrayItem(best_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) || !key->valuestring || !val->valuestring) { continue; } if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') { continue; } if (relay_list_contains(relays, relay_count, val->valuestring)) { continue; } char** grown = (char**)realloc(relays, (size_t)(relay_count + 1) * sizeof(char*)); if (!grown) { free_relay_list(relays, relay_count); cJSON_Delete(events); return -1; } relays = grown; relays[relay_count] = strdup(val->valuestring); if (!relays[relay_count]) { free_relay_list(relays, relay_count); cJSON_Delete(events); return -1; } relay_count++; } cJSON_Delete(events); if (relay_count <= 0) { free(relays); return 0; } *out_relays = relays; *out_relay_count = relay_count; cJSON* relays_debug = cJSON_CreateArray(); if (relays_debug) { for (int i = 0; i < relay_count; i++) { if (relays[i]) { cJSON_AddItemToArray(relays_debug, cJSON_CreateString(relays[i])); } } char* relays_debug_json = cJSON_PrintUnformatted(relays_debug); if (relays_debug_json) { DEBUG_INFO("[didactyl] kind10002 query extracted relays=%s", relays_debug_json); free(relays_debug_json); } cJSON_Delete(relays_debug); } return relay_count; } static int merge_discovered_relays_into_config(didactyl_config_t* cfg, char** discovered_relays, int discovered_count) { if (!cfg || !discovered_relays || discovered_count <= 0) { return 0; } int old_count = cfg->relay_count; int max_count = old_count + discovered_count; char** merged = (char**)calloc((size_t)max_count, sizeof(char*)); if (!merged) { return -1; } int merged_count = 0; for (int i = 0; i < old_count; i++) { if (!cfg->relays || !cfg->relays[i] || cfg->relays[i][0] == '\0') { continue; } if (relay_list_contains(merged, merged_count, cfg->relays[i])) { continue; } merged[merged_count] = strdup(cfg->relays[i]); if (!merged[merged_count]) { free_relay_list(merged, merged_count); return -1; } merged_count++; } int added = 0; for (int i = 0; i < discovered_count; i++) { if (!discovered_relays[i] || discovered_relays[i][0] == '\0') { continue; } if (relay_list_contains(merged, merged_count, discovered_relays[i])) { continue; } merged[merged_count] = strdup(discovered_relays[i]); if (!merged[merged_count]) { free_relay_list(merged, merged_count); return -1; } merged_count++; added++; } if (cfg->relays) { for (int i = 0; i < cfg->relay_count; i++) { free(cfg->relays[i]); } free(cfg->relays); } cfg->relays = merged; cfg->relay_count = merged_count; return added; } static int wait_for_self_kind10002_relays(const didactyl_config_t* cfg, int timeout_ms, int poll_ms, char*** out_relays, int* out_relay_count) { if (!cfg || !out_relays || !out_relay_count) { return -1; } *out_relays = NULL; *out_relay_count = 0; if (poll_ms <= 0) { poll_ms = 100; } const int query_timeout_ms = 5000; if (timeout_ms <= 0) { return query_self_kind10002_relays(cfg, query_timeout_ms, out_relays, out_relay_count); } struct timespec start; if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) { return query_self_kind10002_relays(cfg, query_timeout_ms, out_relays, out_relay_count); } for (;;) { char** relays = NULL; int relay_count = 0; int rc = query_self_kind10002_relays(cfg, query_timeout_ms, &relays, &relay_count); if (rc > 0 && relay_count > 0 && relays) { DEBUG_INFO("[didactyl] kind10002 wait success: relay_count=%d", relay_count); for (int i = 0; i < relay_count; i++) { DEBUG_INFO("[didactyl] kind10002 relay[%d]=%s", i, relays[i] ? relays[i] : ""); } char* status_after = nostr_handler_relay_status_json(); if (status_after) { DEBUG_INFO("[didactyl] kind10002 wait success relay snapshot: %s", status_after); free(status_after); } *out_relays = relays; *out_relay_count = relay_count; return relay_count; } free_relay_list(relays, relay_count); char* status_before_poll = nostr_handler_relay_status_json(); if (status_before_poll) { DEBUG_INFO("[didactyl] kind10002 wait retry: rc=%d relay_count=%d snapshot=%s", rc, relay_count, status_before_poll); free(status_before_poll); } else { DEBUG_INFO("[didactyl] kind10002 wait retry: rc=%d relay_count=%d snapshot=", rc, relay_count); } (void)nostr_handler_poll(poll_ms); struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { break; } long elapsed_ms = (long)((now.tv_sec - start.tv_sec) * 1000L) + (long)((now.tv_nsec - start.tv_nsec) / 1000000L); if (elapsed_ms >= timeout_ms) { break; } } DEBUG_WARN("[didactyl] kind10002 wait timed out after %dms", timeout_ms); return 0; } static int llm_config_is_complete(const llm_config_t* llm) { if (!llm) return 0; return llm->api_key[0] != '\0' && llm->model[0] != '\0' && llm->base_url[0] != '\0'; } static int admin_config_is_complete(const didactyl_config_t* cfg) { return cfg && cfg->admin.pubkey[0] != '\0'; } static const char* dm_protocol_to_string(dm_protocol_t protocol) { switch (protocol) { case DM_PROTOCOL_NIP17: return "nip17"; case DM_PROTOCOL_BOTH: return "both"; case DM_PROTOCOL_NIP04: default: return "nip04"; } } static void dm_protocol_from_string(const char* s, didactyl_config_t* cfg) { if (!cfg || !s) return; if (strcmp(s, "nip17") == 0) { cfg->dm_protocol = DM_PROTOCOL_NIP17; } else if (strcmp(s, "both") == 0) { cfg->dm_protocol = DM_PROTOCOL_BOTH; } else { cfg->dm_protocol = DM_PROTOCOL_NIP04; } } static int decode_pubkey_hex_or_npub_local(const char* input, char out_hex[65]) { if (!input || !out_hex) return -1; if (strncmp(input, "npub1", 5) == 0) { unsigned char pubkey[32]; if (nostr_decode_npub(input, pubkey) != 0) { return -1; } nostr_bytes_to_hex(pubkey, 32, out_hex); return 0; } if (strlen(input) == 64U) { unsigned char buf[32]; if (nostr_hex_to_bytes(input, buf, 32) != 0) { return -1; } snprintf(out_hex, 65, "%s", input); return 0; } return -1; } static int fetch_self_config_plaintext(const didactyl_config_t* cfg, const char* d_tag, char** out_plaintext) { if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') { return -1; } *out_plaintext = NULL; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); cJSON* d_vals = cJSON_CreateArray(); if (!filter || !kinds || !authors || !d_vals) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); cJSON_Delete(d_vals); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag)); cJSON_AddItemToObject(filter, "#d", d_vals); cJSON_AddNumberToObject(filter, "limit", 20); char* events_json = nostr_handler_query_json(filter, 4000); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); return -1; } cJSON* newest_ev = NULL; long newest_created_at = -1; int n = cJSON_GetArraySize(arr); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(arr, i); if (!ev || !cJSON_IsObject(ev)) { continue; } cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content"); if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { continue; } cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); long ts = (created_at && cJSON_IsNumber(created_at)) ? (long)created_at->valuedouble : 0; if (!newest_ev || ts > newest_created_at) { newest_ev = ev; newest_created_at = ts; } } cJSON* content = newest_ev ? cJSON_GetObjectItemCaseSensitive(newest_ev, "content") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { cJSON_Delete(arr); return -1; } size_t out_cap = strlen(content->valuestring) + 1024U; char* plaintext = (char*)malloc(out_cap); if (!plaintext) { cJSON_Delete(arr); return -1; } int dec_rc = nostr_nip44_decrypt(cfg->keys.private_key, cfg->keys.public_key, content->valuestring, plaintext, out_cap); cJSON_Delete(arr); if (dec_rc != NOSTR_SUCCESS) { free(plaintext); return -1; } *out_plaintext = plaintext; return 0; } static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plaintext, int* out_backfill_needed) { if (!cfg || !plaintext) return -1; if (out_backfill_needed) { *out_backfill_needed = 0; } cJSON* root = cJSON_Parse(plaintext); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return -1; } cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm"); cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl"); if (!global_llm || !cJSON_IsObject(global_llm)) { cJSON_Delete(root); return -1; } cJSON* provider = cJSON_GetObjectItemCaseSensitive(global_llm, "provider"); cJSON* api_key = cJSON_GetObjectItemCaseSensitive(global_llm, "api_key"); cJSON* model = cJSON_GetObjectItemCaseSensitive(global_llm, "model"); cJSON* base_url = cJSON_GetObjectItemCaseSensitive(global_llm, "base_url"); cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature"); cJSON* admin_pubkey = NULL; cJSON* dm_protocol = NULL; cJSON* max_turns = NULL; if (didactyl && cJSON_IsObject(didactyl)) { admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); } else if (out_backfill_needed) { *out_backfill_needed = 1; } if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') { cJSON_Delete(root); return -1; } if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') { snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring); } else if (cfg->llm.provider[0] == '\0') { snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai"); } snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring); snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring); snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring); if (max_tokens && cJSON_IsNumber(max_tokens)) { cfg->llm.max_tokens = (int)max_tokens->valuedouble; } if (temperature && cJSON_IsNumber(temperature)) { cfg->llm.temperature = temperature->valuedouble; } if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') { char decoded[65] = {0}; if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) { cJSON_Delete(root); return -1; } snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded); } else if (out_backfill_needed) { *out_backfill_needed = 1; } if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') { dm_protocol_from_string(dm_protocol->valuestring, cfg); } else if (out_backfill_needed) { *out_backfill_needed = 1; } if (max_turns && cJSON_IsNumber(max_turns)) { int parsed = (int)max_turns->valuedouble; cfg->tools.max_turns = (parsed > 0) ? parsed : 40; } cJSON_Delete(root); return 0; } static int publish_encrypted_self_config(const didactyl_config_t* cfg, const char* d_tag, const char* content_json) { if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1; size_t cipher_cap = (strlen(content_json) * 4U) + 1024U; char* ciphertext = (char*)malloc(cipher_cap); if (!ciphertext) return -1; int enc_rc = nostr_nip44_encrypt(cfg->keys.private_key, cfg->keys.public_key, content_json, ciphertext, cipher_cap); if (enc_rc != NOSTR_SUCCESS) { free(ciphertext); return -1; } cJSON* tags = cJSON_CreateArray(); cJSON* d = cJSON_CreateArray(); cJSON* app = cJSON_CreateArray(); if (!tags || !d || !app) { cJSON_Delete(tags); cJSON_Delete(d); cJSON_Delete(app); free(ciphertext); return -1; } cJSON_AddItemToArray(d, cJSON_CreateString("d")); cJSON_AddItemToArray(d, cJSON_CreateString(d_tag)); cJSON_AddItemToArray(tags, d); cJSON_AddItemToArray(app, cJSON_CreateString("app")); cJSON_AddItemToArray(app, cJSON_CreateString("didactyl")); cJSON_AddItemToArray(tags, app); nostr_publish_result_t result; memset(&result, 0, sizeof(result)); int rc = nostr_handler_publish_kind_event(30078, ciphertext, tags, &result); nostr_handler_publish_result_free(&result); cJSON_Delete(tags); free(ciphertext); return rc; } static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int persist_llm_config) { if (!cfg) return -1; if (!persist_llm_config) { DEBUG_INFO("[didactyl] startup phase: skipping user-settings publish because runtime config was recalled from Nostr"); return 0; } cJSON* user_settings = cJSON_CreateObject(); cJSON* global_llm = cJSON_CreateObject(); cJSON* didactyl = cJSON_CreateObject(); if (!user_settings || !global_llm || !didactyl) { cJSON_Delete(user_settings); cJSON_Delete(global_llm); cJSON_Delete(didactyl); return -1; } cJSON_AddNumberToObject(user_settings, "v", 2); cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL)); cJSON_AddStringToObject(global_llm, "provider", cfg->llm.provider); cJSON_AddStringToObject(global_llm, "api_key", cfg->llm.api_key); cJSON_AddStringToObject(global_llm, "model", cfg->llm.model); cJSON_AddStringToObject(global_llm, "base_url", cfg->llm.base_url); cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->llm.max_tokens); cJSON_AddNumberToObject(global_llm, "temperature", cfg->llm.temperature); cJSON_AddItemToObject(user_settings, "global_llm", global_llm); cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey); cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol)); cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40); cJSON_AddItemToObject(user_settings, "didactyl", didactyl); char* user_settings_json = cJSON_PrintUnformatted(user_settings); cJSON_Delete(user_settings); if (!user_settings_json) { return -1; } int rc = publish_encrypted_self_config(cfg, "user-settings", user_settings_json); free(user_settings_json); return rc; } static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg, int* out_backfill_needed) { if (!cfg) { return 0; } if (out_backfill_needed) { *out_backfill_needed = 0; } char* settings_plaintext = NULL; if (fetch_self_config_plaintext(cfg, "user-settings", &settings_plaintext) != 0 || !settings_plaintext) { DEBUG_WARN("[didactyl] startup phase: user-settings recall unavailable"); free(settings_plaintext); return 0; } int backfill_needed = 0; if (apply_recalled_user_settings(cfg, settings_plaintext, &backfill_needed) != 0) { DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled user-settings"); free(settings_plaintext); return 0; } free(settings_plaintext); if (out_backfill_needed) { *out_backfill_needed = backfill_needed; } if (backfill_needed) { DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr (missing didactyl fields will be backfilled)"); } else { DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr"); } return 1; } int main(int argc, char** argv) { const char* config_path = "./genesis.jsonc"; const char* cli_nsec = NULL; const char* cli_admin_pubkey = NULL; const char* api_bind_override = NULL; int api_port_override = 0; int debug_level = DEBUG_LEVEL_NONE; int dump_schemas = 0; const char* test_tool_name = NULL; const char* test_tool_args = "{}"; didactyl_config_t cfg; memset(&cfg, 0, sizeof(cfg)); int config_preloaded = 0; int bootstrap_mode = 0; if (argc == 1) { if (!isatty(STDIN_FILENO)) { print_usage(argv[0]); return 1; } debug_level = DEBUG_LEVEL_INFO; debug_init(debug_level); configure_mongoose_log_level_from_debug(); DEBUG_INFO("%s %s starting (interactive setup)", DIDACTYL_NAME, DIDACTYL_VERSION); if (nostr_init() != NOSTR_SUCCESS) { fprintf(stderr, "Failed to initialize nostr core\n"); return 1; } char genesis_path[256] = {0}; int wizard_rc = setup_wizard_run(&cfg, genesis_path, sizeof(genesis_path)); if (wizard_rc == SETUP_WIZARD_RC_ABORT) { config_free(&cfg); nostr_cleanup(); return 1; } if (wizard_rc == SETUP_WIZARD_RC_EXIT) { config_free(&cfg); nostr_cleanup(); return 0; } if (wizard_rc == SETUP_WIZARD_RC_BOOTSTRAP) { bootstrap_mode = 1; } config_preloaded = 1; } if (!config_preloaded) { int config_path_explicit = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); return 0; } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { printf("%s %s\n", DIDACTYL_NAME, DIDACTYL_VERSION); return 0; } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { config_path = argv[++i]; config_path_explicit = 1; } else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) { debug_level = atoi(argv[++i]); } else if (strcmp(argv[i], "--nsec") == 0 && i + 1 < argc) { cli_nsec = argv[++i]; } else if (strcmp(argv[i], "--admin") == 0 && i + 1 < argc) { cli_admin_pubkey = argv[++i]; } else if (strcmp(argv[i], "--api-port") == 0 && i + 1 < argc) { api_port_override = atoi(argv[++i]); } else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) { api_bind_override = argv[++i]; } else if (strcmp(argv[i], "--dump-schemas") == 0) { dump_schemas = 1; } else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) { test_tool_name = argv[++i]; test_tool_args = argv[++i]; } else { print_usage(argv[0]); return 1; } } if (config_path_explicit) { bootstrap_mode = 1; } debug_init(debug_level); configure_mongoose_log_level_from_debug(); DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION); if (nostr_init() != NOSTR_SUCCESS) { fprintf(stderr, "Failed to initialize nostr core\n"); return 1; } if (config_load(config_path, &cfg) != 0) { fprintf(stderr, "Failed to load config: %s\n", config_path); fprintf(stderr, "Config error: %s\n", config_last_error()); nostr_cleanup(); return 1; } } if (cfg.keys.nsec[0] == '\0') { const char* env_nsec = getenv("DIDACTYL_NSEC"); const char* input_nsec = (cli_nsec && cli_nsec[0] != '\0') ? cli_nsec : env_nsec; if (!input_nsec || input_nsec[0] == '\0') { fprintf(stderr, "No nsec available: provide key.nsec in config, --nsec, or DIDACTYL_NSEC\n"); config_free(&cfg); nostr_cleanup(); return 1; } if (derive_keys_from_nsec(input_nsec, &cfg) != 0) { fprintf(stderr, "Invalid nsec provided via --nsec or DIDACTYL_NSEC\n"); config_free(&cfg); nostr_cleanup(); return 1; } } if (cli_admin_pubkey && cli_admin_pubkey[0] != '\0') { char decoded_admin[65] = {0}; if (decode_pubkey_hex_or_npub_local(cli_admin_pubkey, decoded_admin) != 0) { fprintf(stderr, "Invalid admin pubkey provided via --admin (expected npub1... or 64-char hex)\n"); config_free(&cfg); nostr_cleanup(); return 1; } snprintf(cfg.admin.pubkey, sizeof(cfg.admin.pubkey), "%s", decoded_admin); DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey); } if (config_ensure_startup_skill_adoption(&cfg) != 0) { fprintf(stderr, "Failed to synthesize startup skill adoption events\n"); config_free(&cfg); nostr_cleanup(); return 1; } if (api_port_override > 0 && api_port_override <= 65535) { cfg.api.enabled = 1; cfg.api.port = api_port_override; } if (api_bind_override && api_bind_override[0] != '\0') { cfg.api.enabled = 1; snprintf(cfg.api.bind_address, sizeof(cfg.api.bind_address), "%s", api_bind_override); } { const char* ca_bundle = nostr_http_detect_ca_bundle(); if (ca_bundle && ca_bundle[0] != '\0') { nostr_http_set_ca_bundle(ca_bundle); } } if (dump_schemas || test_tool_name) { if (llm_init(&cfg.llm) != 0) { fprintf(stderr, "Failed to initialize llm client\n"); config_free(&cfg); nostr_cleanup(); return 1; } if (nostr_handler_init(&cfg) != 0) { fprintf(stderr, "Failed to initialize nostr handler\n"); config_free(&cfg); llm_cleanup(); nostr_cleanup(); return 1; } (void)nostr_block_list_init(&cfg); (void)nostr_block_list_load_from_relays(); tools_context_t tools_ctx; if (tools_init(&tools_ctx, &cfg) != 0) { fprintf(stderr, "Failed to initialize tools context\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); llm_cleanup(); nostr_cleanup(); return 1; } int wallet_initialized = 0; int exit_code = 0; if (cashu_wallet_init(&cfg) == 0) { wallet_initialized = 1; if (cfg.cashu_wallet.enabled) { (void)nostr_handler_subscribe_wallet_events(); int wallet_rc = 0; if (cfg.cashu_wallet.auto_load) { wallet_rc = cashu_wallet_load_from_relays(); } if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) { (void)cashu_wallet_create_new_from_config(); } } } if (test_tool_name) { const int timeout_ms = 15000; if (!wait_for_connected_relays(timeout_ms)) { fprintf(stderr, "No relay connection established within %d ms; refusing to run --test-tool '%s'\n", timeout_ms, test_tool_name); tools_cleanup(&tools_ctx); nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); llm_cleanup(); nostr_cleanup(); return 1; } } if (dump_schemas) { char* schemas = tools_build_openai_schema_json(&tools_ctx); if (!schemas) { fprintf(stderr, "Failed to build tool schemas\n"); exit_code = 1; } else { printf("%s\n", schemas); free(schemas); } } else { char* result = tools_execute(&tools_ctx, test_tool_name, test_tool_args); if (!result) { fprintf(stderr, "Failed to execute tool\n"); exit_code = 1; } else { printf("%s\n", result); exit_code = tool_result_success(result) ? 0 : 1; free(result); } } tools_cleanup(&tools_ctx); if (wallet_initialized) { cashu_wallet_cleanup(); } nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); llm_cleanup(); nostr_cleanup(); return exit_code; } if (nostr_handler_init(&cfg) != 0) { fprintf(stderr, "Failed to initialize nostr handler\n"); config_free(&cfg); nostr_cleanup(); return 1; } (void)nostr_block_list_init(&cfg); (void)nostr_block_list_load_from_relays(); const int startup_connect_timeout_ms = 15000; startup_step_begin(1, "Relay connectivity"); if (!wait_for_connected_relays(startup_connect_timeout_ms)) { startup_step_fail(1, "Relay connectivity", "no relay connected within timeout; continuing best-effort"); DEBUG_WARN("[didactyl] startup phase: no relay connected within %d ms; continuing with best effort", startup_connect_timeout_ms); } else { char detail[64]; snprintf(detail, sizeof(detail), "connected relays: %d/%d", nostr_handler_connected_relay_count(), cfg.relay_count); startup_step_ok(1, "Relay connectivity", detail); } startup_step_begin(2, "Recover runtime config from Nostr"); int user_settings_backfill_needed = 0; int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg, &user_settings_backfill_needed); startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? (user_settings_backfill_needed ? "user-settings recalled (backfill required)" : "user-settings recalled") : NULL); startup_step_begin(3, "Validate LLM config"); if (!llm_config_is_complete(&cfg.llm)) { startup_step_fail(3, "Validate LLM config", "missing user-settings.global_llm (model/base_url/api_key)"); fprintf(stderr, "Missing LLM config: provide genesis file values or store d=user-settings with global_llm\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } startup_step_ok(3, "Validate LLM config", NULL); startup_step_begin(4, "Validate admin config"); if (!admin_config_is_complete(&cfg)) { startup_step_fail(4, "Validate admin config", "missing admin pubkey"); fprintf(stderr, "Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=user-settings with didactyl.admin_pubkey\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } startup_step_ok(4, "Validate admin config", NULL); startup_step_begin(5, "Initialize LLM client"); if (llm_init(&cfg.llm) != 0) { startup_step_fail(5, "Initialize LLM client", "llm_init failed"); fprintf(stderr, "Failed to initialize llm client\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } startup_step_ok(5, "Initialize LLM client", NULL); startup_step_begin(6, "Detect first run via kind 10002"); int first_run = is_first_run_from_kind10002(&cfg); DEBUG_INFO("[didactyl] startup phase: first-run detection via kind 10002 => %s", first_run ? "first-run" : "subsequent-run"); startup_step_ok(6, "Detect first run via kind 10002", first_run ? "first-run" : "subsequent-run"); startup_step_begin(7, "Reconcile/persist startup state"); if (bootstrap_mode || first_run) { if (nostr_handler_reconcile_startup_events() != 0) { startup_step_fail(7, "Reconcile/persist startup state", "startup event reconciliation failed"); fprintf(stderr, "Failed to publish startup events; aborting startup\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } } else { DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run"); } if (bootstrap_mode || first_run || user_settings_backfill_needed) { if (persist_runtime_config_to_nostr(&cfg, (llm_recalled_from_nostr && !user_settings_backfill_needed) ? 0 : 1) != 0) { DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr"); } else { DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr"); } } else { DEBUG_INFO("[didactyl] startup phase: skipping encrypted runtime config publish on subsequent run"); } startup_step_ok(7, "Reconcile/persist startup state", NULL); startup_step_begin(8, "Initialize agent"); if (agent_init(&cfg) != 0) { startup_step_fail(8, "Initialize agent", "agent_init failed"); fprintf(stderr, "Failed to initialize agent\n"); nostr_block_list_cleanup(); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } startup_step_ok(8, "Initialize agent", NULL); startup_step_begin(9, "Initialize trigger manager"); trigger_manager_t trigger_manager; if (trigger_manager_init(&trigger_manager, &cfg) != 0) { startup_step_fail(9, "Initialize trigger manager", "trigger_manager_init failed"); fprintf(stderr, "Failed to initialize trigger manager\n"); agent_cleanup(); nostr_block_list_cleanup(); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } startup_step_ok(9, "Initialize trigger manager", NULL); agent_set_trigger_manager(&trigger_manager); nostr_handler_set_trigger_manager(&trigger_manager); deferred_trigger_load_ctx_t deferred_trigger_ctx = { .trigger_manager = &trigger_manager, .loaded_once = 0 }; nostr_handler_set_self_skill_eose_callback(on_self_skill_eose_load_triggers, &deferred_trigger_ctx); DEBUG_INFO("[didactyl] startup phase: deferred trigger load will run after self-skill EOSE"); startup_step_begin(10, "Load startup triggers"); DEBUG_INFO("[didactyl] startup phase: startup-config trigger load begin"); if (trigger_manager_load_from_startup_events(&trigger_manager) != 0) { DEBUG_WARN("[didactyl] startup phase: startup-config trigger load failed (continuing)"); } DEBUG_INFO("[didactyl] startup phase: startup-config trigger load end"); startup_step_ok(10, "Load startup triggers", NULL); startup_step_begin(11, "Discover self relay list via kind 10002"); const int kind10002_timeout_ms = 25000; char** discovered_relays = NULL; int discovered_count = 0; if (wait_for_self_kind10002_relays(&cfg, kind10002_timeout_ms, 200, &discovered_relays, &discovered_count) <= 0) { startup_step_fail(11, "Discover self relay list via kind 10002", "required relay list not retrievable; aborting startup"); fprintf(stderr, "Startup aborted: could not retrieve valid self kind 10002 within %d ms\n", kind10002_timeout_ms); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } int added_relays = merge_discovered_relays_into_config(&cfg, discovered_relays, discovered_count); free_relay_list(discovered_relays, discovered_count); discovered_relays = NULL; if (added_relays < 0 || nostr_handler_sync_relays_from_config() != 0) { startup_step_fail(11, "Discover self relay list via kind 10002", "failed to apply discovered relay list to pool"); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } if (!wait_for_connected_relays(startup_connect_timeout_ms)) { DEBUG_WARN("[didactyl] startup phase: no relay connected after relay-list expansion; continuing best effort"); } char relay_detail[128]; snprintf(relay_detail, sizeof(relay_detail), "kind10002=%d added=%d connected=%d/%d", discovered_count, added_relays, nostr_handler_connected_relay_count(), cfg.relay_count); startup_step_ok(11, "Discover self relay list via kind 10002", relay_detail); startup_step_begin(12, "Subscribe admin context"); DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin"); if (nostr_handler_subscribe_admin_context() != 0) { DEBUG_WARN("[didactyl] startup phase: subscribe admin context failed (continuing)"); } DEBUG_INFO("[didactyl] startup phase: subscribe admin context end"); startup_step_ok(12, "Subscribe admin context", NULL); startup_step_begin(13, "Subscribe agent self context"); DEBUG_INFO("[didactyl] startup phase: subscribe agent self context begin"); if (nostr_handler_subscribe_agent_context() != 0) { DEBUG_WARN("[didactyl] startup phase: subscribe agent self context failed (continuing)"); } DEBUG_INFO("[didactyl] startup phase: subscribe agent self context end"); startup_step_ok(13, "Subscribe agent self context", NULL); startup_step_begin(14, "Subscribe self-skill cache"); DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin"); if (nostr_handler_subscribe_self_skills() != 0) { startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed"); DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed"); fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n"); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } const int self_skill_eose_timeout_ms = 15000; if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) { startup_step_fail(14, "Subscribe self-skill cache", "self-skill EOSE not received within timeout"); fprintf(stderr, "Startup aborted: self-skill EOSE not received within %d ms\n", self_skill_eose_timeout_ms); fprintf(stderr, "Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n", nostr_handler_connected_relay_count(), cfg.relay_count); fprintf(stderr, "If this is a first run, use genesis to publish startup events\n"); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } int startup_skill_count = 0; int startup_adoption_count = 0; if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) { startup_step_fail(14, "Subscribe self-skill cache", "no skill events found after EOSE"); fprintf(stderr, "Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n"); fprintf(stderr, "The agent requires at least one skill event to operate\n"); fprintf(stderr, "Run genesis to publish the default skill, or publish a skill manually\n"); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } char self_skill_detail[128]; snprintf(self_skill_detail, sizeof(self_skill_detail), "skills=%d adoptions=%d", startup_skill_count, startup_adoption_count); DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end"); startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail); startup_step_begin(15, "Subscribe DMs"); DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin"); if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) { startup_step_fail(15, "Subscribe DMs", "nostr_handler_subscribe_dms failed"); DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed"); fprintf(stderr, "Failed to subscribe to DMs\n"); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 1; } DEBUG_INFO("[didactyl] startup phase: subscribe DMs end"); startup_step_ok(15, "Subscribe DMs", NULL); startup_step_begin(16, "Subscribe wallet events"); DEBUG_INFO("[didactyl] startup phase: subscribe wallet events begin"); if (nostr_handler_subscribe_wallet_events() != 0) { DEBUG_WARN("[didactyl] startup phase: subscribe wallet events failed (continuing)"); } DEBUG_INFO("[didactyl] startup phase: subscribe wallet events end"); startup_step_ok(16, "Subscribe wallet events", NULL); startup_step_begin(17, "Initialize cashu wallet"); if (cashu_wallet_init(&cfg) != 0) { DEBUG_WARN("[didactyl] startup phase: cashu_wallet_init failed (continuing without wallet)"); startup_step_fail(17, "Initialize cashu wallet", "cashu_wallet_init failed; wallet tools may return errors"); } else { int wallet_rc = 0; if (cfg.cashu_wallet.auto_load) { wallet_rc = cashu_wallet_load_from_relays(); } if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) { wallet_rc = cashu_wallet_create_new_from_config(); } if (wallet_rc != 0) { DEBUG_WARN("[didactyl] startup phase: cashu wallet load/create deferred to first tool call"); startup_step_ok(17, "Initialize cashu wallet", "initialized; load/create deferred"); } else { startup_step_ok(17, "Initialize cashu wallet", cfg.cashu_wallet.auto_load ? "loaded or created" : "initialized"); } } struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; (void)sigaction(SIGINT, &sa, NULL); (void)sigaction(SIGTERM, &sa, NULL); struct sigaction sa_pipe; memset(&sa_pipe, 0, sizeof(sa_pipe)); sa_pipe.sa_handler = SIG_IGN; sigemptyset(&sa_pipe.sa_mask); sa_pipe.sa_flags = 0; (void)sigaction(SIGPIPE, &sa_pipe, NULL); int http_api_started = 0; if (cfg.api.enabled) { http_api_context_t http_ctx; memset(&http_ctx, 0, sizeof(http_ctx)); http_ctx.cfg = &cfg; http_ctx.tools_ctx = agent_tools_context(); http_ctx.trigger_manager = &trigger_manager; if (http_api_init(&http_ctx) != 0) { fprintf(stderr, "Failed to initialize HTTP API on %s:%d (continuing without HTTP API)\n", cfg.api.bind_address, cfg.api.port); DEBUG_WARN("[didactyl] HTTP API disabled due to bind/init failure; agent will continue without local API"); http_api_started = 0; } else { http_api_started = 1; DEBUG_INFO("[didactyl] HTTP API listening at http://%s:%d", cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1", cfg.api.port > 0 ? cfg.api.port : 8484); DEBUG_INFO("[didactyl] HTTP API endpoints: http://%s:%d/api/context/current http://%s:%d/api/context/parts", cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1", cfg.api.port > 0 ? cfg.api.port : 8484, cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1", cfg.api.port > 0 ? cfg.api.port : 8484); } } else { DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)"); } nostr_handler_refresh_relay_statuses(); char startup_dm[768]; const char* startup_name = nostr_handler_get_startup_display_name(); int connected_relays = nostr_handler_connected_relay_count(); time_t startup_now = time(NULL); struct tm startup_tm; char startup_time_str[32] = {0}; if (localtime_r(&startup_now, &startup_tm)) { (void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm); } else { snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now); } snprintf(startup_dm, sizeof(startup_dm), "%s has started up and is online at %s (version %s, connected relays: %d/%d).", (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl", startup_time_str, DIDACTYL_VERSION, connected_relays, cfg.relay_count); if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) { DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin"); } startup_step_ok(18, "READY", "agent online; entering main poll loop"); DEBUG_INFO("[didactyl] entering main poll loop"); DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex); (void)systemd_notify_send("READY=1"); time_t last_watchdog_ping = 0; while (g_running) { (void)nostr_handler_poll(100); (void)trigger_manager_poll(&trigger_manager); if (http_api_started) { (void)http_api_poll(0); } time_t now = time(NULL); if (now != (time_t)-1 && (last_watchdog_ping == 0 || (now - last_watchdog_ping) >= 10)) { (void)systemd_notify_send("WATCHDOG=1"); last_watchdog_ping = now; } struct timespec ts = {0, 10 * 1000 * 1000}; nanosleep(&ts, NULL); } DEBUG_INFO("[didactyl] shutting down"); if (http_api_started) { http_api_cleanup(); } cashu_wallet_cleanup(); agent_cleanup(); nostr_block_list_cleanup(); trigger_manager_cleanup(&trigger_manager); nostr_handler_cleanup(); llm_cleanup(); config_free(&cfg); nostr_cleanup(); return 0; }