From 2a091aabf7e8935d0e34e522ecb03d16a1db33a8 Mon Sep 17 00:00:00 2001 From: Didactyl User Date: Thu, 16 Apr 2026 18:06:20 -0400 Subject: [PATCH] v0.2.45 - Expand skill refresh to include adopted authors and clean up skill_list output --- README.md | 4 +- src/main.h | 4 +- src/nostr_handler.c | 116 ++++++++++++++++++++++++++- src/tools/tool_skill.c | 172 +++++++++++++++++++++++++++++++---------- 4 files changed, 249 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index b676f94..d963e7b 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.2.44 +## Current Status — v0.2.45 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.44 — Fix adoption list selection to self-authored latest event in agent and skill tool +> Last release update: v0.2.45 — Expand skill refresh to include adopted authors and clean up skill_list output - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/main.h b/src/main.h index f1d2d60..302bbde 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 2 -#define DIDACTYL_VERSION_PATCH 44 -#define DIDACTYL_VERSION "v0.2.44" +#define DIDACTYL_VERSION_PATCH 45 +#define DIDACTYL_VERSION "v0.2.45" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 113e93b..5f1a380 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -1535,11 +1535,13 @@ static void self_skill_cache_upsert_event_locked(cJSON* event) { int is_agent_author = (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0); int is_admin_author = (g_cfg->admin.pubkey[0] != '\0' && strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0); - if (!is_agent_author && !is_admin_author) { - return; + if (kind_val == 10123) { + if (!is_agent_author && !is_admin_author) { + return; + } } - if (is_admin_author && kind_val != 31123) { + if (is_admin_author && kind_val == 31124) { return; } @@ -3736,6 +3738,114 @@ int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms) { return -1; } + cJSON* adopted_authors = cJSON_CreateObject(); + if (adopted_authors) { + cJSON* latest_self_adoption = NULL; + double latest_self_adoption_created = -1.0; + int n0 = cJSON_GetArraySize(events); + for (int i = 0; i < n0; i++) { + cJSON* ev = cJSON_GetArrayItem(events, i); + if (!ev || !cJSON_IsObject(ev)) { + continue; + } + + cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey"); + if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 10123 || + !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || + strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) { + continue; + } + + cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); + double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0; + if (!latest_self_adoption || created > latest_self_adoption_created) { + latest_self_adoption = ev; + latest_self_adoption_created = created; + } + } + + if (latest_self_adoption) { + cJSON* tags = cJSON_GetObjectItemCaseSensitive(latest_self_adoption, "tags"); + if (tags && cJSON_IsArray(tags)) { + int tn = cJSON_GetArraySize(tags); + for (int ti = 0; ti < tn; ti++) { + cJSON* tag = cJSON_GetArrayItem(tags, ti); + if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + cJSON* k = cJSON_GetArrayItem(tag, 0); + cJSON* v = cJSON_GetArrayItem(tag, 1); + if (!k || !v || !cJSON_IsString(k) || !k->valuestring || + !cJSON_IsString(v) || !v->valuestring || strcmp(k->valuestring, "a") != 0) { + continue; + } + + int adopted_kind = 0; + char adopted_pubkey[65] = {0}; + char adopted_d_tag[65] = {0}; + if (parse_skill_address_local(v->valuestring, &adopted_kind, adopted_pubkey, adopted_d_tag) != 0) { + continue; + } + if (adopted_pubkey[0] == '\0') { + continue; + } + if (!cJSON_GetObjectItemCaseSensitive(adopted_authors, adopted_pubkey)) { + cJSON_AddBoolToObject(adopted_authors, adopted_pubkey, 1); + } + } + } + } + + int adopted_author_count = 0; + cJSON* adopted_it = NULL; + cJSON_ArrayForEach(adopted_it, adopted_authors) { + adopted_author_count++; + } + + if (adopted_author_count > 0) { + cJSON* adopted_filter = cJSON_CreateObject(); + cJSON* adopted_kinds = cJSON_CreateArray(); + cJSON* adopted_author_arr = cJSON_CreateArray(); + if (adopted_filter && adopted_kinds && adopted_author_arr) { + cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31123)); + cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31124)); + cJSON_AddItemToObject(adopted_filter, "kinds", adopted_kinds); + + cJSON_ArrayForEach(adopted_it, adopted_authors) { + if (adopted_it->string && adopted_it->string[0] != '\0') { + cJSON_AddItemToArray(adopted_author_arr, cJSON_CreateString(adopted_it->string)); + } + } + cJSON_AddItemToObject(adopted_filter, "authors", adopted_author_arr); + cJSON_AddNumberToObject(adopted_filter, "limit", 1000); + + char* adopted_json = nostr_handler_query_json(adopted_filter, effective_timeout_ms); + if (adopted_json) { + cJSON* adopted_events = cJSON_Parse(adopted_json); + free(adopted_json); + if (adopted_events && cJSON_IsArray(adopted_events)) { + int an = cJSON_GetArraySize(adopted_events); + for (int ai = 0; ai < an; ai++) { + cJSON* ev = cJSON_GetArrayItem(adopted_events, ai); + cJSON* dup = ev ? cJSON_Duplicate(ev, 1) : NULL; + if (dup) { + cJSON_AddItemToArray(events, dup); + } + } + } + cJSON_Delete(adopted_events); + } + } else { + cJSON_Delete(adopted_filter); + cJSON_Delete(adopted_kinds); + cJSON_Delete(adopted_author_arr); + } + cJSON_Delete(adopted_filter); + } + } + cJSON_Delete(adopted_authors); + int merged = 0; pthread_mutex_lock(&g_self_skill_mutex); int n = cJSON_GetArraySize(events); diff --git a/src/tools/tool_skill.c b/src/tools/tool_skill.c index f270c4e..82d0e71 100644 --- a/src/tools/tool_skill.c +++ b/src/tools/tool_skill.c @@ -3,6 +3,8 @@ #include "tools_internal.h" #include +#include +#include #include #include #include @@ -131,6 +133,52 @@ static cJSON* parse_args_local(const char* args_json) { return args; } +static int append_textf_skill_list_local(char** out, size_t* cap, size_t* used, const char* fmt, ...) { + if (!out || !cap || !used || !fmt) { + return -1; + } + + va_list ap; + va_start(ap, fmt); + va_list ap2; + va_copy(ap2, ap); + int needed = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (needed < 0) { + va_end(ap2); + return -1; + } + + size_t need = (size_t)needed; + if (*used + need + 1U > *cap) { + size_t new_cap = (*cap > 0U) ? *cap : 256U; + while (*used + need + 1U > new_cap) { + if (new_cap > (SIZE_MAX / 2U)) { + va_end(ap2); + return -1; + } + new_cap *= 2U; + } + + char* grown = (char*)realloc(*out, new_cap); + if (!grown) { + va_end(ap2); + return -1; + } + *out = grown; + *cap = new_cap; + } + + int wrote = vsnprintf((*out) + (*used), *cap - *used, fmt, ap2); + va_end(ap2); + if (wrote < 0) { + return -1; + } + + *used += (size_t)wrote; + return 0; +} + static int is_hex_string_len_local(const char* s, size_t expected_len) { if (!s || strlen(s) != expected_len) return 0; for (size_t i = 0; i < expected_len; i++) { @@ -1192,11 +1240,9 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) { } cJSON* out = cJSON_CreateObject(); - cJSON* summary_rows = cJSON_CreateArray(); cJSON* skills = cJSON_CreateArray(); - if (!out || !summary_rows || !skills) { + if (!out || !skills) { cJSON_Delete(out); - cJSON_Delete(summary_rows); cJSON_Delete(skills); cJSON_Delete(events); cJSON_Delete(adoption_tags); @@ -1204,7 +1250,21 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) { return NULL; } + size_t rendered_cap = 512U; + size_t rendered_used = 0U; + char* rendered_lines = (char*)malloc(rendered_cap); + if (!rendered_lines) { + cJSON_Delete(out); + cJSON_Delete(skills); + cJSON_Delete(events); + cJSON_Delete(adoption_tags); + free(adoption_content); + return NULL; + } + rendered_lines[0] = '\0'; + int adopted_count = 0; + int skill_count = 0; int event_count = cJSON_GetArraySize(events); for (int i = 0; i < event_count; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); @@ -1236,59 +1296,91 @@ char* execute_skill_list(tools_context_t* ctx, const char* args_json) { if (adopted_filter >= 0 && is_adopted != adopted_filter) { continue; } + + const char* owner = skill_owner_label_local(ctx->cfg, pubkey->valuestring); + cJSON* scope_tag = find_tag_value_string_local(tags, "scope"); + cJSON* desc_tag = find_tag_value_string_local(tags, "description"); + const char* scope_out = (scope_tag && cJSON_IsString(scope_tag) && scope_tag->valuestring) + ? scope_tag->valuestring + : ((kind_val == 31124) ? "private" : "public"); + const char* desc_out = (desc_tag && cJSON_IsString(desc_tag) && desc_tag->valuestring) + ? desc_tag->valuestring + : ""; + + cJSON* row = cJSON_CreateObject(); + if (!row) { + continue; + } + cJSON_AddStringToObject(row, "d_tag", d->valuestring); + cJSON_AddStringToObject(row, "owner", owner); + cJSON_AddStringToObject(row, "scope", scope_out); + cJSON_AddNumberToObject(row, "kind", kind_val); + cJSON_AddBoolToObject(row, "adopted", is_adopted ? 1 : 0); + if (desc_out[0] != '\0') { + cJSON_AddStringToObject(row, "description", desc_out); + } + cJSON_AddItemToArray(skills, row); + if (is_adopted) { adopted_count++; } + skill_count++; - const char* owner = skill_owner_label_local(ctx->cfg, pubkey->valuestring); - - cJSON* detail = extract_skill_summary_local(ev); - if (detail) { - cJSON_AddStringToObject(detail, "owner", owner); - cJSON_AddBoolToObject(detail, "adopted", is_adopted ? 1 : 0); - cJSON_AddItemToArray(skills, detail); - } - - cJSON* row = cJSON_CreateObject(); - if (row) { - cJSON_AddStringToObject(row, "d_tag", d->valuestring); - cJSON_AddStringToObject(row, "owner", owner); - cJSON_AddBoolToObject(row, "adopted", is_adopted ? 1 : 0); - cJSON_AddItemToArray(summary_rows, row); + if (append_textf_skill_list_local(&rendered_lines, + &rendered_cap, + &rendered_used, + "- %s | owner=%s | scope=%s | adopted=%s%s%s\n", + d->valuestring, + owner, + scope_out, + is_adopted ? "yes" : "no", + desc_out[0] ? " | description=" : "", + desc_out) != 0) { + free(rendered_lines); + cJSON_Delete(out); + cJSON_Delete(skills); + cJSON_Delete(events); + cJSON_Delete(adoption_tags); + free(adoption_content); + return NULL; } } - cJSON* skills_pretty = cJSON_CreateArray(); - if (!skills_pretty) { + if (skill_count == 0) { + if (append_textf_skill_list_local(&rendered_lines, &rendered_cap, &rendered_used, "- (none)\n") != 0) { + free(rendered_lines); + cJSON_Delete(out); + cJSON_Delete(skills); + cJSON_Delete(events); + cJSON_Delete(adoption_tags); + free(adoption_content); + return NULL; + } + } + + char header[128]; + snprintf(header, sizeof(header), "Skills: %d total (%d adopted)\n", skill_count, adopted_count); + size_t header_len = strlen(header); + char* rendered = (char*)malloc(header_len + rendered_used + 1U); + if (!rendered) { + free(rendered_lines); cJSON_Delete(out); + cJSON_Delete(skills); cJSON_Delete(events); cJSON_Delete(adoption_tags); free(adoption_content); return NULL; } - - int skill_count = cJSON_GetArraySize(skills); - for (int i = 0; i < skill_count; i++) { - cJSON* skill_item = cJSON_GetArrayItem(skills, i); - if (!skill_item) continue; - char* skill_json = cJSON_PrintUnformatted(skill_item); - if (!skill_json) continue; - - cJSON* block = cJSON_CreateString(skill_json); - free(skill_json); - if (block) { - cJSON_AddItemToArray(skills_pretty, block); - } - } + memcpy(rendered, header, header_len); + memcpy(rendered + header_len, rendered_lines, rendered_used + 1U); + free(rendered_lines); cJSON_AddBoolToObject(out, "success", 1); - cJSON_AddBoolToObject(out, "fallback_used", 0); - cJSON_AddStringToObject(out, "skills_separator", "\n\n\n"); - cJSON_AddItemToObject(out, "summary", summary_rows); - cJSON_AddItemToObject(out, "skills", skills); - cJSON_AddItemToObject(out, "skills_pretty", skills_pretty); - cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills)); + cJSON_AddNumberToObject(out, "count", skill_count); cJSON_AddNumberToObject(out, "adopted_count", adopted_count); + cJSON_AddItemToObject(out, "skills", skills); + cJSON_AddStringToObject(out, "rendered", rendered); + free(rendered); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out);