Files
didactyl/src/tools/tool_skill.c

2678 lines
104 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}