#define _POSIX_C_SOURCE 200809L #include "tools_internal.h" #include #include #include #include #include "cjson/cJSON.h" #include "../debug.h" #include "../nostr_handler.h" #include "../trigger_manager.h" #include "../agent.h" #include "../llm.h" static char* json_error_local(const char* msg) { cJSON* root = cJSON_CreateObject(); if (!root) return NULL; cJSON_AddBoolToObject(root, "success", 0); cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error"); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); return out; } static const char* skip_ws_local(const char* p) { while (p && *p && isspace((unsigned char)*p)) p++; return p; } static char* sanitize_json_string_controls_local(const char* in) { if (!in) return NULL; size_t len = strlen(in); size_t cap = (len * 2U) + 1U; char* out = (char*)malloc(cap); if (!out) return NULL; int in_string = 0; int escaping = 0; size_t j = 0; for (size_t i = 0; i < len; i++) { char c = in[i]; if (escaping) { if (j + 1U >= cap) { free(out); return NULL; } out[j++] = c; escaping = 0; continue; } if (c == '\\') { if (j + 1U >= cap) { free(out); return NULL; } out[j++] = c; if (in_string) escaping = 1; continue; } if (c == '"') { if (j + 1U >= cap) { free(out); return NULL; } out[j++] = c; in_string = !in_string; continue; } if (in_string && (c == '\n' || c == '\r' || c == '\t')) { if (j + 2U >= cap) { free(out); return NULL; } out[j++] = '\\'; out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't'; continue; } if (j + 1U >= cap) { free(out); return NULL; } out[j++] = c; } out[j] = '\0'; return out; } static cJSON* parse_args_local(const char* args_json) { const char* raw_args_json = args_json ? args_json : "{}"; raw_args_json = skip_ws_local(raw_args_json); if (!raw_args_json || raw_args_json[0] == '\0') { raw_args_json = "{}"; } cJSON* args = cJSON_Parse(raw_args_json); char* repaired_args_json = NULL; if (args && cJSON_IsString(args) && args->valuestring) { cJSON* nested = cJSON_Parse(args->valuestring); if (nested) { cJSON_Delete(args); args = nested; } } if (!args) { repaired_args_json = sanitize_json_string_controls_local(raw_args_json); if (repaired_args_json) { args = cJSON_Parse(repaired_args_json); if (args && cJSON_IsString(args) && args->valuestring) { cJSON* nested = cJSON_Parse(args->valuestring); if (nested) { cJSON_Delete(args); args = nested; } } } } free(repaired_args_json); return args; } 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++) { unsigned char c = (unsigned char)s[i]; if (!isxdigit(c)) return 0; } return 1; } static int add_string_tag_local(cJSON* tags, const char* key, const char* value) { if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1; cJSON* tag = cJSON_CreateArray(); if (!tag) return -1; cJSON_AddItemToArray(tag, cJSON_CreateString(key)); cJSON_AddItemToArray(tag, cJSON_CreateString(value)); cJSON_AddItemToArray(tags, tag); return 0; } static int remove_tag_key_all_local(cJSON* tags, const char* key) { if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') return 0; int removed = 0; for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 1) continue; cJSON* k = cJSON_GetArrayItem(tag, 0); if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) { cJSON_DeleteItemFromArray(tags, i); removed++; } } return removed; } static int upsert_string_tag_local(cJSON* tags, const char* key, const char* value) { if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1; remove_tag_key_all_local(tags, key); return add_string_tag_local(tags, key, value); } static int tag_tuple_equal_local(cJSON* a, cJSON* b) { if (!a || !b || !cJSON_IsArray(a) || !cJSON_IsArray(b)) return 0; int an = cJSON_GetArraySize(a); int bn = cJSON_GetArraySize(b); if (an != bn) return 0; for (int i = 0; i < an; i++) { cJSON* ai = cJSON_GetArrayItem(a, i); cJSON* bi = cJSON_GetArrayItem(b, i); if (!ai || !bi || !cJSON_IsString(ai) || !cJSON_IsString(bi) || !ai->valuestring || !bi->valuestring || strcmp(ai->valuestring, bi->valuestring) != 0) { return 0; } } return 1; } static int tags_contains_tuple_local(cJSON* tags, cJSON* tuple) { if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0; int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* cur = cJSON_GetArrayItem(tags, i); if (tag_tuple_equal_local(cur, tuple)) { return 1; } } return 0; } static int remove_matching_tag_tuples_local(cJSON* tags, cJSON* tuple) { if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0; int removed = 0; for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) { cJSON* cur = cJSON_GetArrayItem(tags, i); if (tag_tuple_equal_local(cur, tuple)) { cJSON_DeleteItemFromArray(tags, i); removed++; } } return removed; } static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) { if (!tags || !key || !cJSON_IsArray(tags)) return NULL; int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue; cJSON* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (k && v && cJSON_IsString(k) && cJSON_IsString(v) && k->valuestring && v->valuestring && strcmp(k->valuestring, key) == 0) { return v; } } return NULL; } static int validate_skill_d_tag_local(const char* d_tag) { if (!d_tag) return 0; size_t len = strlen(d_tag); if (len == 0 || len > 64U) return 0; if (d_tag[0] == '-' || d_tag[len - 1] == '-') return 0; int prev_dash = 0; for (size_t i = 0; i < len; i++) { unsigned char c = (unsigned char)d_tag[i]; if (c == '-') { if (prev_dash) return 0; prev_dash = 1; continue; } if (!islower(c) && !isdigit(c) && c != '_') return 0; prev_dash = 0; } return 1; } static void apply_llm_spec_local_to_config(const char* llm_spec, llm_config_t* cfg) { if (!llm_spec || !cfg) return; const char* spec = llm_spec; while (*spec && isspace((unsigned char)*spec)) spec++; const char* comma = strchr(spec, ','); size_t len = comma ? (size_t)(comma - spec) : strlen(spec); while (len > 0 && isspace((unsigned char)spec[len - 1])) len--; const char* slash = memchr(spec, '/', len); if (slash) { size_t provider_len = (size_t)(slash - spec); const char* model = slash + 1; size_t model_len = len - (size_t)(model - spec); while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--; while (model_len > 0 && isspace((unsigned char)*model)) { model++; model_len--; } while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--; if (provider_len > 0 && provider_len < sizeof(cfg->provider)) { snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec); } if (model_len > 0 && model_len < sizeof(cfg->model)) { snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model); } } else if (len > 0 && len < sizeof(cfg->model)) { snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)len, spec); } } static int ci_contains_local(const char* haystack, const char* needle) { if (!haystack || !needle) return 0; if (needle[0] == '\0') return 1; size_t nlen = strlen(needle); size_t hlen = strlen(haystack); if (nlen > hlen) return 0; for (size_t i = 0; i + nlen <= hlen; i++) { size_t j = 0; while (j < nlen) { unsigned char a = (unsigned char)haystack[i + j]; unsigned char b = (unsigned char)needle[j]; if (tolower(a) != tolower(b)) break; j++; } if (j == nlen) return 1; } return 0; } static int fetch_adoption_list_tags_local(tools_context_t* ctx, cJSON** out_tags, char** out_content) { if (!ctx || !ctx->cfg || !out_tags || !out_content) return -1; *out_tags = NULL; *out_content = NULL; char* events_json = nostr_handler_get_self_events_by_kind_json(10123); cJSON* tags = cJSON_CreateArray(); char* content = strdup(""); if (!tags || !content) { free(events_json); cJSON_Delete(tags); free(content); return -1; } if (events_json) { cJSON* events = cJSON_Parse(events_json); free(events_json); if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) { cJSON* ev0 = cJSON_GetArrayItem(events, 0); if (ev0 && cJSON_IsObject(ev0)) { cJSON* ev_content = cJSON_GetObjectItemCaseSensitive(ev0, "content"); if (ev_content && cJSON_IsString(ev_content) && ev_content->valuestring) { free(content); content = strdup(ev_content->valuestring); if (!content) { cJSON_Delete(events); cJSON_Delete(tags); return -1; } } cJSON* ev_tags = cJSON_GetObjectItemCaseSensitive(ev0, "tags"); if (ev_tags && cJSON_IsArray(ev_tags)) { cJSON* dup = cJSON_Duplicate(ev_tags, 1); if (!dup) { cJSON_Delete(events); cJSON_Delete(tags); free(content); return -1; } cJSON_Delete(tags); tags = dup; } } } cJSON_Delete(events); } *out_tags = tags; *out_content = content; return 0; } static int publish_adoption_list_local(const char* content, cJSON* tags, nostr_publish_result_t* out_result) { if (!tags || !cJSON_IsArray(tags) || !out_result) return -1; memset(out_result, 0, sizeof(*out_result)); return nostr_handler_publish_kind_event(10123, content ? content : "", tags, out_result); } static int adoption_tags_contains_address_local(cJSON* adoption_tags, int kind, const char* pubkey, const char* d_tag) { if (!adoption_tags || !cJSON_IsArray(adoption_tags) || !pubkey || !d_tag || d_tag[0] == '\0') { return 0; } char addr[256]; snprintf(addr, sizeof(addr), "%d:%s:%s", kind, pubkey, d_tag); int tag_count = cJSON_GetArraySize(adoption_tags); for (int i = 0; i < tag_count; i++) { cJSON* tag = cJSON_GetArrayItem(adoption_tags, i); 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) { continue; } if (strcmp(k->valuestring, "a") == 0 && strcmp(v->valuestring, addr) == 0) { return 1; } } return 0; } static const char* skill_owner_label_local(const didactyl_config_t* cfg, const char* pubkey_hex) { if (!cfg || !pubkey_hex || pubkey_hex[0] == '\0') return "unknown"; if (strcmp(pubkey_hex, cfg->keys.public_key_hex) == 0) return "agent"; if (cfg->admin.pubkey[0] != '\0' && strcmp(pubkey_hex, cfg->admin.pubkey) == 0) return "admin"; return "external"; } static cJSON* extract_skill_summary_local(cJSON* event) { if (!event || !cJSON_IsObject(event)) return NULL; cJSON* summary = cJSON_CreateObject(); if (!summary) return NULL; cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); if (kind && cJSON_IsNumber(kind)) { cJSON_AddNumberToObject(summary, "kind", (int)kind->valuedouble); } if (created_at && cJSON_IsNumber(created_at)) { char ts_buf[32]; snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble); cJSON* ts_raw = cJSON_CreateRaw(ts_buf); if (ts_raw) { cJSON_AddItemToObject(summary, "created_at", ts_raw); } } if (id && cJSON_IsString(id) && id->valuestring) { cJSON_AddStringToObject(summary, "id", id->valuestring); } if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) { cJSON_AddStringToObject(summary, "pubkey", pubkey->valuestring); } if (tags && cJSON_IsArray(tags)) { cJSON* d = find_tag_value_string_local(tags, "d"); cJSON* scope = find_tag_value_string_local(tags, "scope"); cJSON* description = find_tag_value_string_local(tags, "description"); if (d && cJSON_IsString(d) && d->valuestring) { cJSON_AddStringToObject(summary, "d_tag", d->valuestring); } if (scope && cJSON_IsString(scope) && scope->valuestring) { cJSON_AddStringToObject(summary, "scope", scope->valuestring); } if (description && cJSON_IsString(description) && description->valuestring) { cJSON_AddStringToObject(summary, "description", description->valuestring); } } if (content && cJSON_IsString(content) && content->valuestring) { size_t len = strlen(content->valuestring); size_t preview_len = len > 200U ? 200U : len; char* preview = (char*)malloc(preview_len + 1U); if (!preview) { cJSON_Delete(summary); return NULL; } memcpy(preview, content->valuestring, preview_len); preview[preview_len] = '\0'; cJSON_AddStringToObject(summary, "content_preview", preview); cJSON_AddNumberToObject(summary, "content_length", (double)len); cJSON_AddBoolToObject(summary, "content_truncated", len > preview_len ? 1 : 0); free(preview); } return summary; } typedef struct { char* d_tag; char* author_pubkey; char* content; cJSON* tags; int kind; int is_own; } skill_fetch_result_local_t; static int g_skill_run_depth_local = 0; static pthread_mutex_t g_skill_run_depth_mutex_local = PTHREAD_MUTEX_INITIALIZER; static void free_skill_fetch_result_local(skill_fetch_result_local_t* result) { if (!result) return; free(result->d_tag); free(result->author_pubkey); free(result->content); cJSON_Delete(result->tags); memset(result, 0, sizeof(*result)); } static int set_error_local(char** out_error, const char* msg) { if (!out_error) return -1; *out_error = strdup(msg ? msg : "error"); return (*out_error) ? 0 : -1; } static int append_simple_message_local(cJSON* messages, const char* role, const char* content) { if (!messages || !role) return -1; cJSON* msg = cJSON_CreateObject(); if (!msg) return -1; cJSON_AddStringToObject(msg, "role", role); cJSON_AddStringToObject(msg, "content", content ? content : ""); cJSON_AddItemToArray(messages, msg); return 0; } static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) { if (!messages || !resp || resp->tool_call_count <= 0) return -1; cJSON* assistant = cJSON_CreateObject(); cJSON* tool_calls = cJSON_CreateArray(); if (!assistant || !tool_calls) { cJSON_Delete(assistant); cJSON_Delete(tool_calls); return -1; } cJSON_AddStringToObject(assistant, "role", "assistant"); if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content); else cJSON_AddNullToObject(assistant, "content"); for (int i = 0; i < resp->tool_call_count; i++) { const llm_tool_call_t* tc = &resp->tool_calls[i]; cJSON* tc_obj = cJSON_CreateObject(); cJSON* fn_obj = cJSON_CreateObject(); if (!tc_obj || !fn_obj) { cJSON_Delete(tc_obj); cJSON_Delete(fn_obj); cJSON_Delete(assistant); cJSON_Delete(tool_calls); return -1; } cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : ""); cJSON_AddStringToObject(tc_obj, "type", "function"); cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : ""); cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}"); cJSON_AddItemToObject(tc_obj, "function", fn_obj); cJSON_AddItemToArray(tool_calls, tc_obj); } cJSON_AddItemToObject(assistant, "tool_calls", tool_calls); cJSON_AddItemToArray(messages, assistant); return 0; } static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) { if (!messages || !tool_call_id) return -1; cJSON* msg = cJSON_CreateObject(); if (!msg) return -1; cJSON_AddStringToObject(msg, "role", "tool"); cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id); cJSON_AddStringToObject(msg, "content", tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}"); cJSON_AddItemToArray(messages, msg); return 0; } static int maybe_append_loaded_skill_instructions_local(cJSON* messages, const char* tool_name, const char* tool_result_json) { if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) { return 0; } cJSON* root = cJSON_Parse(tool_result_json); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return 0; } cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success"); cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type"); cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions"); int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) && type && cJSON_IsString(type) && type->valuestring && strcmp(type->valuestring, "skill_instructions") == 0 && instructions && cJSON_IsString(instructions) && instructions->valuestring && instructions->valuestring[0] != '\0'); int rc = 0; if (should_apply) { rc = append_simple_message_local(messages, "system", instructions->valuestring); } cJSON_Delete(root); return rc; } static int is_tool_sandbox_safe_local(const char* tool_name) { if (!tool_name || tool_name[0] == '\0') return 0; static const char* safe_tools[] = { "nostr_query", "nostr_nip05_lookup", "nostr_profile_get", "nostr_encode", "nostr_decode", "nostr_relay_status", "nostr_relay_info", "nostr_post", "nostr_react", "nostr_list_manage", "nostr_dm_send", "nostr_dm_send_nip17", "skill_list", "skill_search", "skill_get", "skill_view", "skill_load", "skill_run", "memory_recall", "my_memory", "memory_short_term_read", "agent_version", "nostr_pubkey", "nostr_npub", "tool_list", "trigger_list", "message_current", "agent_identity", "admin_identity", NULL }; for (int i = 0; safe_tools[i] != NULL; i++) { if (strcmp(tool_name, safe_tools[i]) == 0) return 1; } return 0; } static char* build_sandboxed_tools_json_local(const tools_context_t* ctx) { char* full_tools_json = tools_build_openai_schema_json(ctx); if (!full_tools_json) return NULL; cJSON* arr = cJSON_Parse(full_tools_json); free(full_tools_json); if (!arr || !cJSON_IsArray(arr)) { cJSON_Delete(arr); return NULL; } for (int i = cJSON_GetArraySize(arr) - 1; i >= 0; i--) { cJSON* item = cJSON_GetArrayItem(arr, i); cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL; cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL; if (!name || !cJSON_IsString(name) || !name->valuestring || !is_tool_sandbox_safe_local(name->valuestring)) { cJSON_DeleteItemFromArray(arr, i); } } char* out = cJSON_PrintUnformatted(arr); cJSON_Delete(arr); return out; } static int fetch_skill_from_events_local(tools_context_t* ctx, cJSON* events, const char* d_tag, const char* pubkey_filter, skill_fetch_result_local_t* out, char** out_error) { if (!ctx || !ctx->cfg || !events || !cJSON_IsArray(events) || !d_tag || !out) { if (out_error) (void)set_error_local(out_error, "skill lookup failed"); return -1; } cJSON* best = NULL; long long best_created = -1; int n = cJSON_GetArraySize(events); for (int i = 0; i < n; 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"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !tags || !cJSON_IsArray(tags)) { continue; } int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124) continue; if (pubkey_filter && strcmp(pubkey_filter, pubkey->valuestring) != 0) continue; cJSON* d_val = find_tag_value_string_local(tags, "d"); if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring || strcmp(d_val->valuestring, d_tag) != 0) continue; cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0; if (!best || created > best_created) { best = ev; best_created = created; } } if (!best) { if (out_error) (void)set_error_local(out_error, "skill not found"); return -1; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(best, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(best, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(best, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(best, "tags"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) { if (out_error) (void)set_error_local(out_error, "malformed skill event"); return -1; } out->kind = (int)kind->valuedouble; out->d_tag = strdup(d_tag); out->author_pubkey = strdup(pubkey->valuestring); out->content = strdup(content->valuestring); out->tags = cJSON_Duplicate(tags, 1); out->is_own = (strcmp(pubkey->valuestring, ctx->cfg->keys.public_key_hex) == 0) ? 1 : 0; if (!out->d_tag || !out->author_pubkey || !out->content || !out->tags) { free_skill_fetch_result_local(out); if (out_error) (void)set_error_local(out_error, "allocation failure"); return -1; } return 0; } static int fetch_skill_by_identifier_local(tools_context_t* ctx, const char* d_tag, const char* pubkey, skill_fetch_result_local_t* out, char** out_error) { if (out) memset(out, 0, sizeof(*out)); if (out_error) *out_error = NULL; if (!ctx || !ctx->cfg || !out || !d_tag || !validate_skill_d_tag_local(d_tag)) { if (out_error) (void)set_error_local(out_error, "skill identifier is invalid"); return -1; } if (pubkey && pubkey[0] != '\0' && !is_hex_string_len_local(pubkey, 64U)) { if (out_error) (void)set_error_local(out_error, "pubkey must be 64-char hex when provided"); return -1; } int own_lookup = (!pubkey || pubkey[0] == '\0' || strcmp(pubkey, ctx->cfg->keys.public_key_hex) == 0); char* events_json = NULL; if (own_lookup) { events_json = nostr_handler_get_self_skill_events_json(); if (!events_json) { if (out_error) (void)set_error_local(out_error, "self skill cache unavailable"); return -1; } } else { cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); cJSON* d_tags = cJSON_CreateArray(); if (!filter || !kinds || !authors || !d_tags) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); cJSON_Delete(d_tags); if (out_error) (void)set_error_local(out_error, "failed to build external skill filter"); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124)); cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey)); cJSON_AddItemToArray(d_tags, cJSON_CreateString(d_tag)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddItemToObject(filter, "#d", d_tags); cJSON_AddNumberToObject(filter, "limit", 20); events_json = nostr_handler_query_json(filter, 8000); cJSON_Delete(filter); if (!events_json) { if (out_error) (void)set_error_local(out_error, "external skill query failed"); return -1; } } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(events); if (out_error) (void)set_error_local(out_error, "skill lookup returned invalid JSON"); return -1; } int rc = fetch_skill_from_events_local(ctx, events, d_tag, own_lookup ? NULL : pubkey, out, out_error); cJSON_Delete(events); return rc; } char* execute_skill_create(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content"); cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope"); cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description"); cJSON* auto_adopt = cJSON_GetObjectItemCaseSensitive(args, "auto_adopt"); cJSON* trigger = cJSON_GetObjectItemCaseSensitive(args, "trigger"); cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter"); cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action"); cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled"); cJSON* llm = cJSON_GetObjectItemCaseSensitive(args, "llm"); cJSON* tools = cJSON_GetObjectItemCaseSensitive(args, "tools"); cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature"); cJSON* seed = cJSON_GetObjectItemCaseSensitive(args, "seed"); if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !content || !cJSON_IsString(content) || !content->valuestring) { cJSON_Delete(args); return json_error_local("skill_create requires d and content strings"); } if (!validate_skill_d_tag_local(d_tag->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create d must be lowercase letters/digits/hyphens (1-64 chars)"); } if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create scope must be string when provided"); } if (description && (!cJSON_IsString(description) || !description->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create description must be string when provided"); } if (trigger && (!cJSON_IsString(trigger) || !trigger->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create trigger must be string when provided"); } if (filter && (!cJSON_IsString(filter) || !filter->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create filter must be string when provided"); } if (action && (!cJSON_IsString(action) || !action->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create action must be string when provided"); } if (enabled && !cJSON_IsBool(enabled)) { cJSON_Delete(args); return json_error_local("skill_create enabled must be boolean when provided"); } if (llm && (!cJSON_IsString(llm) || !llm->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create llm must be string when provided"); } if (tools && (!cJSON_IsString(tools) || !tools->valuestring)) { cJSON_Delete(args); return json_error_local("skill_create tools must be string when provided"); } if (max_tokens && !cJSON_IsNumber(max_tokens)) { cJSON_Delete(args); return json_error_local("skill_create max_tokens must be number when provided"); } if (temperature && !cJSON_IsNumber(temperature)) { cJSON_Delete(args); return json_error_local("skill_create temperature must be number when provided"); } if (seed && !cJSON_IsNumber(seed)) { cJSON_Delete(args); return json_error_local("skill_create seed must be number when provided"); } const char* scope_str = (scope && scope->valuestring && scope->valuestring[0] != '\0') ? scope->valuestring : "public"; int kind = 0; if (strcmp(scope_str, "public") == 0) { kind = 31123; } else if (strcmp(scope_str, "private") == 0) { kind = 31124; } else { cJSON_Delete(args); return json_error_local("skill_create scope must be public or private"); } int do_auto_adopt = (!auto_adopt || !cJSON_IsBool(auto_adopt) || cJSON_IsTrue(auto_adopt)) ? 1 : 0; cJSON* tags = cJSON_CreateArray(); if (!tags) { cJSON_Delete(args); return json_error_local("failed to create skill tags"); } if (add_string_tag_local(tags, "d", d_tag->valuestring) != 0 || add_string_tag_local(tags, "app", "didactyl") != 0 || add_string_tag_local(tags, "scope", scope_str) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add required skill tags"); } if (description && description->valuestring && description->valuestring[0] != '\0') { if (add_string_tag_local(tags, "description", description->valuestring) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add description tag"); } } const char* trigger_str = (trigger && trigger->valuestring && trigger->valuestring[0] != '\0') ? trigger->valuestring : NULL; const char* filter_str = (filter && filter->valuestring && filter->valuestring[0] != '\0') ? filter->valuestring : NULL; const char* action_str = (action && action->valuestring && action->valuestring[0] != '\0') ? action->valuestring : "llm"; int enabled_int = (!enabled || cJSON_IsTrue(enabled)) ? 1 : 0; if (trigger_str && strcmp(trigger_str, "nostr-subscription") != 0 && strcmp(trigger_str, "webhook") != 0 && strcmp(trigger_str, "cron") != 0 && strcmp(trigger_str, "chain") != 0 && strcmp(trigger_str, "dm") != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("skill_create trigger must be one of: nostr-subscription, webhook, cron, chain, dm"); } if ((trigger_str && !filter_str) || (!trigger_str && filter_str)) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("skill_create trigger and filter must both be provided together"); } if (trigger_str) { if (add_string_tag_local(tags, "trigger", trigger_str) != 0 || add_string_tag_local(tags, "filter", filter_str) != 0 || add_string_tag_local(tags, "action", action_str) != 0 || add_string_tag_local(tags, "enabled", enabled_int ? "true" : "false") != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add trigger tags"); } if (llm && llm->valuestring && llm->valuestring[0] != '\0' && add_string_tag_local(tags, "llm", llm->valuestring) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add llm trigger tag"); } if (tools && tools->valuestring && tools->valuestring[0] != '\0' && add_string_tag_local(tags, "tools", tools->valuestring) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add tools trigger tag"); } if (max_tokens) { char buf[32]; snprintf(buf, sizeof(buf), "%d", (int)max_tokens->valuedouble); if (add_string_tag_local(tags, "max_tokens", buf) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add max_tokens trigger tag"); } } if (temperature) { char buf[32]; snprintf(buf, sizeof(buf), "%g", temperature->valuedouble); if (add_string_tag_local(tags, "temperature", buf) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add temperature trigger tag"); } } if (seed) { char buf[32]; snprintf(buf, sizeof(buf), "%d", (int)seed->valuedouble); if (add_string_tag_local(tags, "seed", buf) != 0) { cJSON_Delete(tags); cJSON_Delete(args); return json_error_local("failed to add seed trigger tag"); } } } nostr_publish_result_t skill_publish; memset(&skill_publish, 0, sizeof(skill_publish)); int rc = nostr_handler_publish_kind_event(kind, content->valuestring, tags, &skill_publish); cJSON_Delete(tags); if (rc != 0) { cJSON_Delete(args); nostr_handler_publish_result_free(&skill_publish); return json_error_local("skill_create failed to publish skill event"); } int adopted = 0; int already_adopted = 0; nostr_publish_result_t adoption_publish; memset(&adoption_publish, 0, sizeof(adoption_publish)); if (do_auto_adopt && kind == 31123) { cJSON* adoption_tags = NULL; char* adoption_content = NULL; if (fetch_adoption_list_tags_local(ctx, &adoption_tags, &adoption_content) == 0) { char addr[256]; snprintf(addr, sizeof(addr), "31123:%s:%s", ctx->cfg->keys.public_key_hex, d_tag->valuestring); cJSON* tuple = cJSON_CreateArray(); if (tuple) { cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); if (tags_contains_tuple_local(adoption_tags, tuple)) { already_adopted = 1; adopted = 1; } else { cJSON* dup = cJSON_Duplicate(tuple, 1); if (dup) { cJSON_AddItemToArray(adoption_tags, dup); if (publish_adoption_list_local(adoption_content, adoption_tags, &adoption_publish) == 0) { adopted = 1; } } } cJSON_Delete(tuple); } cJSON_Delete(adoption_tags); free(adoption_content); } } int trigger_registered = 0; if (trigger_str && ctx->trigger_manager && enabled_int) { if (strcmp(action_str, "template") == 0) { DEBUG_WARN("[didactyl] skill_create trigger action template is deprecated; forcing llm for d_tag=%s", d_tag->valuestring); } if (trigger_manager_add(ctx->trigger_manager, d_tag->valuestring, content->valuestring, filter_str, TRIGGER_ACTION_LLM, trigger_str, enabled_int, (llm && llm->valuestring) ? llm->valuestring : NULL, (tools && tools->valuestring) ? tools->valuestring : NULL, max_tokens ? 1 : 0, max_tokens ? (int)max_tokens->valuedouble : 0, temperature ? 1 : 0, temperature ? temperature->valuedouble : 0.0, seed ? 1 : 0, seed ? (int)seed->valuedouble : 0) == 0) { trigger_registered = 1; } } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(args); nostr_handler_publish_result_free(&skill_publish); nostr_handler_publish_result_free(&adoption_publish); return NULL; } cJSON_AddBoolToObject(out, "success", skill_publish.success ? 1 : 0); cJSON_AddStringToObject(out, "d_tag", d_tag->valuestring); cJSON_AddNumberToObject(out, "kind", kind); cJSON_AddStringToObject(out, "scope", scope_str); cJSON_AddStringToObject(out, "skill_event_id", skill_publish.event_id); cJSON_AddBoolToObject(out, "auto_adopt", do_auto_adopt ? 1 : 0); cJSON_AddBoolToObject(out, "adopted", adopted ? 1 : 0); cJSON_AddBoolToObject(out, "already_adopted", already_adopted ? 1 : 0); cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0); if (adoption_publish.event_id[0] != '\0') { cJSON_AddStringToObject(out, "adoption_event_id", adoption_publish.event_id); } if (skill_publish.naddr_uri[0] != '\0') { cJSON_AddStringToObject(out, "naddr_uri", skill_publish.naddr_uri); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(args); nostr_handler_publish_result_free(&skill_publish); nostr_handler_publish_result_free(&adoption_publish); return json; } char* execute_skill_list(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope"); cJSON* adopted = cJSON_GetObjectItemCaseSensitive(args, "adopted"); if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) { cJSON_Delete(args); return json_error_local("skill_list scope must be string when provided"); } if (adopted && !cJSON_IsBool(adopted)) { cJSON_Delete(args); return json_error_local("skill_list adopted must be boolean when provided"); } const char* scope_str = (scope && scope->valuestring && scope->valuestring[0] != '\0') ? scope->valuestring : NULL; int include_public = 1; int include_private = 1; if (scope_str) { if (strcmp(scope_str, "public") == 0) { include_private = 0; } else if (strcmp(scope_str, "private") == 0) { include_public = 0; } else { cJSON_Delete(args); return json_error_local("skill_list scope must be public or private"); } } int adopted_filter = -1; if (adopted) { adopted_filter = cJSON_IsTrue(adopted) ? 1 : 0; } cJSON_Delete(args); cJSON* adoption_tags = NULL; char* adoption_content = NULL; if (fetch_adoption_list_tags_local(ctx, &adoption_tags, &adoption_content) != 0) { return json_error_local("skill_list failed to load adoption list"); } char* events_json = nostr_handler_get_self_skill_events_json(); if (!events_json) { cJSON_Delete(adoption_tags); free(adoption_content); return json_error_local("skill_list cache unavailable"); } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(events); cJSON_Delete(adoption_tags); free(adoption_content); return json_error_local("skill_list cache returned invalid JSON"); } cJSON* out = cJSON_CreateObject(); cJSON* summary_rows = cJSON_CreateArray(); cJSON* skills = cJSON_CreateArray(); if (!out || !summary_rows || !skills) { cJSON_Delete(out); cJSON_Delete(summary_rows); cJSON_Delete(skills); cJSON_Delete(events); cJSON_Delete(adoption_tags); free(adoption_content); return NULL; } int adopted_count = 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* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !tags || !cJSON_IsArray(tags)) { continue; } int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124) { continue; } if ((kind_val == 31123 && !include_public) || (kind_val == 31124 && !include_private)) { continue; } cJSON* d = find_tag_value_string_local(tags, "d"); if (!d || !cJSON_IsString(d) || !d->valuestring || d->valuestring[0] == '\0') { continue; } int is_adopted = adoption_tags_contains_address_local(adoption_tags, kind_val, pubkey->valuestring, d->valuestring) ? 1 : 0; if (adopted_filter >= 0 && is_adopted != adopted_filter) { continue; } if (is_adopted) { adopted_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); } } cJSON* skills_pretty = cJSON_CreateArray(); if (!skills_pretty) { cJSON_Delete(out); 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); } } 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, "adopted_count", adopted_count); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(events); cJSON_Delete(adoption_tags); free(adoption_content); return json; } char* execute_skill_get(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input"); const char* d_tag_value = NULL; if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && validate_skill_d_tag_local(d_tag->valuestring)) { d_tag_value = d_tag->valuestring; } else if (input && cJSON_IsString(input) && input->valuestring && validate_skill_d_tag_local(input->valuestring)) { d_tag_value = input->valuestring; } if (!d_tag_value) { cJSON_Delete(args); return json_error_local("skill_get requires valid d tag"); } char* events_json = nostr_handler_get_self_skill_events_json(); if (!events_json) { cJSON_Delete(args); return json_error_local("skill_get cache unavailable"); } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_get cache returned invalid JSON"); } cJSON* target = NULL; long long best_created_at = -1; 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* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue; int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124) continue; cJSON* d_val = find_tag_value_string_local(tags, "d"); if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue; if (strcmp(d_val->valuestring, d_tag_value) != 0) continue; cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0; if (!target || created > best_created_at) { target = ev; best_created_at = created; } } if (!target) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_get could not find skill with that d"); } cJSON* kind = cJSON_GetObjectItemCaseSensitive(target, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(target, "pubkey"); cJSON* id = cJSON_GetObjectItemCaseSensitive(target, "id"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(target, "created_at"); cJSON* content = cJSON_GetObjectItemCaseSensitive(target, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(target, "tags"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_get found malformed skill event"); } int kind_val = (int)kind->valuedouble; cJSON* scope = find_tag_value_string_local(tags, "scope"); cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(args); cJSON_Delete(events); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "d_tag", d_tag_value); cJSON_AddNumberToObject(out, "kind", kind_val); cJSON_AddStringToObject(out, "scope", (scope && cJSON_IsString(scope) && scope->valuestring) ? scope->valuestring : (kind_val == 31124 ? "private" : "public")); cJSON_AddStringToObject(out, "owner", skill_owner_label_local(ctx->cfg, pubkey->valuestring)); if (id && cJSON_IsString(id) && id->valuestring) { cJSON_AddStringToObject(out, "id", id->valuestring); } if (created_at && cJSON_IsNumber(created_at)) { char ts_buf[32]; snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble); cJSON* ts_raw = cJSON_CreateRaw(ts_buf); if (ts_raw) { cJSON_AddItemToObject(out, "created_at", ts_raw); } } cJSON_AddStringToObject(out, "content", content->valuestring); cJSON* tags_dup = cJSON_Duplicate(tags, 1); if (tags_dup) { cJSON_AddItemToObject(out, "tags", tags_dup); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(args); cJSON_Delete(events); return json; } char* execute_skill_view(tools_context_t* ctx, const char* args_json) { return execute_skill_get(ctx, args_json); } char* execute_skill_set(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* skill = cJSON_GetObjectItemCaseSensitive(args, "skill"); cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input"); cJSON* parsed_input = NULL; cJSON* src = NULL; if (skill && cJSON_IsObject(skill)) { src = skill; } else if (input && cJSON_IsString(input) && input->valuestring && input->valuestring[0] != '\0') { parsed_input = cJSON_Parse(input->valuestring); if (parsed_input && cJSON_IsObject(parsed_input)) { src = parsed_input; } } else if (cJSON_IsObject(args)) { src = args; } if (!src || !cJSON_IsObject(src)) { cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set requires a skill object"); } cJSON* d = cJSON_GetObjectItemCaseSensitive(src, "d"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(src, "kind"); cJSON* content = cJSON_GetObjectItemCaseSensitive(src, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(src, "tags"); cJSON* scope = cJSON_GetObjectItemCaseSensitive(src, "scope"); if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag_local(d->valuestring)) { cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set requires valid d"); } if (!content || !cJSON_IsString(content) || !content->valuestring) { cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set requires string content"); } if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set requires tags array"); } int out_kind = 0; if (kind && cJSON_IsNumber(kind)) { out_kind = (int)kind->valuedouble; } else if (scope && cJSON_IsString(scope) && scope->valuestring) { out_kind = (strcmp(scope->valuestring, "private") == 0) ? 31124 : 31123; } else { cJSON* scope_tag = find_tag_value_string_local(tags, "scope"); out_kind = (scope_tag && cJSON_IsString(scope_tag) && scope_tag->valuestring && strcmp(scope_tag->valuestring, "private") == 0) ? 31124 : 31123; } if (out_kind != 31123 && out_kind != 31124) { cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set kind must be 31123 or 31124"); } cJSON* tags_out = cJSON_Duplicate(tags, 1); if (!tags_out || !cJSON_IsArray(tags_out)) { cJSON_Delete(tags_out); cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set failed to duplicate tags"); } if (upsert_string_tag_local(tags_out, "d", d->valuestring) != 0 || upsert_string_tag_local(tags_out, "app", "didactyl") != 0 || upsert_string_tag_local(tags_out, "scope", out_kind == 31124 ? "private" : "public") != 0) { cJSON_Delete(tags_out); cJSON_Delete(parsed_input); cJSON_Delete(args); return json_error_local("skill_set failed to normalize required tags"); } nostr_publish_result_t publish_result; memset(&publish_result, 0, sizeof(publish_result)); int rc = nostr_handler_publish_kind_event(out_kind, content->valuestring, tags_out, &publish_result); int trigger_registered = 0; if (rc == 0 && ctx->trigger_manager) { cJSON* trigger = find_tag_value_string_local(tags_out, "trigger"); cJSON* filter = find_tag_value_string_local(tags_out, "filter"); cJSON* enabled = find_tag_value_string_local(tags_out, "enabled"); cJSON* llm = find_tag_value_string_local(tags_out, "llm"); cJSON* tools = find_tag_value_string_local(tags_out, "tools"); cJSON* max_tokens = find_tag_value_string_local(tags_out, "max_tokens"); cJSON* temperature = find_tag_value_string_local(tags_out, "temperature"); cJSON* seed = find_tag_value_string_local(tags_out, "seed"); int enabled_i = !(enabled && cJSON_IsString(enabled) && enabled->valuestring && strcmp(enabled->valuestring, "false") == 0); const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL; if (trigger_s && filter_s && enabled_i) { int max_tokens_set = (max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring && max_tokens->valuestring[0] != '\0') ? 1 : 0; int max_tokens_v = max_tokens_set ? atoi(max_tokens->valuestring) : 0; int temperature_set = (temperature && cJSON_IsString(temperature) && temperature->valuestring && temperature->valuestring[0] != '\0') ? 1 : 0; double temperature_v = temperature_set ? atof(temperature->valuestring) : 0.0; int seed_set = (seed && cJSON_IsString(seed) && seed->valuestring && seed->valuestring[0] != '\0') ? 1 : 0; int seed_v = seed_set ? atoi(seed->valuestring) : 0; if (trigger_manager_update(ctx->trigger_manager, d->valuestring, content->valuestring, filter_s, TRIGGER_ACTION_LLM, trigger_s, enabled_i, (llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL, (tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL, max_tokens_set, max_tokens_v, temperature_set, temperature_v, seed_set, seed_v) == 0 || trigger_manager_add(ctx->trigger_manager, d->valuestring, content->valuestring, filter_s, TRIGGER_ACTION_LLM, trigger_s, enabled_i, (llm && cJSON_IsString(llm) && llm->valuestring) ? llm->valuestring : NULL, (tools && cJSON_IsString(tools) && tools->valuestring) ? tools->valuestring : NULL, max_tokens_set, max_tokens_v, temperature_set, temperature_v, seed_set, seed_v) == 0) { trigger_registered = 1; } } else { (void)trigger_manager_remove(ctx->trigger_manager, d->valuestring); } } cJSON_Delete(tags_out); if (rc != 0) { cJSON_Delete(parsed_input); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json_error_local("skill_set failed to publish skill event"); } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(parsed_input); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return NULL; } cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0); cJSON_AddStringToObject(out, "d_tag", d->valuestring); cJSON_AddNumberToObject(out, "kind", out_kind); cJSON_AddStringToObject(out, "scope", out_kind == 31124 ? "private" : "public"); cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id); cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0); if (publish_result.naddr_uri[0] != '\0') { cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(parsed_input); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json; } char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind"); if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag_local(d_tag->valuestring)) { cJSON_Delete(args); return json_error_local("skill_adopt requires pubkey (hex) and valid d tag"); } int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123; if (kind_val != 31123 && kind_val != 31124) { cJSON_Delete(args); return json_error_local("skill_adopt kind must be 31123 or 31124"); } char addr[256]; snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey->valuestring, d_tag->valuestring); cJSON* tags = NULL; char* content = NULL; if (fetch_adoption_list_tags_local(ctx, &tags, &content) != 0) { cJSON_Delete(args); return json_error_local("skill_adopt failed to load adoption list"); } cJSON* tuple = cJSON_CreateArray(); if (!tuple) { cJSON_Delete(tags); free(content); cJSON_Delete(args); return json_error_local("allocation failure"); } cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); int already_adopted = tags_contains_tuple_local(tags, tuple); nostr_publish_result_t publish_result; memset(&publish_result, 0, sizeof(publish_result)); if (!already_adopted) { cJSON* dup = cJSON_Duplicate(tuple, 1); if (!dup) { cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); return json_error_local("failed to duplicate adoption tuple"); } cJSON_AddItemToArray(tags, dup); if (publish_adoption_list_local(content, tags, &publish_result) != 0) { cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); return json_error_local("skill_adopt failed to publish adoption list"); } } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "adopted_address", addr); cJSON_AddBoolToObject(out, "already_adopted", already_adopted ? 1 : 0); if (publish_result.event_id[0] != '\0') { cJSON_AddStringToObject(out, "event_id", publish_result.event_id); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json; } char* execute_skill_remove(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind"); const char* pubkey_str = ctx->cfg->keys.public_key_hex; if (pubkey) { if (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U)) { cJSON_Delete(args); return json_error_local("skill_remove pubkey must be 64-char hex when provided"); } pubkey_str = pubkey->valuestring; } if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag_local(d_tag->valuestring)) { cJSON_Delete(args); return json_error_local("skill_remove requires valid d tag"); } int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123; if (kind_val != 31123 && kind_val != 31124) { cJSON_Delete(args); return json_error_local("skill_remove kind must be 31123 or 31124"); } char addr[256]; snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey_str, d_tag->valuestring); cJSON* tags = NULL; char* content = NULL; if (fetch_adoption_list_tags_local(ctx, &tags, &content) != 0) { cJSON_Delete(args); return json_error_local("skill_remove failed to load adoption list"); } cJSON* tuple = cJSON_CreateArray(); if (!tuple) { cJSON_Delete(tags); free(content); cJSON_Delete(args); return json_error_local("allocation failure"); } cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); int removed = remove_matching_tag_tuples_local(tags, tuple); nostr_publish_result_t publish_result; memset(&publish_result, 0, sizeof(publish_result)); if (removed > 0) { if (publish_adoption_list_local(content, tags, &publish_result) != 0) { cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); return json_error_local("skill_remove failed to publish adoption list"); } } if (ctx->trigger_manager) { (void)trigger_manager_remove(ctx->trigger_manager, d_tag->valuestring); } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "removed_address", addr); cJSON_AddNumberToObject(out, "removed_count", removed); cJSON_AddBoolToObject(out, "already_absent", removed == 0 ? 1 : 0); if (publish_result.event_id[0] != '\0') { cJSON_AddStringToObject(out, "event_id", publish_result.event_id); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(tuple); cJSON_Delete(tags); free(content); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json; } char* execute_skill_edit(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content"); cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope"); cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description"); cJSON* trigger = cJSON_GetObjectItemCaseSensitive(args, "trigger"); cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter"); cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action"); cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled"); cJSON* llm = cJSON_GetObjectItemCaseSensitive(args, "llm"); cJSON* tools = cJSON_GetObjectItemCaseSensitive(args, "tools"); cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature"); cJSON* seed = cJSON_GetObjectItemCaseSensitive(args, "seed"); if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag_local(d->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit requires valid d tag"); } if (content && (!cJSON_IsString(content) || !content->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit content must be string when provided"); } if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit scope must be string when provided"); } if (description && (!cJSON_IsString(description) || !description->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit description must be string when provided"); } if (trigger && (!cJSON_IsString(trigger) || !trigger->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit trigger must be string when provided"); } if (filter && (!cJSON_IsString(filter) || !filter->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit filter must be string when provided"); } if (action && (!cJSON_IsString(action) || !action->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit action must be string when provided"); } if (enabled && !cJSON_IsBool(enabled)) { cJSON_Delete(args); return json_error_local("skill_edit enabled must be boolean when provided"); } if (llm && (!cJSON_IsString(llm) || !llm->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit llm must be string when provided"); } if (tools && (!cJSON_IsString(tools) || !tools->valuestring)) { cJSON_Delete(args); return json_error_local("skill_edit tools must be string when provided"); } if (max_tokens && !cJSON_IsNumber(max_tokens)) { cJSON_Delete(args); return json_error_local("skill_edit max_tokens must be number when provided"); } if (temperature && !cJSON_IsNumber(temperature)) { cJSON_Delete(args); return json_error_local("skill_edit temperature must be number when provided"); } if (seed && !cJSON_IsNumber(seed)) { cJSON_Delete(args); return json_error_local("skill_edit seed must be number when provided"); } char* events_json = nostr_handler_get_self_skill_events_json(); if (!events_json) { cJSON_Delete(args); return json_error_local("skill_edit cache unavailable"); } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_edit cache returned invalid JSON"); } cJSON* target = NULL; long long best_created_at = -1; 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* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags"); if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue; int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124) continue; cJSON* d_val = find_tag_value_string_local(tags, "d"); if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue; if (strcmp(d_val->valuestring, d->valuestring) != 0) continue; cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0; if (!target || created > best_created_at) { target = ev; best_created_at = created; } } if (!target) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_edit could not find self skill with that d"); } cJSON* target_kind = cJSON_GetObjectItemCaseSensitive(target, "kind"); cJSON* target_content = cJSON_GetObjectItemCaseSensitive(target, "content"); cJSON* target_tags = cJSON_GetObjectItemCaseSensitive(target, "tags"); if (!target_kind || !cJSON_IsNumber(target_kind) || !target_content || !cJSON_IsString(target_content) || !target_content->valuestring || !target_tags || !cJSON_IsArray(target_tags)) { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_edit found malformed skill event"); } int out_kind = (int)target_kind->valuedouble; if (scope && scope->valuestring) { if (strcmp(scope->valuestring, "public") == 0) { out_kind = 31123; } else if (strcmp(scope->valuestring, "private") == 0) { out_kind = 31124; } else { cJSON_Delete(args); cJSON_Delete(events); return json_error_local("skill_edit scope must be public or private"); } } const char* out_content = (content && content->valuestring) ? content->valuestring : target_content->valuestring; cJSON* tags_out = cJSON_Duplicate(target_tags, 1); if (!tags_out || !cJSON_IsArray(tags_out)) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to duplicate existing tags"); } if (upsert_string_tag_local(tags_out, "d", d->valuestring) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set d tag"); } if (upsert_string_tag_local(tags_out, "app", "didactyl") != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set app tag"); } if (upsert_string_tag_local(tags_out, "scope", out_kind == 31123 ? "public" : "private") != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set scope tag"); } if (description) { if (description->valuestring[0] == '\0') { remove_tag_key_all_local(tags_out, "description"); } else if (upsert_string_tag_local(tags_out, "description", description->valuestring) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set description tag"); } } cJSON* existing_trigger = find_tag_value_string_local(tags_out, "trigger"); cJSON* existing_filter = find_tag_value_string_local(tags_out, "filter"); cJSON* existing_action = find_tag_value_string_local(tags_out, "action"); cJSON* existing_enabled = find_tag_value_string_local(tags_out, "enabled"); const char* merged_trigger = (trigger && trigger->valuestring) ? trigger->valuestring : (existing_trigger && cJSON_IsString(existing_trigger) ? existing_trigger->valuestring : NULL); const char* merged_filter = (filter && filter->valuestring) ? filter->valuestring : (existing_filter && cJSON_IsString(existing_filter) ? existing_filter->valuestring : NULL); const char* merged_action = (action && action->valuestring) ? action->valuestring : (existing_action && cJSON_IsString(existing_action) && existing_action->valuestring[0] != '\0' ? existing_action->valuestring : "llm"); int merged_enabled = (enabled && cJSON_IsBool(enabled)) ? (cJSON_IsTrue(enabled) ? 1 : 0) : (existing_enabled && cJSON_IsString(existing_enabled) && existing_enabled->valuestring && strcmp(existing_enabled->valuestring, "false") == 0) ? 0 : 1; int trigger_patch_requested = (trigger || filter || action || enabled || llm || tools || max_tokens || temperature || seed) ? 1 : 0; if (trigger_patch_requested || (merged_trigger || merged_filter)) { if ((merged_trigger && !merged_filter) || (!merged_trigger && merged_filter)) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit trigger and filter must both be set together"); } remove_tag_key_all_local(tags_out, "trigger"); remove_tag_key_all_local(tags_out, "filter"); remove_tag_key_all_local(tags_out, "action"); remove_tag_key_all_local(tags_out, "enabled"); if (merged_trigger && merged_filter) { if (strcmp(merged_trigger, "nostr-subscription") != 0 && strcmp(merged_trigger, "webhook") != 0 && strcmp(merged_trigger, "cron") != 0 && strcmp(merged_trigger, "chain") != 0 && strcmp(merged_trigger, "dm") != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit trigger must be one of: nostr-subscription, webhook, cron, chain, dm"); } if (add_string_tag_local(tags_out, "trigger", merged_trigger) != 0 || add_string_tag_local(tags_out, "filter", merged_filter) != 0 || add_string_tag_local(tags_out, "action", merged_action) != 0 || add_string_tag_local(tags_out, "enabled", merged_enabled ? "true" : "false") != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set trigger tags"); } if (llm && llm->valuestring && llm->valuestring[0] != '\0' && add_string_tag_local(tags_out, "llm", llm->valuestring) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set llm tag"); } if (tools && tools->valuestring && tools->valuestring[0] != '\0' && add_string_tag_local(tags_out, "tools", tools->valuestring) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set tools tag"); } if (max_tokens) { char buf[32]; snprintf(buf, sizeof(buf), "%d", (int)max_tokens->valuedouble); if (add_string_tag_local(tags_out, "max_tokens", buf) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set max_tokens tag"); } } if (temperature) { char buf[32]; snprintf(buf, sizeof(buf), "%g", temperature->valuedouble); if (add_string_tag_local(tags_out, "temperature", buf) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set temperature tag"); } } if (seed) { char buf[32]; snprintf(buf, sizeof(buf), "%d", (int)seed->valuedouble); if (add_string_tag_local(tags_out, "seed", buf) != 0) { cJSON_Delete(args); cJSON_Delete(events); cJSON_Delete(tags_out); return json_error_local("skill_edit failed to set seed tag"); } } } } nostr_publish_result_t publish_result; memset(&publish_result, 0, sizeof(publish_result)); int rc = nostr_handler_publish_kind_event(out_kind, out_content, tags_out, &publish_result); cJSON_Delete(tags_out); cJSON_Delete(events); if (rc != 0) { cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json_error_local("skill_edit failed to publish updated skill"); } int trigger_registered = 0; if (ctx->trigger_manager) { if (merged_trigger && merged_filter && merged_enabled) { if (strcmp(merged_action, "template") == 0) { DEBUG_WARN("[didactyl] skill_edit trigger action template is deprecated; forcing llm for d_tag=%s", d->valuestring); } if (trigger_manager_update(ctx->trigger_manager, d->valuestring, out_content, merged_filter, TRIGGER_ACTION_LLM, merged_trigger, merged_enabled, (llm && llm->valuestring) ? llm->valuestring : NULL, (tools && tools->valuestring) ? tools->valuestring : NULL, max_tokens ? 1 : 0, max_tokens ? (int)max_tokens->valuedouble : 0, temperature ? 1 : 0, temperature ? temperature->valuedouble : 0.0, seed ? 1 : 0, seed ? (int)seed->valuedouble : 0) == 0 || trigger_manager_add(ctx->trigger_manager, d->valuestring, out_content, merged_filter, TRIGGER_ACTION_LLM, merged_trigger, merged_enabled, (llm && llm->valuestring) ? llm->valuestring : NULL, (tools && tools->valuestring) ? tools->valuestring : NULL, max_tokens ? 1 : 0, max_tokens ? (int)max_tokens->valuedouble : 0, temperature ? 1 : 0, temperature ? temperature->valuedouble : 0.0, seed ? 1 : 0, seed ? (int)seed->valuedouble : 0) == 0) { trigger_registered = 1; } } else { (void)trigger_manager_remove(ctx->trigger_manager, d->valuestring); } } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return NULL; } cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0); cJSON_AddStringToObject(out, "d_tag", d->valuestring); cJSON_AddNumberToObject(out, "kind", out_kind); cJSON_AddStringToObject(out, "scope", out_kind == 31123 ? "public" : "private"); cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id); cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0); if (publish_result.naddr_uri[0] != '\0') { cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri); } char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(args); nostr_handler_publish_result_free(&publish_result); return json; } char* execute_skill_run(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d_tag"); cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); cJSON* run_args = cJSON_GetObjectItemCaseSensitive(args, "args"); cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input"); cJSON* sandbox = cJSON_GetObjectItemCaseSensitive(args, "sandbox"); const char* d_tag_value = NULL; if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) { d_tag_value = d_tag->valuestring; } else if (d && cJSON_IsString(d) && d->valuestring) { d_tag_value = d->valuestring; } if (!d_tag_value || !validate_skill_d_tag_local(d_tag_value)) { cJSON_Delete(args); return json_error_local("skill_run requires valid d_tag"); } const char* pubkey_value = NULL; if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0') { pubkey_value = pubkey->valuestring; } const char* user_args = ""; if (run_args && cJSON_IsString(run_args) && run_args->valuestring) { user_args = run_args->valuestring; } else if (input && cJSON_IsString(input) && input->valuestring) { user_args = input->valuestring; } int depth_incremented = 0; pthread_mutex_lock(&g_skill_run_depth_mutex_local); if (g_skill_run_depth_local >= 3) { pthread_mutex_unlock(&g_skill_run_depth_mutex_local); cJSON_Delete(args); return json_error_local("skill_run max recursion depth exceeded"); } g_skill_run_depth_local++; depth_incremented = 1; pthread_mutex_unlock(&g_skill_run_depth_mutex_local); skill_fetch_result_local_t skill; memset(&skill, 0, sizeof(skill)); char* fetch_error = NULL; char* tools_json = NULL; char* final_result = NULL; cJSON* messages = NULL; char* response_json = NULL; int turns_run = 0; llm_config_t old_llm_cfg; int llm_overridden = 0; if (fetch_skill_by_identifier_local(ctx, d_tag_value, pubkey_value, &skill, &fetch_error) != 0) { response_json = json_error_local(fetch_error ? fetch_error : "skill lookup failed"); goto cleanup; } int sandbox_enabled = (!skill.is_own) ? 1 : 0; if (sandbox && cJSON_IsBool(sandbox)) { sandbox_enabled = cJSON_IsTrue(sandbox) ? 1 : 0; } cJSON* llm_tag = find_tag_value_string_local(skill.tags, "llm"); if (llm_tag && cJSON_IsString(llm_tag) && llm_tag->valuestring && llm_tag->valuestring[0] != '\0') { if (llm_get_config(&old_llm_cfg) == 0) { llm_config_t next_llm_cfg = old_llm_cfg; apply_llm_spec_local_to_config(llm_tag->valuestring, &next_llm_cfg); if (llm_set_config(&next_llm_cfg) == 0) { llm_overridden = 1; } } } const char* source = skill.is_own ? "own" : "external"; size_t sys_cap = strlen(skill.d_tag ? skill.d_tag : d_tag_value) + strlen(skill.content ? skill.content : "") + strlen(source) + 320U; char* system_prompt = (char*)malloc(sys_cap); if (!system_prompt) { response_json = json_error_local("allocation failure"); goto cleanup; } snprintf(system_prompt, sys_cap, "Skill execution context:\n" "- You are executing a specific skill on demand.\n" "- Follow the skill instructions below precisely.\n" "- Return a concise, actionable result.\n\n" "Skill d_tag: %s\n" "Skill source: %s\n\n" "Skill instructions:\n%s", skill.d_tag ? skill.d_tag : d_tag_value, source, skill.content ? skill.content : ""); tools_json = sandbox_enabled ? build_sandboxed_tools_json_local(ctx) : tools_build_openai_schema_json(ctx); if (!tools_json) { free(system_prompt); response_json = json_error_local("skill_run failed to build tool schema"); goto cleanup; } messages = cJSON_CreateArray(); if (!messages || append_simple_message_local(messages, "system", system_prompt) != 0 || append_simple_message_local(messages, "user", user_args) != 0) { free(system_prompt); response_json = json_error_local("skill_run failed to initialize messages"); goto cleanup; } free(system_prompt); int max_turns = ctx->cfg->tools.trigger_max_turns > 0 ? ctx->cfg->tools.trigger_max_turns : (ctx->cfg->tools.max_turns > 0 ? ctx->cfg->tools.max_turns : 8); for (int turn = 0; turn < max_turns; turn++) { turns_run = turn + 1; char* messages_json = cJSON_PrintUnformatted(messages); if (!messages_json) { response_json = json_error_local("skill_run failed to serialize messages"); goto cleanup; } llm_response_t resp; int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp); free(messages_json); if (rc != 0) { response_json = json_error_local("skill_run LLM request failed"); goto cleanup; } if (resp.tool_call_count <= 0) { final_result = strdup(resp.content ? resp.content : ""); llm_response_free(&resp); if (!final_result) { response_json = json_error_local("allocation failure"); goto cleanup; } break; } if (append_assistant_tool_calls_message_local(messages, &resp) != 0) { llm_response_free(&resp); response_json = json_error_local("skill_run failed to append assistant tool calls"); goto cleanup; } for (int i = 0; i < resp.tool_call_count; i++) { llm_tool_call_t* tc = &resp.tool_calls[i]; char* tool_result = NULL; if (sandbox_enabled && !is_tool_sandbox_safe_local(tc->name)) { tool_result = strdup("{\"success\":false,\"error\":\"tool blocked by skill_run sandbox\"}"); } else { tool_result = tools_execute(ctx, tc->name, tc->arguments_json); } if (!tool_result) { tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}"); } if (append_tool_result_message_local(messages, tc->id ? tc->id : "", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) { free(tool_result); llm_response_free(&resp); response_json = json_error_local("skill_run failed to append tool result"); goto cleanup; } if (maybe_append_loaded_skill_instructions_local(messages, tc->name, tool_result) != 0) { free(tool_result); llm_response_free(&resp); response_json = json_error_local("skill_run failed to apply loaded skill instructions"); goto cleanup; } free(tool_result); } llm_response_free(&resp); } if (!final_result) { final_result = strdup(""); if (!final_result) { response_json = json_error_local("allocation failure"); goto cleanup; } } cJSON* out = cJSON_CreateObject(); if (!out) { response_json = NULL; goto cleanup; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "d_tag", skill.d_tag ? skill.d_tag : d_tag_value); cJSON_AddStringToObject(out, "source", source); cJSON_AddBoolToObject(out, "sandboxed", sandbox_enabled ? 1 : 0); cJSON_AddNumberToObject(out, "turns", turns_run); cJSON_AddStringToObject(out, "result", final_result); response_json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cleanup: free(final_result); cJSON_Delete(messages); free(tools_json); free(fetch_error); free_skill_fetch_result_local(&skill); cJSON_Delete(args); if (llm_overridden) { (void)llm_set_config(&old_llm_cfg); } if (depth_incremented) { pthread_mutex_lock(&g_skill_run_depth_mutex_local); g_skill_run_depth_local--; if (g_skill_run_depth_local < 0) g_skill_run_depth_local = 0; pthread_mutex_unlock(&g_skill_run_depth_mutex_local); } return response_json ? response_json : json_error_local("skill_run failed"); } char* execute_skill_load(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d_tag"); cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); const char* d_tag_value = NULL; if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) { d_tag_value = d_tag->valuestring; } else if (d && cJSON_IsString(d) && d->valuestring) { d_tag_value = d->valuestring; } if (!d_tag_value || !validate_skill_d_tag_local(d_tag_value)) { cJSON_Delete(args); return json_error_local("skill_load requires valid d_tag"); } const char* pubkey_value = NULL; if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0') { pubkey_value = pubkey->valuestring; } skill_fetch_result_local_t skill; memset(&skill, 0, sizeof(skill)); char* fetch_error = NULL; if (fetch_skill_by_identifier_local(ctx, d_tag_value, pubkey_value, &skill, &fetch_error) != 0) { char* err = json_error_local(fetch_error ? fetch_error : "skill lookup failed"); free(fetch_error); cJSON_Delete(args); return err; } const char* content = skill.content ? skill.content : ""; size_t instructions_cap = strlen(content) + 256U; char* instructions = (char*)malloc(instructions_cap); if (!instructions) { free_skill_fetch_result_local(&skill); cJSON_Delete(args); return json_error_local("allocation failure"); } snprintf(instructions, instructions_cap, "SKILL INSTRUCTIONS — Follow these for the remainder of this conversation:\n\n%s\n\nEND SKILL INSTRUCTIONS", content); cJSON* out = cJSON_CreateObject(); if (!out) { free(instructions); free_skill_fetch_result_local(&skill); cJSON_Delete(args); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "type", "skill_instructions"); cJSON_AddStringToObject(out, "d_tag", skill.d_tag ? skill.d_tag : d_tag_value); cJSON_AddStringToObject(out, "source", skill.is_own ? "own" : "external"); cJSON_AddStringToObject(out, "instructions", instructions); cJSON_AddStringToObject(out, "note", "The above instructions should be followed for subsequent reasoning in this conversation."); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); free(instructions); free_skill_fetch_result_local(&skill); free(fetch_error); cJSON_Delete(args); return json; } char* execute_skill_refresh(tools_context_t* ctx, const char* args_json) { (void)args_json; if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); agent_invalidate_adopted_skills_cache(); cJSON* out = cJSON_CreateObject(); if (!out) return NULL; cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "message", "adopted skills cache invalidated; next context build will reload skills"); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } char* execute_skill_search(const char* args_json) { cJSON* args = parse_args_local(args_json); if (!args) return json_error_local("invalid arguments JSON"); cJSON* query = cJSON_GetObjectItemCaseSensitive(args, "query"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); cJSON* popular = cJSON_GetObjectItemCaseSensitive(args, "popular"); if (query && (!cJSON_IsString(query) || !query->valuestring)) { cJSON_Delete(args); return json_error_local("skill_search query must be string when provided"); } if (pubkey && (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U))) { cJSON_Delete(args); return json_error_local("skill_search pubkey must be 64-char hex string when provided"); } int do_popular = (popular && cJSON_IsBool(popular) && cJSON_IsTrue(popular)) ? 1 : 0; const char* query_str = (query && query->valuestring) ? query->valuestring : NULL; if (do_popular) { cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); if (!filter || !kinds) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(args); return json_error_local("failed to create skill_search popularity filter"); } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddNumberToObject(filter, "limit", 300); char* events_json = nostr_handler_query_json(filter, 8000); cJSON_Delete(filter); cJSON_Delete(args); if (!events_json) return json_error_local("skill_search popularity query failed"); cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(events); return json_error_local("skill_search popularity returned invalid JSON"); } typedef struct { char* addr; int count; } skill_count_t; skill_count_t* counts = NULL; int count_len = 0; int event_n = cJSON_GetArraySize(events); for (int i = 0; i < event_n; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; if (!tags || !cJSON_IsArray(tags)) continue; int tn = cJSON_GetArraySize(tags); for (int t = 0; t < tn; t++) { cJSON* tuple = cJSON_GetArrayItem(tags, t); if (!tuple || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) < 2) continue; cJSON* k = cJSON_GetArrayItem(tuple, 0); cJSON* v = cJSON_GetArrayItem(tuple, 1); if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue; if (strcmp(k->valuestring, "a") != 0) continue; if (strncmp(v->valuestring, "31123:", 6) != 0) continue; if (pubkey && !strstr(v->valuestring, pubkey->valuestring)) continue; if (query_str && !ci_contains_local(v->valuestring, query_str)) continue; int found = -1; for (int j = 0; j < count_len; j++) { if (strcmp(counts[j].addr, v->valuestring) == 0) { found = j; break; } } if (found >= 0) { counts[found].count++; } else { skill_count_t* bigger = (skill_count_t*)realloc(counts, (size_t)(count_len + 1) * sizeof(skill_count_t)); if (!bigger) { for (int j = 0; j < count_len; j++) free(counts[j].addr); free(counts); cJSON_Delete(events); return json_error_local("allocation failure"); } counts = bigger; counts[count_len].addr = strdup(v->valuestring); if (!counts[count_len].addr) { for (int j = 0; j < count_len; j++) free(counts[j].addr); free(counts); cJSON_Delete(events); return json_error_local("allocation failure"); } counts[count_len].count = 1; count_len++; } } } for (int i = 0; i < count_len; i++) { for (int j = i + 1; j < count_len; j++) { if (counts[j].count > counts[i].count) { skill_count_t tmp = counts[i]; counts[i] = counts[j]; counts[j] = tmp; } } } cJSON* out = cJSON_CreateObject(); cJSON* items = cJSON_CreateArray(); if (!out || !items) { cJSON_Delete(out); cJSON_Delete(items); for (int j = 0; j < count_len; j++) free(counts[j].addr); free(counts); cJSON_Delete(events); return NULL; } for (int i = 0; i < count_len; i++) { cJSON* it = cJSON_CreateObject(); if (!it) continue; cJSON_AddStringToObject(it, "address", counts[i].addr); cJSON_AddNumberToObject(it, "adoption_count", counts[i].count); cJSON_AddItemToArray(items, it); } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddBoolToObject(out, "popular", 1); cJSON_AddItemToObject(out, "results", items); cJSON_AddNumberToObject(out, "count", count_len); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); for (int j = 0; j < count_len; j++) free(counts[j].addr); free(counts); cJSON_Delete(events); return json; } cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); if (!filter || !kinds) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(args); return json_error_local("failed to create skill_search filter"); } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToObject(filter, "kinds", kinds); if (pubkey) { cJSON* authors = cJSON_CreateArray(); if (!authors) { cJSON_Delete(filter); cJSON_Delete(args); return json_error_local("failed to create skill_search authors"); } cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey->valuestring)); cJSON_AddItemToObject(filter, "authors", authors); } cJSON_AddNumberToObject(filter, "limit", 200); char* events_json = nostr_handler_query_json(filter, 8000); cJSON_Delete(filter); cJSON_Delete(args); if (!events_json) return json_error_local("skill_search query failed"); cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(events); return json_error_local("skill_search returned invalid JSON"); } cJSON* out = cJSON_CreateObject(); cJSON* results = cJSON_CreateArray(); if (!out || !results) { cJSON_Delete(out); cJSON_Delete(results); cJSON_Delete(events); return NULL; } int n = cJSON_GetArraySize(events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); cJSON* summary = extract_skill_summary_local(ev); if (!summary) continue; if (query_str && query_str[0] != '\0') { cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(summary, "d_tag"); cJSON* desc = cJSON_GetObjectItemCaseSensitive(summary, "description"); cJSON* preview = cJSON_GetObjectItemCaseSensitive(summary, "content_preview"); int match = 0; if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && ci_contains_local(d_tag->valuestring, query_str)) match = 1; if (!match && desc && cJSON_IsString(desc) && desc->valuestring && ci_contains_local(desc->valuestring, query_str)) match = 1; if (!match && preview && cJSON_IsString(preview) && preview->valuestring && ci_contains_local(preview->valuestring, query_str)) match = 1; if (!match) { cJSON_Delete(summary); continue; } } cJSON_AddItemToArray(results, summary); } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddBoolToObject(out, "popular", 0); cJSON_AddItemToObject(out, "results", results); cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(results)); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(events); return json; }