Files
sovereign_browser/src/agent_skills.c
T

834 lines
26 KiB
C

/*
* agent_skills.c — Nostr kind 31123 skill management for sovereign_browser
*
* Implements fetch, selection, system-prompt building, publish, and delete
* for kind 31123 skill events. Skills are PUBLIC events (no NIP-44
* encryption) authored by any user. sovereign_browser fetches them from
* the local SQLite cache (populated by the relay fetch thread on startup)
* and lets users select skills whose templates are concatenated into the
* system prompt.
*
* Reuses the event signing + relay publishing patterns from
* settings_sync.c, bookmarks.c, and agent_conversations.c.
*
* See plans/cross-project-agent-sync.md (Phase C/D) for the full design.
*/
#include "agent_skills.h"
#include "db.h"
#include "settings.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "nostr_core/nostr_core.h"
#include "../nostr_core_lib/cjson/cJSON.h"
/* ── Global state ───────────────────────────────────────────────────── */
static nostr_signer_t *g_signer = NULL;
static char g_pubkey[65] = {0};
static int g_have_signer = 0;
/* ── Init ───────────────────────────────────────────────────────────── */
void agent_skills_init(nostr_signer_t *signer, const char *pubkey_hex) {
g_signer = signer;
g_have_signer = (signer != NULL);
if (pubkey_hex) {
snprintf(g_pubkey, sizeof(g_pubkey), "%s", pubkey_hex);
} else {
g_pubkey[0] = '\0';
}
}
void agent_skills_set_signer(nostr_signer_t *signer, const char *pubkey_hex) {
agent_skills_init(signer, pubkey_hex);
}
/* ── Helpers ────────────────────────────────────────────────────────── */
/* Parse bootstrap relays from settings into an array.
* Returns the count. Fills urls_out (pointers into relay_buf).
* Caller must g_free(relay_buf) after use. (Same pattern as
* settings_sync.c, bookmarks.c, agent_conversations.c.) */
static int parse_relays(char **relay_buf, const char **urls_out, int max) {
const browser_settings_t *s = settings_get();
*relay_buf = g_strdup(s->bootstrap_relays);
int count = 0;
char *saveptr = NULL;
char *line = strtok_r(*relay_buf, "\n\r", &saveptr);
while (line != NULL && count < max) {
while (*line == ' ' || *line == '\t') line++;
if (line[0] != '\0' &&
(strncmp(line, "wss://", 6) == 0 ||
strncmp(line, "ws://", 5) == 0)) {
urls_out[count++] = line;
}
line = strtok_r(NULL, "\n\r", &saveptr);
}
return count;
}
/* Get the value of the first tag with the given name. Returns a pointer
* into the event's tags (or NULL). Not owned by caller. */
static const char *event_tag_value(const cJSON *event, const char *name) {
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
if (!cJSON_IsArray(tags)) return NULL;
cJSON *tag;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(t0) && strcmp(t0->valuestring, name) == 0 &&
cJSON_IsString(t1)) {
return t1->valuestring;
}
}
return NULL;
}
/* Check whether an event has a tag [name, value]. Returns 1/0. */
static int event_has_tag(const cJSON *event, const char *name,
const char *value) {
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
if (!cJSON_IsArray(tags)) return 0;
cJSON *tag;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(t0) && strcmp(t0->valuestring, name) == 0 &&
cJSON_IsString(t1) && strcmp(t1->valuestring, value) == 0) {
return 1;
}
}
return 0;
}
/* Collect all values of tags with the given name into a newly allocated
* cJSON array of strings. Caller must cJSON_Delete(). */
static cJSON *event_tag_values(const cJSON *event, const char *name) {
cJSON *result = cJSON_CreateArray();
cJSON *tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
if (!cJSON_IsArray(tags)) return result;
cJSON *tag;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(t0) && strcmp(t0->valuestring, name) == 0 &&
cJSON_IsString(t1)) {
cJSON_AddItemToArray(result, cJSON_CreateString(t1->valuestring));
}
}
return result;
}
/* ── Fetch / list ──────────────────────────────────────────────────── */
/* Parse a single kind 31123 event into an agent_skill_t struct.
* Returns 0 on success, -1 on error. On success, the skill's owned
* fields (content, requires_tools) are allocated and must be freed
* via agent_skills_free_list(). */
static int parse_skill_event(const cJSON *event, agent_skill_t *out) {
if (event == NULL || out == NULL) return -1;
memset(out, 0, sizeof(*out));
/* d-tag (unique identifier) */
const char *d = event_tag_value(event, "d");
if (d == NULL || d[0] == '\0') return -1;
snprintf(out->d_tag, sizeof(out->d_tag), "%s", d);
/* name tag */
const char *name = event_tag_value(event, "name");
if (name) {
snprintf(out->name, sizeof(out->name), "%s", name);
} else {
/* Fall back to the d-tag if no name tag. */
snprintf(out->name, sizeof(out->name), "%s", d);
}
/* description tag */
const char *desc = event_tag_value(event, "description");
if (desc) {
snprintf(out->description, sizeof(out->description), "%s", desc);
}
/* author pubkey */
cJSON *pk = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
if (cJSON_IsString(pk)) {
snprintf(out->pubkey, sizeof(out->pubkey), "%s", pk->valuestring);
}
/* content (system prompt template) */
cJSON *content = cJSON_GetObjectItemCaseSensitive(event, "content");
if (cJSON_IsString(content)) {
out->content = g_strdup(content->valuestring);
} else {
out->content = g_strdup("");
}
/* requires_tool tags */
cJSON *tools = event_tag_values(event, "requires_tool");
if (tools != NULL) {
int n = cJSON_GetArraySize(tools);
if (n > AGENT_SKILL_MAX_TOOLS) n = AGENT_SKILL_MAX_TOOLS;
out->tool_count = n;
if (n > 0) {
out->requires_tools = g_new0(char *, n);
for (int i = 0; i < n; i++) {
cJSON *t = cJSON_GetArrayItem(tools, i);
if (cJSON_IsString(t)) {
out->requires_tools[i] = g_strdup(t->valuestring);
} else {
out->requires_tools[i] = g_strdup("");
}
}
}
cJSON_Delete(tools);
}
return 0;
}
void agent_skills_free_list(agent_skill_t *skills, int count) {
if (skills == NULL) return;
for (int i = 0; i < count; i++) {
g_free(skills[i].content);
if (skills[i].requires_tools) {
for (int j = 0; j < skills[i].tool_count; j++) {
g_free(skills[i].requires_tools[j]);
}
g_free(skills[i].requires_tools);
}
}
g_free(skills);
}
/* Fetch and parse all kind 31123 events into an array of agent_skill_t.
* Returns the array (caller must agent_skills_free_list()) and sets
* *count_out. Returns NULL on error or if no skills exist. */
static agent_skill_t *fetch_skill_structs(int *count_out) {
if (count_out == NULL) return NULL;
*count_out = 0;
cJSON *events = db_get_events_by_kind(AGENT_SKILL_KIND, 500);
if (events == NULL) return NULL;
int n = cJSON_GetArraySize(events);
if (n <= 0) {
cJSON_Delete(events);
return NULL;
}
/* Deduplicate by d-tag: keep the newest (events are newest-first
* from db_get_events_by_kind). */
agent_skill_t *skills = g_new0(agent_skill_t, n);
int count = 0;
cJSON *seen_d = cJSON_CreateArray();
for (int i = 0; i < n; i++) {
cJSON *ev = cJSON_GetArrayItem(events, i);
const char *d = event_tag_value(ev, "d");
if (d == NULL || d[0] == '\0') continue;
/* Skip if we've already seen this d-tag. */
int seen = 0;
cJSON *s;
cJSON_ArrayForEach(s, seen_d) {
if (cJSON_IsString(s) && strcmp(s->valuestring, d) == 0) {
seen = 1;
break;
}
}
if (seen) continue;
cJSON_AddItemToArray(seen_d, cJSON_CreateString(d));
if (parse_skill_event(ev, &skills[count]) == 0) {
count++;
}
}
cJSON_Delete(seen_d);
cJSON_Delete(events);
if (count == 0) {
g_free(skills);
return NULL;
}
*count_out = count;
return skills;
}
cJSON *agent_skills_fetch(void) {
cJSON *result = cJSON_CreateArray();
int count = 0;
agent_skill_t *skills = fetch_skill_structs(&count);
if (skills == NULL) return result;
for (int i = 0; i < count; i++) {
cJSON *obj = cJSON_CreateObject();
cJSON_AddStringToObject(obj, "d", skills[i].d_tag);
cJSON_AddStringToObject(obj, "name", skills[i].name);
cJSON_AddStringToObject(obj, "description", skills[i].description);
cJSON_AddStringToObject(obj, "pubkey", skills[i].pubkey);
cJSON_AddStringToObject(obj, "content", skills[i].content);
/* requires_tools array */
cJSON *tools = cJSON_CreateArray();
for (int j = 0; j < skills[i].tool_count; j++) {
cJSON_AddItemToArray(tools,
cJSON_CreateString(skills[i].requires_tools[j]));
}
cJSON_AddItemToObject(obj, "requires_tools", tools);
cJSON_AddItemToArray(result, obj);
}
agent_skills_free_list(skills, count);
return result;
}
/* ── Sovereign Browser Skill (default, from local settings) ─────────── */
cJSON *agent_skills_get_default(void) {
const browser_settings_t *s = settings_get();
cJSON *obj = cJSON_CreateObject();
/* d is empty — this skill is unsaved (not published to Nostr). */
cJSON_AddStringToObject(obj, "d", "");
cJSON_AddStringToObject(obj, "name",
s->agent_skill_name[0] ? s->agent_skill_name
: SETTINGS_AGENT_SKILL_NAME_DEFAULT);
cJSON_AddStringToObject(obj, "description",
s->agent_skill_description[0] ? s->agent_skill_description
: SETTINGS_AGENT_SKILL_DESCRIPTION_DEFAULT);
cJSON_AddStringToObject(obj, "content",
s->agent_skill_template[0] ? s->agent_skill_template
: SETTINGS_AGENT_SYSTEM_PROMPT_DEFAULT);
cJSON_AddStringToObject(obj, "pubkey", "");
cJSON_AddBoolToObject(obj, "unsaved", 1);
/* Parse requires_tools (comma-separated) into a JSON array. */
cJSON *tools = cJSON_CreateArray();
const char *tools_str = s->agent_skill_requires_tools[0]
? s->agent_skill_requires_tools
: SETTINGS_AGENT_SKILL_REQUIRES_TOOLS_DEFAULT;
char *buf = g_strdup(tools_str);
char *saveptr = NULL;
char *tok = strtok_r(buf, ",", &saveptr);
while (tok != NULL) {
/* Trim leading whitespace. */
while (*tok == ' ' || *tok == '\t') tok++;
if (tok[0] != '\0') {
cJSON_AddItemToArray(tools, cJSON_CreateString(tok));
}
tok = strtok_r(NULL, ",", &saveptr);
}
g_free(buf);
cJSON_AddItemToObject(obj, "requires_tools", tools);
return obj;
}
/* ── Selection (local settings) ────────────────────────────────────── */
cJSON *agent_skills_get_selected(void) {
cJSON *result = cJSON_CreateArray();
char *json_str = db_kv_get_copy(AGENT_SKILLS_SELECTED_KEY);
if (json_str == NULL || json_str[0] == '\0') {
g_free(json_str);
return result;
}
cJSON *parsed = cJSON_Parse(json_str);
g_free(json_str);
if (cJSON_IsArray(parsed)) {
cJSON *item;
cJSON_ArrayForEach(item, parsed) {
if (cJSON_IsString(item)) {
cJSON_AddItemToArray(result, cJSON_CreateString(item->valuestring));
}
}
}
if (parsed) cJSON_Delete(parsed);
return result;
}
int agent_skills_set_selected(const char *d_tags_json) {
if (d_tags_json == NULL) d_tags_json = "[]";
return db_kv_set(AGENT_SKILLS_SELECTED_KEY, d_tags_json);
}
cJSON *agent_skills_toggle_selected(const char *d_tag) {
if (d_tag == NULL || d_tag[0] == '\0') return NULL;
cJSON *selected = agent_skills_get_selected();
/* Check if already selected. */
int found = -1;
int n = cJSON_GetArraySize(selected);
for (int i = 0; i < n; i++) {
cJSON *item = cJSON_GetArrayItem(selected, i);
if (cJSON_IsString(item) && strcmp(item->valuestring, d_tag) == 0) {
found = i;
break;
}
}
if (found >= 0) {
/* Remove it. */
cJSON_DeleteItemFromArray(selected, found);
} else {
/* Add it (at the end to preserve selection order). */
cJSON_AddItemToArray(selected, cJSON_CreateString(d_tag));
}
/* Save back to db_kv. */
char *json = cJSON_PrintUnformatted(selected);
if (json) {
agent_skills_set_selected(json);
free(json);
}
return selected;
}
/* ── Build system prompt ───────────────────────────────────────────── */
char *agent_skills_build_system_prompt(void) {
/* Always start with the Sovereign Browser Skill template (the
* default system prompt from local settings). This is the base
* prompt; selected Nostr skills are appended after it. */
const browser_settings_t *s = settings_get();
const char *base = s->agent_skill_template[0]
? s->agent_skill_template
: SETTINGS_AGENT_SYSTEM_PROMPT_DEFAULT;
GString *combined = g_string_new(base);
/* Fetch selected skills and append their templates. */
cJSON *selected = agent_skills_get_selected();
if (selected != NULL) {
int n = cJSON_GetArraySize(selected);
if (n > 0) {
int skill_count = 0;
agent_skill_t *skills = fetch_skill_structs(&skill_count);
if (skills != NULL) {
for (int i = 0; i < n; i++) {
cJSON *item = cJSON_GetArrayItem(selected, i);
if (!cJSON_IsString(item)) continue;
const char *d = item->valuestring;
agent_skill_t *skill = NULL;
for (int j = 0; j < skill_count; j++) {
if (strcmp(skills[j].d_tag, d) == 0) {
skill = &skills[j];
break;
}
}
if (skill == NULL || skill->content == NULL ||
skill->content[0] == '\0') continue;
g_string_append(combined, "\n\n---\n\n");
g_string_append(combined, skill->content);
}
agent_skills_free_list(skills, skill_count);
}
}
cJSON_Delete(selected);
}
return g_string_free(combined, FALSE);
}
/* ── Publish ───────────────────────────────────────────────────────── */
/* Derive a d-tag from a skill name: lowercase, replace spaces/separators
* with hyphens, strip non-alphanumeric. Returns a newly allocated string
* (caller must g_free). */
static char *derive_d_tag(const char *name) {
if (name == NULL || name[0] == '\0') {
/* Fall back to a timestamp-based id. */
return g_strdup_printf("skill-%ld", (long)time(NULL));
}
/* Build a slug. */
GString *slug = g_string_new(NULL);
for (const char *p = name; *p; p++) {
char c = *p;
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
g_string_append_c(slug, c);
} else if (c >= 'A' && c <= 'Z') {
g_string_append_c(slug, c - 'A' + 'a');
} else if (c == ' ' || c == '_' || c == '-' || c == '.') {
g_string_append_c(slug, '-');
}
/* Skip other characters. */
}
/* Collapse consecutive hyphens and trim leading/trailing. */
GString *clean = g_string_new(NULL);
int prev_hyphen = 1; /* treat start as if previous was hyphen */
for (gsize i = 0; i < slug->len; i++) {
char c = slug->str[i];
if (c == '-') {
if (!prev_hyphen) {
g_string_append_c(clean, '-');
prev_hyphen = 1;
}
} else {
g_string_append_c(clean, c);
prev_hyphen = 0;
}
}
/* Trim trailing hyphen. */
if (clean->len > 0 && clean->str[clean->len - 1] == '-') {
g_string_truncate(clean, clean->len - 1);
}
g_string_free(slug, TRUE);
if (clean->len == 0) {
g_string_free(clean, TRUE);
return g_strdup_printf("skill-%ld", (long)time(NULL));
}
/* Append a short timestamp suffix to ensure uniqueness. */
char *result = g_strdup_printf("%s-%ld", clean->str, (long)time(NULL) % 100000);
g_string_free(clean, TRUE);
return result;
}
char *agent_skills_publish(const char *name,
const char *description,
const char *content,
const char *requires_tools_json) {
if (!g_have_signer) {
g_printerr("[agent_skills] No signer, cannot publish\n");
return NULL;
}
if (content == NULL || content[0] == '\0') {
g_printerr("[agent_skills] Content is required\n");
return NULL;
}
/* Derive the d-tag from the name. */
char *d_tag = derive_d_tag(name);
/* Build tags:
* [["d", d_tag],
* ["name", name],
* ["description", desc],
* ["client", "sovereign_browser"],
* ["requires_tool", tool1], ...] */
cJSON *tags = cJSON_CreateArray();
cJSON *d_tag_arr = cJSON_CreateArray();
cJSON_AddItemToArray(d_tag_arr, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag_arr, cJSON_CreateString(d_tag));
cJSON_AddItemToArray(tags, d_tag_arr);
if (name && name[0]) {
cJSON *name_tag = cJSON_CreateArray();
cJSON_AddItemToArray(name_tag, cJSON_CreateString("name"));
cJSON_AddItemToArray(name_tag, cJSON_CreateString(name));
cJSON_AddItemToArray(tags, name_tag);
}
if (description && description[0]) {
cJSON *desc_tag = cJSON_CreateArray();
cJSON_AddItemToArray(desc_tag, cJSON_CreateString("description"));
cJSON_AddItemToArray(desc_tag, cJSON_CreateString(description));
cJSON_AddItemToArray(tags, desc_tag);
}
cJSON *client_tag = cJSON_CreateArray();
cJSON_AddItemToArray(client_tag, cJSON_CreateString("client"));
cJSON_AddItemToArray(client_tag, cJSON_CreateString("sovereign_browser"));
cJSON_AddItemToArray(tags, client_tag);
/* Parse requires_tools_json and add a requires_tool tag for each. */
if (requires_tools_json && requires_tools_json[0]) {
cJSON *tools = cJSON_Parse(requires_tools_json);
if (cJSON_IsArray(tools)) {
int n = cJSON_GetArraySize(tools);
if (n > AGENT_SKILL_MAX_TOOLS) n = AGENT_SKILL_MAX_TOOLS;
for (int i = 0; i < n; i++) {
cJSON *t = cJSON_GetArrayItem(tools, i);
if (cJSON_IsString(t) && t->valuestring[0]) {
cJSON *rt_tag = cJSON_CreateArray();
cJSON_AddItemToArray(rt_tag, cJSON_CreateString("requires_tool"));
cJSON_AddItemToArray(rt_tag, cJSON_CreateString(t->valuestring));
cJSON_AddItemToArray(tags, rt_tag);
}
}
}
if (tools) cJSON_Delete(tools);
}
/* Create and sign the event. Skills are PUBLIC — no encryption.
* The content is the raw system prompt template. */
cJSON *event = nostr_create_and_sign_event_with_signer(
AGENT_SKILL_KIND, content, tags, g_signer, time(NULL));
cJSON_Delete(tags);
if (event == NULL) {
g_printerr("[agent_skills] Failed to create/sign event\n");
g_free(d_tag);
return NULL;
}
/* Store in SQLite. */
db_store_event(event);
/* Publish to bootstrap relays. */
char *relay_buf = NULL;
const char *relay_urls[32];
int relay_count = parse_relays(&relay_buf, relay_urls, 32);
if (relay_count > 0) {
int success_count = 0;
publish_result_t *results = synchronous_publish_event_with_progress(
relay_urls, relay_count, event, &success_count,
15, NULL, NULL, 0, NULL);
if (results) free(results);
g_print("[agent_skills] Published skill '%s' (d:%s) to %d/%d relays\n",
name ? name : "(unnamed)", d_tag, success_count, relay_count);
} else {
g_print("[agent_skills] No relays configured, skill stored locally\n");
}
g_free(relay_buf);
cJSON_Delete(event);
return d_tag;
}
/* ── Delete ────────────────────────────────────────────────────────── */
int agent_skills_delete(const char *d_tag) {
if (d_tag == NULL || d_tag[0] == '\0') return -1;
if (g_pubkey[0] == '\0') return -1;
/* Find the kind 31123 event with this d-tag authored by the user. */
cJSON *events = db_get_events(g_pubkey, AGENT_SKILL_KIND, 500);
if (events == NULL) return -1;
char *event_id = NULL;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON *ev = cJSON_GetArrayItem(events, i);
if (event_has_tag(ev, "d", d_tag)) {
cJSON *id_item = cJSON_GetObjectItemCaseSensitive(ev, "id");
if (cJSON_IsString(id_item)) {
event_id = g_strdup(id_item->valuestring);
}
break;
}
}
cJSON_Delete(events);
if (event_id == NULL) {
g_printerr("[agent_skills] Skill '%s' not found for delete\n", d_tag);
return -1;
}
/* Publish a kind 5 deletion event referencing the skill event. */
if (g_have_signer) {
cJSON *tags = cJSON_CreateArray();
cJSON *e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(event_id));
cJSON_AddItemToArray(tags, e_tag);
cJSON *event = nostr_create_and_sign_event_with_signer(
5, "Deleted skill", tags, g_signer, time(NULL));
cJSON_Delete(tags);
if (event) {
db_store_event(event);
char *relay_buf = NULL;
const char *relay_urls[32];
int relay_count = parse_relays(&relay_buf, relay_urls, 32);
if (relay_count > 0) {
int success_count = 0;
publish_result_t *results =
synchronous_publish_event_with_progress(
relay_urls, relay_count, event, &success_count,
15, NULL, NULL, 0, NULL);
if (results) free(results);
g_print("[agent_skills] Published deletion for '%s' to %d/%d relays\n",
d_tag, success_count, relay_count);
}
g_free(relay_buf);
cJSON_Delete(event);
}
}
/* Remove from local SQLite cache. */
db_delete_event(event_id);
g_free(event_id);
/* Also remove from the selected list if present. */
cJSON *selected = agent_skills_get_selected();
if (selected) {
int sn = cJSON_GetArraySize(selected);
for (int i = 0; i < sn; i++) {
cJSON *item = cJSON_GetArrayItem(selected, i);
if (cJSON_IsString(item) && strcmp(item->valuestring, d_tag) == 0) {
cJSON_DeleteItemFromArray(selected, i);
break;
}
}
char *json = cJSON_PrintUnformatted(selected);
if (json) {
agent_skills_set_selected(json);
free(json);
}
cJSON_Delete(selected);
}
return 0;
}
/* ── Update (re-publish an existing skill) ──────────────────────────── */
int agent_skills_update(const char *d_tag,
const char *name,
const char *description,
const char *content,
const char *requires_tools_json) {
if (d_tag == NULL || d_tag[0] == '\0') return -1;
if (content == NULL || content[0] == '\0') return -1;
if (!g_have_signer || g_pubkey[0] == '\0') {
g_printerr("[agent_skills] No signer, cannot update\n");
return -1;
}
/* Find the existing kind 31123 event with this d-tag authored by
* the user. We reuse the existing d-tag (addressable event) so the
* update replaces the prior version. */
cJSON *events = db_get_events(g_pubkey, AGENT_SKILL_KIND, 500);
if (events == NULL) {
g_printerr("[agent_skills] No skill events found for update\n");
return -1;
}
cJSON *found = NULL;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON *ev = cJSON_GetArrayItem(events, i);
if (event_has_tag(ev, "d", d_tag)) {
found = cJSON_Duplicate(ev, 1);
break;
}
}
cJSON_Delete(events);
if (found == NULL) {
g_printerr("[agent_skills] Skill '%s' not found for update\n", d_tag);
return -1;
}
/* If name/description not provided, keep the existing values. */
char name_buf[128];
char desc_buf[512];
if (name == NULL || name[0] == '\0') {
const char *old = event_tag_value(found, "name");
snprintf(name_buf, sizeof(name_buf), "%s", old ? old : d_tag);
name = name_buf;
}
if (description == NULL) {
const char *old = event_tag_value(found, "description");
snprintf(desc_buf, sizeof(desc_buf), "%s", old ? old : "");
description = desc_buf;
}
/* Build the new tags: same d-tag, new name/description, client tag,
* and requires_tool tags. */
cJSON *tags = cJSON_CreateArray();
cJSON *d_tag_arr = cJSON_CreateArray();
cJSON_AddItemToArray(d_tag_arr, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag_arr, cJSON_CreateString(d_tag));
cJSON_AddItemToArray(tags, d_tag_arr);
if (name && name[0]) {
cJSON *name_tag = cJSON_CreateArray();
cJSON_AddItemToArray(name_tag, cJSON_CreateString("name"));
cJSON_AddItemToArray(name_tag, cJSON_CreateString(name));
cJSON_AddItemToArray(tags, name_tag);
}
if (description && description[0]) {
cJSON *desc_tag = cJSON_CreateArray();
cJSON_AddItemToArray(desc_tag, cJSON_CreateString("description"));
cJSON_AddItemToArray(desc_tag, cJSON_CreateString(description));
cJSON_AddItemToArray(tags, desc_tag);
}
cJSON *client_tag = cJSON_CreateArray();
cJSON_AddItemToArray(client_tag, cJSON_CreateString("client"));
cJSON_AddItemToArray(client_tag, cJSON_CreateString("sovereign_browser"));
cJSON_AddItemToArray(tags, client_tag);
if (requires_tools_json && requires_tools_json[0]) {
cJSON *tools = cJSON_Parse(requires_tools_json);
if (cJSON_IsArray(tools)) {
int tn = cJSON_GetArraySize(tools);
if (tn > AGENT_SKILL_MAX_TOOLS) tn = AGENT_SKILL_MAX_TOOLS;
for (int i = 0; i < tn; i++) {
cJSON *t = cJSON_GetArrayItem(tools, i);
if (cJSON_IsString(t) && t->valuestring[0]) {
cJSON *rt_tag = cJSON_CreateArray();
cJSON_AddItemToArray(rt_tag, cJSON_CreateString("requires_tool"));
cJSON_AddItemToArray(rt_tag, cJSON_CreateString(t->valuestring));
cJSON_AddItemToArray(tags, rt_tag);
}
}
}
if (tools) cJSON_Delete(tools);
}
/* Create and sign the new event (same d-tag → replaces the old). */
cJSON *event = nostr_create_and_sign_event_with_signer(
AGENT_SKILL_KIND, content, tags, g_signer, time(NULL));
cJSON_Delete(tags);
cJSON_Delete(found);
if (event == NULL) {
g_printerr("[agent_skills] Failed to create/sign update event\n");
return -1;
}
/* Store in SQLite (replaces the old event by id; the d-tag is the
* same so fetch_skill_structs dedup keeps the newest). */
db_store_event(event);
/* Publish to bootstrap relays. */
char *relay_buf = NULL;
const char *relay_urls[32];
int relay_count = parse_relays(&relay_buf, relay_urls, 32);
if (relay_count > 0) {
int success_count = 0;
publish_result_t *results = synchronous_publish_event_with_progress(
relay_urls, relay_count, event, &success_count,
15, NULL, NULL, 0, NULL);
if (results) free(results);
g_print("[agent_skills] Updated skill '%s' to %d/%d relays\n",
d_tag, success_count, relay_count);
} else {
g_print("[agent_skills] No relays configured, update stored locally\n");
}
g_free(relay_buf);
cJSON_Delete(event);
return 0;
}