1708 lines
59 KiB
C
1708 lines
59 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 <errno.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.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 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 <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", 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;
|
|
}
|