Files
didactyl/src/main.c
T

1601 lines
56 KiB
C

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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 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 <path>\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 <nsec_or_hex>\n"
" Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n"
" --admin <npub_or_hex>\n"
" Administrator pubkey override; accepts npub1... or 64-char hex.\n"
" --api-port <port>\n"
" Enable HTTP API and bind to this port (1-65535).\n"
" --api-bind <addr>\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 <name> <args_json>\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=<unavailable>", 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", 1);
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* ev = cJSON_GetArrayItem(arr, 0);
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(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_llm_config(didactyl_config_t* cfg, const char* plaintext) {
if (!cfg || !plaintext) return -1;
cJSON* root = cJSON_Parse(plaintext);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");
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;
}
cJSON_Delete(root);
return 0;
}
static int apply_recalled_agent_config(didactyl_config_t* cfg, const char* plaintext) {
if (!cfg || !plaintext) return -1;
cJSON* root = cJSON_Parse(plaintext);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
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) {
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
}
}
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
dm_protocol_from_string(dm_protocol->valuestring, cfg);
}
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;
int llm_rc = 0;
if (persist_llm_config) {
cJSON* llm = cJSON_CreateObject();
if (!llm) return -1;
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
char* llm_json = cJSON_PrintUnformatted(llm);
cJSON_Delete(llm);
if (!llm_json) return -1;
llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
free(llm_json);
} else {
DEBUG_INFO("[didactyl] startup phase: skipping llm_config publish because it was recalled from Nostr");
}
cJSON* agent = cJSON_CreateObject();
if (!agent) return llm_rc;
cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol));
char* agent_json = cJSON_PrintUnformatted(agent);
cJSON_Delete(agent);
if (!agent_json) return llm_rc;
int agent_rc = publish_encrypted_self_config(cfg, "agent_config", agent_json);
free(agent_json);
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
}
static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
if (!cfg) {
return 0;
}
int llm_recalled_from_nostr = 0;
if (!llm_config_is_complete(&cfg->llm)) {
char* llm_plaintext = NULL;
if (fetch_self_config_plaintext(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
if (apply_recalled_llm_config(cfg, llm_plaintext) == 0) {
llm_recalled_from_nostr = 1;
DEBUG_INFO("[didactyl] startup phase: recovered llm_config from Nostr");
} else {
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled llm_config");
}
} else {
DEBUG_WARN("[didactyl] startup phase: llm_config recall unavailable");
}
free(llm_plaintext);
}
char* agent_plaintext = NULL;
if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
didactyl_config_t recalled = *cfg;
if (apply_recalled_agent_config(&recalled, agent_plaintext) == 0) {
if (!admin_config_is_complete(cfg)) {
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", recalled.admin.pubkey);
cfg->dm_protocol = recalled.dm_protocol;
DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr");
} else if (recalled.admin.pubkey[0] != '\0' && strcmp(cfg->admin.pubkey, recalled.admin.pubkey) != 0) {
DEBUG_WARN("[didactyl] startup phase: agent_config admin pubkey differs from loaded config (loaded=%.16s..., nostr=%.16s...); loaded config wins",
cfg->admin.pubkey,
recalled.admin.pubkey);
}
} else {
DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled agent_config");
}
} else {
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
}
free(agent_plaintext);
return llm_recalled_from_nostr;
}
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 llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg);
startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "llm_config recalled" : NULL);
startup_step_begin(3, "Validate LLM config");
if (!llm_config_is_complete(&cfg.llm)) {
startup_step_fail(3, "Validate LLM config", "missing llm_config (model/base_url/api_key)");
fprintf(stderr,
"Missing LLM config: provide genesis file values or store d=llm_config via config_store\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=agent_config via config_store\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 (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 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");
}
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);
while (g_running) {
(void)nostr_handler_poll(100);
(void)trigger_manager_poll(&trigger_manager);
if (http_api_started) {
(void)http_api_poll(0);
}
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;
}