From a798f2c345e8bc5a38eb84af2887f8bf8876dcd4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Mar 2026 17:45:57 -0400 Subject: [PATCH] v0.0.19 - Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend --- README.md | 4 +- plans/new_nostr_tools.md | 247 +++++++++++++++++++++ src/main.h | 4 +- src/nostr_handler.c | 88 ++++++++ src/nostr_handler.h | 1 + src/tools.c | 469 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 809 insertions(+), 4 deletions(-) create mode 100644 plans/new_nostr_tools.md diff --git a/README.md b/README.md index 93d1f17..0cf5687 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.0.18 +## Current Status — v0.0.19 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.18 — Harden tool argument JSON parsing and set fixed README publish image tag +> Last release update: v0.0.19 — Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/plans/new_nostr_tools.md b/plans/new_nostr_tools.md new file mode 100644 index 0000000..cb53da2 --- /dev/null +++ b/plans/new_nostr_tools.md @@ -0,0 +1,247 @@ +# Implementation Plan: 4 New Nostr Tools + +## Overview + +Add four new tools to Didactyl's tool system. Each tool follows the same pattern established by existing tools in `src/tools.c`: + +1. Register OpenAI function schema in `tools_build_openai_schema_json()` +2. Implement `execute_*()` function +3. Wire dispatch in `tools_execute()` + +All four tools reuse existing infrastructure — no new `nostr_handler` APIs needed for the first three, and only a thin new wrapper for the fourth. + +--- + +## Tool 1: `nostr_delete` — Event Deletion Request (NIP-09) + +### Purpose +Let the agent retract or request deletion of events it previously published. Essential for self-correction. + +### OpenAI Schema +```json +{ + "name": "nostr_delete", + "description": "Request deletion of one or more previously published events (NIP-09 kind 5)", + "parameters": { + "type": "object", + "properties": { + "event_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Array of event ID hex strings to request deletion for" + }, + "kinds": { + "type": "array", + "items": { "type": "integer" }, + "description": "Array of kind numbers corresponding to the events being deleted" + }, + "reason": { + "type": "string", + "description": "Optional reason for the deletion request" + } + }, + "required": ["event_ids"] + } +} +``` + +### Implementation: `execute_nostr_delete()` + +``` +1. Parse args_json (with same hardened parsing as nostr_post) +2. Extract "event_ids" array (required) — validate each is a 64-char hex string +3. Extract "kinds" array (optional) — validate each is an integer +4. Extract "reason" string (optional) — use as content, default to empty string +5. Build tags array: + - For each event_id: add ["e", event_id] + - For each kind: add ["k", kind_as_string] +6. Call nostr_handler_publish_kind_event(5, reason, tags, &publish_result) +7. Return standard publish result JSON +``` + +### Files Modified +- `src/tools.c`: Add schema block in `tools_build_openai_schema_json()`, add `execute_nostr_delete()`, add dispatch case in `tools_execute()` + +--- + +## Tool 2: `nostr_react` — Reactions (NIP-25) + +### Purpose +Let the agent react to events — like, dislike, or emoji react. Gives the agent social presence. + +### OpenAI Schema +```json +{ + "name": "nostr_react", + "description": "React to a Nostr event with a like, dislike, or emoji (NIP-25 kind 7)", + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "description": "Hex event ID of the event to react to" + }, + "event_pubkey": { + "type": "string", + "description": "Hex pubkey of the event author" + }, + "event_kind": { + "type": "integer", + "description": "Kind number of the event being reacted to" + }, + "reaction": { + "type": "string", + "description": "Reaction content: + for like, - for dislike, or an emoji. Default: +" + } + }, + "required": ["event_id", "event_pubkey"] + } +} +``` + +### Implementation: `execute_nostr_react()` + +``` +1. Parse args_json +2. Extract "event_id" (required, 64-char hex) +3. Extract "event_pubkey" (required, 64-char hex) +4. Extract "event_kind" (optional integer) +5. Extract "reaction" (optional string, default "+") +6. Build tags array: + - ["e", event_id] + - ["p", event_pubkey] + - If event_kind provided: ["k", kind_as_string] +7. Call nostr_handler_publish_kind_event(7, reaction, tags, &publish_result) +8. Return standard publish result JSON +``` + +### Files Modified +- `src/tools.c`: Same three insertion points as above + +--- + +## Tool 3: `nostr_profile_get` — Profile Lookup (kind 0) + +### Purpose +Let the agent look up any user's profile metadata. Useful for WoT reasoning, greeting users by name, checking identity. + +### OpenAI Schema +```json +{ + "name": "nostr_profile_get", + "description": "Look up a Nostr user profile (kind 0 metadata) by pubkey", + "parameters": { + "type": "object", + "properties": { + "pubkey": { + "type": "string", + "description": "Hex public key of the user to look up" + } + }, + "required": ["pubkey"] + } +} +``` + +### Implementation: `execute_nostr_profile_get()` + +``` +1. Parse args_json +2. Extract "pubkey" (required, 64-char hex) +3. Build filter: + { + "kinds": [0], + "authors": [pubkey], + "limit": 1 + } +4. Call nostr_handler_query_json(filter, 8000) +5. Parse the returned events JSON +6. If events found: + - Extract the first event's content (which is a JSON string of profile fields) + - Parse that content JSON + - Return { success: true, pubkey: ..., profile: { name, display_name, about, picture, nip05, ... } } +7. If no events: return { success: true, pubkey: ..., profile: null } +``` + +### Files Modified +- `src/tools.c`: Same three insertion points + +--- + +## Tool 4: `nostr_relay_status` — Relay Health Dashboard + +### Purpose +Let the agent introspect its own relay connectivity. Useful for self-diagnostics and admin status reports. + +### OpenAI Schema +```json +{ + "name": "nostr_relay_status", + "description": "Get connection status and statistics for all connected relays", + "parameters": { + "type": "object", + "properties": {} + } +} +``` + +### Implementation + +This tool needs access to the relay pool, which is currently a `static` global in `nostr_handler.c`. Two options: + +**Option A (preferred):** Add a new function to `nostr_handler.h`: +```c +char* nostr_handler_relay_status_json(void); +``` +This function iterates the pool's relays using `nostr_relay_pool_list_relays()` and `nostr_relay_pool_get_relay_stats()`, builds a JSON array, and returns it. + +**Option B:** Expose the pool pointer — not recommended, breaks encapsulation. + +### `nostr_handler_relay_status_json()` in `nostr_handler.c`: + +``` +1. Call nostr_relay_pool_list_relays(g_pool, &urls, &statuses) +2. For each relay: + a. Get stats via nostr_relay_pool_get_relay_stats(g_pool, url) + b. Build JSON object with: + - url + - status (disconnected/connecting/connected/error) + - ping_latency_ms + - events_received + - events_published + - events_published_ok + - events_published_failed + - connection_uptime_start +3. Return JSON array as string +``` + +### `execute_nostr_relay_status()` in `tools.c`: + +``` +1. Parse args_json (accept empty/no args) +2. Call nostr_handler_relay_status_json() +3. Wrap in { success: true, relays: [...] } +4. Return +``` + +### Files Modified +- `src/nostr_handler.h`: Add `nostr_handler_relay_status_json()` declaration +- `src/nostr_handler.c`: Implement `nostr_handler_relay_status_json()` +- `src/tools.c`: Schema + execute + dispatch + +--- + +## Implementation Order + +1. **`nostr_delete`** — simplest, pure tag construction + existing publish +2. **`nostr_react`** — same pattern, slightly different tags +3. **`nostr_profile_get`** — uses existing query path, adds profile content parsing +4. **`nostr_relay_status`** — requires one new `nostr_handler` function + +All four schemas should be added to `tools_build_openai_schema_json()` in a single pass, and all four dispatch cases added to `tools_execute()` in a single pass, to minimize diff churn. + +## Build & Validate + +After all four tools are implemented: +1. Run `./build_static.sh` to verify compilation +2. Run `./increment_and_push.sh "Add nostr_delete, nostr_react, nostr_profile_get, nostr_relay_status tools"` diff --git a/src/main.h b/src/main.h index a520898..ed07215 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 0 -#define DIDACTYL_VERSION_PATCH 18 -#define DIDACTYL_VERSION "v0.0.18" +#define DIDACTYL_VERSION_PATCH 19 +#define DIDACTYL_VERSION "v0.0.19" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 8c4ab0f..580da56 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -1295,6 +1295,94 @@ int nostr_handler_connected_relay_count(void) { return connected; } +char* nostr_handler_relay_status_json(void) { + if (!g_pool) { + return NULL; + } + + char** relay_urls = NULL; + nostr_pool_relay_status_t* statuses = NULL; + int relay_count = nostr_relay_pool_list_relays(g_pool, &relay_urls, &statuses); + if (relay_count < 0) { + return NULL; + } + + cJSON* root = cJSON_CreateObject(); + cJSON* relays = cJSON_CreateArray(); + if (!root || !relays) { + cJSON_Delete(root); + cJSON_Delete(relays); + if (relay_urls) { + for (int i = 0; i < relay_count; i++) { + free(relay_urls[i]); + } + free(relay_urls); + } + free(statuses); + return NULL; + } + + int connected_count = 0; + for (int i = 0; i < relay_count; i++) { + cJSON* relay = cJSON_CreateObject(); + if (!relay) { + continue; + } + + const char* url = (relay_urls && relay_urls[i]) ? relay_urls[i] : ""; + nostr_pool_relay_status_t st = statuses ? statuses[i] : NOSTR_POOL_RELAY_DISCONNECTED; + if (st == NOSTR_POOL_RELAY_CONNECTED) { + connected_count++; + } + + const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, url); + + cJSON_AddStringToObject(relay, "url", url); + cJSON_AddStringToObject(relay, "status", relay_status_str(st)); + + if (stats) { + cJSON_AddNumberToObject(relay, "events_received", stats->events_received); + cJSON_AddNumberToObject(relay, "events_published", stats->events_published); + cJSON_AddNumberToObject(relay, "events_published_ok", stats->events_published_ok); + cJSON_AddNumberToObject(relay, "events_published_failed", stats->events_published_failed); + cJSON_AddNumberToObject(relay, "ping_latency_current", stats->ping_latency_current); + cJSON_AddNumberToObject(relay, "ping_latency_avg", stats->ping_latency_avg); + cJSON_AddNumberToObject(relay, "query_latency_avg", stats->query_latency_avg); + cJSON_AddNumberToObject(relay, "publish_latency_avg", stats->publish_latency_avg); + cJSON_AddNumberToObject(relay, "connection_uptime_start", (double)stats->connection_uptime_start); + cJSON_AddNumberToObject(relay, "last_event_time", (double)stats->last_event_time); + } + + const char* last_pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, url); + const char* last_conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, url); + if (last_pub_err && last_pub_err[0] != '\0') { + cJSON_AddStringToObject(relay, "last_publish_error", last_pub_err); + } + if (last_conn_err && last_conn_err[0] != '\0') { + cJSON_AddStringToObject(relay, "last_connection_error", last_conn_err); + } + + cJSON_AddItemToArray(relays, relay); + } + + cJSON_AddNumberToObject(root, "relay_count", relay_count); + cJSON_AddNumberToObject(root, "connected_count", connected_count); + cJSON_AddItemToObject(root, "relays", relays); + + char* out = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + if (relay_urls) { + for (int i = 0; i < relay_count; i++) { + free(relay_urls[i]); + } + free(relay_urls); + } + free(statuses); + + return out; +} + char* nostr_handler_get_admin_kind0_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) { return NULL; diff --git a/src/nostr_handler.h b/src/nostr_handler.h index 33ba0b4..a802dcb 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -43,6 +43,7 @@ char* nostr_handler_get_admin_kind0_context(void); char* nostr_handler_get_admin_kind10002_context(void); char* nostr_handler_get_admin_kind1_notes_context(void); int nostr_handler_is_wot_contact(const char* pubkey_hex); +char* nostr_handler_relay_status_json(void); void nostr_handler_cleanup(void); #endif \ No newline at end of file diff --git a/src/tools.c b/src/tools.c index c203ec9..f1bc6d1 100644 --- a/src/tools.c +++ b/src/tools.c @@ -508,6 +508,51 @@ void tools_cleanup(tools_context_t* ctx) { memset(ctx, 0, sizeof(*ctx)); } +static int is_hex_string_len(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 cJSON* parse_tool_args_json(const char* args_json) { + const char* raw_args_json = args_json ? args_json : "{}"; + raw_args_json = skip_ws(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(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; +} + char* tools_build_openai_schema_json(const tools_context_t* ctx) { (void)ctx; @@ -668,6 +713,117 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) { cJSON_AddItemToObject(t6, "function", t6_fn); cJSON_AddItemToArray(tools, t6); + cJSON* t7 = cJSON_CreateObject(); + cJSON* t7_fn = cJSON_CreateObject(); + cJSON* t7_params = cJSON_CreateObject(); + cJSON* t7_props = cJSON_CreateObject(); + cJSON* t7_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t7, "type", "function"); + cJSON_AddStringToObject(t7_fn, "name", "nostr_delete"); + cJSON_AddStringToObject(t7_fn, "description", "Request deletion of one or more previously published events (NIP-09 kind 5)"); + cJSON_AddStringToObject(t7_params, "type", "object"); + cJSON_AddItemToObject(t7_params, "properties", t7_props); + cJSON_AddItemToObject(t7_params, "required", t7_required); + + cJSON* p_del_ids = cJSON_CreateObject(); + cJSON_AddStringToObject(p_del_ids, "type", "array"); + cJSON* p_del_ids_items = cJSON_CreateObject(); + cJSON_AddStringToObject(p_del_ids_items, "type", "string"); + cJSON_AddItemToObject(p_del_ids, "items", p_del_ids_items); + cJSON_AddItemToObject(t7_props, "event_ids", p_del_ids); + + cJSON* p_del_kinds = cJSON_CreateObject(); + cJSON_AddStringToObject(p_del_kinds, "type", "array"); + cJSON* p_del_kinds_items = cJSON_CreateObject(); + cJSON_AddStringToObject(p_del_kinds_items, "type", "integer"); + cJSON_AddItemToObject(p_del_kinds, "items", p_del_kinds_items); + cJSON_AddItemToObject(t7_props, "kinds", p_del_kinds); + + cJSON* p_del_reason = cJSON_CreateObject(); + cJSON_AddStringToObject(p_del_reason, "type", "string"); + cJSON_AddItemToObject(t7_props, "reason", p_del_reason); + + cJSON_AddItemToArray(t7_required, cJSON_CreateString("event_ids")); + + cJSON_AddItemToObject(t7_fn, "parameters", t7_params); + cJSON_AddItemToObject(t7, "function", t7_fn); + cJSON_AddItemToArray(tools, t7); + + cJSON* t8 = cJSON_CreateObject(); + cJSON* t8_fn = cJSON_CreateObject(); + cJSON* t8_params = cJSON_CreateObject(); + cJSON* t8_props = cJSON_CreateObject(); + cJSON* t8_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t8, "type", "function"); + cJSON_AddStringToObject(t8_fn, "name", "nostr_react"); + cJSON_AddStringToObject(t8_fn, "description", "React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)"); + cJSON_AddStringToObject(t8_params, "type", "object"); + cJSON_AddItemToObject(t8_params, "properties", t8_props); + cJSON_AddItemToObject(t8_params, "required", t8_required); + + cJSON* p_react_event_id = cJSON_CreateObject(); + cJSON_AddStringToObject(p_react_event_id, "type", "string"); + cJSON_AddItemToObject(t8_props, "event_id", p_react_event_id); + + cJSON* p_react_event_pubkey = cJSON_CreateObject(); + cJSON_AddStringToObject(p_react_event_pubkey, "type", "string"); + cJSON_AddItemToObject(t8_props, "event_pubkey", p_react_event_pubkey); + + cJSON* p_react_event_kind = cJSON_CreateObject(); + cJSON_AddStringToObject(p_react_event_kind, "type", "integer"); + cJSON_AddItemToObject(t8_props, "event_kind", p_react_event_kind); + + cJSON* p_react_reaction = cJSON_CreateObject(); + cJSON_AddStringToObject(p_react_reaction, "type", "string"); + cJSON_AddItemToObject(t8_props, "reaction", p_react_reaction); + + cJSON_AddItemToArray(t8_required, cJSON_CreateString("event_id")); + cJSON_AddItemToArray(t8_required, cJSON_CreateString("event_pubkey")); + + cJSON_AddItemToObject(t8_fn, "parameters", t8_params); + cJSON_AddItemToObject(t8, "function", t8_fn); + cJSON_AddItemToArray(tools, t8); + + cJSON* t9 = cJSON_CreateObject(); + cJSON* t9_fn = cJSON_CreateObject(); + cJSON* t9_params = cJSON_CreateObject(); + cJSON* t9_props = cJSON_CreateObject(); + cJSON* t9_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t9, "type", "function"); + cJSON_AddStringToObject(t9_fn, "name", "nostr_profile_get"); + cJSON_AddStringToObject(t9_fn, "description", "Look up a Nostr profile (kind 0 metadata) by pubkey"); + cJSON_AddStringToObject(t9_params, "type", "object"); + cJSON_AddItemToObject(t9_params, "properties", t9_props); + cJSON_AddItemToObject(t9_params, "required", t9_required); + + cJSON* p_profile_pubkey = cJSON_CreateObject(); + cJSON_AddStringToObject(p_profile_pubkey, "type", "string"); + cJSON_AddItemToObject(t9_props, "pubkey", p_profile_pubkey); + + cJSON_AddItemToArray(t9_required, cJSON_CreateString("pubkey")); + + cJSON_AddItemToObject(t9_fn, "parameters", t9_params); + cJSON_AddItemToObject(t9, "function", t9_fn); + cJSON_AddItemToArray(tools, t9); + + cJSON* t10 = cJSON_CreateObject(); + cJSON* t10_fn = cJSON_CreateObject(); + cJSON* t10_params = cJSON_CreateObject(); + cJSON* t10_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t10, "type", "function"); + cJSON_AddStringToObject(t10_fn, "name", "nostr_relay_status"); + cJSON_AddStringToObject(t10_fn, "description", "Get connection status and statistics for all relays"); + cJSON_AddStringToObject(t10_params, "type", "object"); + cJSON_AddItemToObject(t10_params, "properties", t10_props); + + cJSON_AddItemToObject(t10_fn, "parameters", t10_params); + cJSON_AddItemToObject(t10, "function", t10_fn); + cJSON_AddItemToArray(tools, t10); + char* out = cJSON_PrintUnformatted(tools); cJSON_Delete(tools); return out; @@ -955,6 +1111,307 @@ static char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_js return json; } +static char* execute_nostr_delete(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* event_ids = cJSON_GetObjectItemCaseSensitive(args, "event_ids"); + cJSON* kinds = cJSON_GetObjectItemCaseSensitive(args, "kinds"); + cJSON* reason = cJSON_GetObjectItemCaseSensitive(args, "reason"); + + if (!event_ids || !cJSON_IsArray(event_ids) || cJSON_GetArraySize(event_ids) <= 0) { + cJSON_Delete(args); + return json_error("nostr_delete requires non-empty event_ids array"); + } + if (kinds && !cJSON_IsArray(kinds)) { + cJSON_Delete(args); + return json_error("nostr_delete kinds must be an array when provided"); + } + if (reason && !cJSON_IsString(reason)) { + cJSON_Delete(args); + return json_error("nostr_delete reason must be a string when provided"); + } + + cJSON* tags = cJSON_CreateArray(); + if (!tags) { + cJSON_Delete(args); + return json_error("failed to create tags"); + } + + int event_id_count = cJSON_GetArraySize(event_ids); + for (int i = 0; i < event_id_count; i++) { + cJSON* id = cJSON_GetArrayItem(event_ids, i); + if (!id || !cJSON_IsString(id) || !id->valuestring || !is_hex_string_len(id->valuestring, 64U)) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("nostr_delete event_ids must contain 64-char hex strings"); + } + if (add_string_tag(tags, "e", id->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add e tag"); + } + } + + if (kinds) { + int kind_count = cJSON_GetArraySize(kinds); + for (int i = 0; i < kind_count; i++) { + cJSON* k = cJSON_GetArrayItem(kinds, i); + if (!k || !cJSON_IsNumber(k)) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("nostr_delete kinds must contain integers"); + } + char kind_buf[32]; + snprintf(kind_buf, sizeof(kind_buf), "%d", (int)k->valuedouble); + if (add_string_tag(tags, "k", kind_buf) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add k tag"); + } + } + } + + nostr_publish_result_t publish_result; + memset(&publish_result, 0, sizeof(publish_result)); + + const char* reason_content = (reason && reason->valuestring) ? reason->valuestring : ""; + int rc = nostr_handler_publish_kind_event(5, reason_content, tags, &publish_result); + cJSON_Delete(tags); + cJSON_Delete(args); + if (rc != 0) { + nostr_handler_publish_result_free(&publish_result); + return json_error("nostr_delete failed"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + nostr_handler_publish_result_free(&publish_result); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0); + cJSON_AddStringToObject(out, "message", "nostr_delete published"); + cJSON_AddNumberToObject(out, "kind", publish_result.kind); + cJSON_AddStringToObject(out, "event_id", publish_result.event_id); + cJSON_AddNumberToObject(out, "requested_event_count", event_id_count); + cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count); + cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count); + + cJSON* relays = cJSON_CreateArray(); + if (!relays) { + nostr_handler_publish_result_free(&publish_result); + cJSON_Delete(out); + return NULL; + } + for (int i = 0; i < publish_result.relay_count; i++) { + cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : "")); + } + cJSON_AddItemToObject(out, "relays", relays); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + nostr_handler_publish_result_free(&publish_result); + return json; +} + +static char* execute_nostr_react(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* event_id = cJSON_GetObjectItemCaseSensitive(args, "event_id"); + cJSON* event_pubkey = cJSON_GetObjectItemCaseSensitive(args, "event_pubkey"); + cJSON* event_kind = cJSON_GetObjectItemCaseSensitive(args, "event_kind"); + cJSON* reaction = cJSON_GetObjectItemCaseSensitive(args, "reaction"); + + if (!event_id || !cJSON_IsString(event_id) || !event_id->valuestring || !is_hex_string_len(event_id->valuestring, 64U) || + !event_pubkey || !cJSON_IsString(event_pubkey) || !event_pubkey->valuestring || !is_hex_string_len(event_pubkey->valuestring, 64U)) { + cJSON_Delete(args); + return json_error("nostr_react requires event_id and event_pubkey as 64-char hex strings"); + } + if (event_kind && !cJSON_IsNumber(event_kind)) { + cJSON_Delete(args); + return json_error("nostr_react event_kind must be an integer when provided"); + } + if (reaction && !cJSON_IsString(reaction)) { + cJSON_Delete(args); + return json_error("nostr_react reaction must be a string when provided"); + } + + cJSON* tags = cJSON_CreateArray(); + if (!tags) { + cJSON_Delete(args); + return json_error("failed to create tags"); + } + + if (add_string_tag(tags, "e", event_id->valuestring) != 0 || + add_string_tag(tags, "p", event_pubkey->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add required reaction tags"); + } + + if (event_kind) { + char kind_buf[32]; + snprintf(kind_buf, sizeof(kind_buf), "%d", (int)event_kind->valuedouble); + if (add_string_tag(tags, "k", kind_buf) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add k tag"); + } + } + + const char* reaction_content = (reaction && reaction->valuestring && reaction->valuestring[0] != '\0') ? reaction->valuestring : "+"; + + nostr_publish_result_t publish_result; + memset(&publish_result, 0, sizeof(publish_result)); + + int rc = nostr_handler_publish_kind_event(7, reaction_content, tags, &publish_result); + cJSON_Delete(tags); + cJSON_Delete(args); + if (rc != 0) { + nostr_handler_publish_result_free(&publish_result); + return json_error("nostr_react failed"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + nostr_handler_publish_result_free(&publish_result); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0); + cJSON_AddStringToObject(out, "message", "nostr_react published"); + cJSON_AddStringToObject(out, "reaction", reaction_content); + cJSON_AddNumberToObject(out, "kind", publish_result.kind); + cJSON_AddStringToObject(out, "event_id", publish_result.event_id); + cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count); + cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count); + + cJSON* relays = cJSON_CreateArray(); + if (!relays) { + nostr_handler_publish_result_free(&publish_result); + cJSON_Delete(out); + return NULL; + } + for (int i = 0; i < publish_result.relay_count; i++) { + cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : "")); + } + cJSON_AddItemToObject(out, "relays", relays); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + nostr_handler_publish_result_free(&publish_result); + return json; +} + +static char* execute_nostr_profile_get(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); + if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U)) { + cJSON_Delete(args); + return json_error("nostr_profile_get requires pubkey as 64-char hex string"); + } + + cJSON* filter = cJSON_CreateObject(); + cJSON* kinds = cJSON_CreateArray(); + cJSON* authors = cJSON_CreateArray(); + if (!filter || !kinds || !authors) { + cJSON_Delete(filter); + cJSON_Delete(kinds); + cJSON_Delete(authors); + cJSON_Delete(args); + return json_error("failed to create profile query filter"); + } + + cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey->valuestring)); + cJSON_AddItemToObject(filter, "authors", authors); + cJSON_AddNumberToObject(filter, "limit", 1); + + char* events_json = nostr_handler_query_json(filter, 8000); + cJSON_Delete(filter); + cJSON_Delete(args); + if (!events_json) { + return json_error("nostr_profile_get query failed"); + } + + cJSON* events = cJSON_Parse(events_json); + free(events_json); + if (!events || !cJSON_IsArray(events)) { + cJSON_Delete(events); + return json_error("nostr_profile_get returned invalid events JSON"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + cJSON_Delete(events); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + + int found = cJSON_GetArraySize(events) > 0; + cJSON_AddBoolToObject(out, "found", found ? 1 : 0); + + if (found) { + cJSON* ev = cJSON_GetArrayItem(events, 0); + cJSON* ev_dup = ev ? cJSON_Duplicate(ev, 1) : NULL; + if (ev_dup) { + cJSON_AddItemToObject(out, "event", ev_dup); + } + + cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; + if (content && cJSON_IsString(content) && content->valuestring) { + cJSON* profile = cJSON_Parse(content->valuestring); + if (profile && cJSON_IsObject(profile)) { + cJSON_AddItemToObject(out, "profile", profile); + } else { + cJSON_Delete(profile); + cJSON_AddStringToObject(out, "profile_raw", content->valuestring); + } + } + } + + cJSON_Delete(events); + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + +static char* execute_nostr_relay_status(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + cJSON_Delete(args); + + char* status_json = nostr_handler_relay_status_json(); + if (!status_json) { + return json_error("nostr_relay_status failed"); + } + + cJSON* status = cJSON_Parse(status_json); + free(status_json); + if (!status) { + return json_error("nostr_relay_status returned invalid JSON"); + } + + cJSON* out = cJSON_CreateObject(); + if (!out) { + cJSON_Delete(status); + return NULL; + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddItemToObject(out, "status", status); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + static char* execute_nostr_query(const char* args_json) { cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); @@ -1167,6 +1624,18 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg if (strcmp(tool_name, "nostr_post") == 0) { return execute_nostr_post(args_json); } + if (strcmp(tool_name, "nostr_delete") == 0) { + return execute_nostr_delete(args_json); + } + if (strcmp(tool_name, "nostr_react") == 0) { + return execute_nostr_react(args_json); + } + if (strcmp(tool_name, "nostr_profile_get") == 0) { + return execute_nostr_profile_get(args_json); + } + if (strcmp(tool_name, "nostr_relay_status") == 0) { + return execute_nostr_relay_status(args_json); + } if (strcmp(tool_name, "nostr_query") == 0) { return execute_nostr_query(args_json); }