2678 lines
104 KiB
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;
|
|
}
|