From a446f254007cc2a96ff8e5c9e02189120e9c0e92 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Mar 2026 18:55:04 -0400 Subject: [PATCH] v0.0.21 - Add full skill_* tool family with schema, execution, dispatch, and README updates --- README.md | 46 +- plans/skill_tools.md | 337 +++++++++++ plans/tool_testing.md | 139 +++++ src/main.h | 4 +- src/tools.c | 1299 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1814 insertions(+), 11 deletions(-) create mode 100644 plans/skill_tools.md create mode 100644 plans/tool_testing.md diff --git a/README.md b/README.md index d0e3abc..e952409 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.20 +## Current Status — v0.0.21 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.20 — Add Tier2/Tier3 Nostr tools: nip05 lookup, encode/decode, dm send, relay info, nip44 encrypt/decrypt, nip17 dm, list manage +> Last release update: v0.0.21 — Add full skill_* tool family with schema, execution, dispatch, and README updates - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected @@ -276,15 +276,43 @@ Every serialized LLM context payload is appended to [`context.log`](context.log) ## Tooling Interface -Current tool schema exposed to the LLM in [`tools_build_openai_schema_json()`](src/tools.c:72): +Current tool schema exposed to the LLM in [`tools_build_openai_schema_json()`](src/tools.c:881): -- `nostr_post` -- `nostr_query` -- `shell_exec` -- `file_read` -- `file_write` +- Nostr publish/query: + - `nostr_post` + - `nostr_post_readme` + - `nostr_query` +- Nostr interaction and moderation: + - `nostr_delete` + - `nostr_react` + - `nostr_profile_get` + - `nostr_relay_status` + - `nostr_relay_info` + - `nostr_nip05_lookup` +- Nostr encode/decode + encryption/DM: + - `nostr_encode` + - `nostr_decode` + - `nostr_encrypt` + - `nostr_decrypt` + - `nostr_dm_send` + - `nostr_dm_send_nip17` +- Nostr list management: + - `nostr_list_manage` +- Skill management: + - `skill_create` + - `skill_list` + - `skill_adopt` + - `skill_remove` + - `skill_search` +- Local/host tools: + - `shell_exec` + - `file_read` + - `file_write` + - `http_fetch` +- Agent metadata: + - `my_version` -Execution entrypoint: [`tools_execute()`](src/tools.c:434). +Execution entrypoint: [`tools_execute()`](src/tools.c:3765). ## Project Structure diff --git a/plans/skill_tools.md b/plans/skill_tools.md new file mode 100644 index 0000000..711975e --- /dev/null +++ b/plans/skill_tools.md @@ -0,0 +1,337 @@ +# Skill Tools — Architecture Plan + +## Overview + +Add a family of five skill-management tools to Didactyl so the agent can **create, list, adopt, remove, and discover** skills at runtime — all through the existing LLM tool-calling loop. + +Skills are Nostr events. The tools are thin orchestration wrappers over the existing `nostr_handler_publish_kind_event()` and `nostr_handler_query_json()` primitives. + +--- + +## Nostr Kind Reference + +| Kind | Purpose | Replaceable? | Key tag | +|---|---|---|---| +| `31123` | Public skill definition | Yes (d-tag) | `d=` | +| `31124` | Private skill definition | Yes (d-tag) | `d=` | +| `10123` | Public skill adoption list | Yes (replaceable) | `a` refs to 31123 events | + +All skill events carry these standard tags: +- `["d", ""]` — unique identifier within the author's pubkey +- `["app", "didactyl"]` — app namespace +- `["scope", "public"]` or `["scope", "private"]` + +--- + +## Tool Family + +### 1. `skill_create` + +**Purpose:** Create or update a skill definition and optionally auto-adopt it. + +**OpenAI schema:** + +```json +{ + "name": "skill_create", + "description": "Create or update a skill definition (kind 31123 public / 31124 private) and optionally auto-adopt it", + "parameters": { + "type": "object", + "properties": { + "slug": { "type": "string", "description": "Unique skill identifier (lowercase, hyphens allowed)" }, + "content": { "type": "string", "description": "Skill body — markdown instructions or structured JSON" }, + "scope": { "type": "string", "description": "public (kind 31123) or private (kind 31124). Default: public" }, + "description": { "type": "string", "description": "Short one-line description for the skill" }, + "auto_adopt": { "type": "boolean", "description": "Automatically add to adoption list (kind 10123). Default: true" } + }, + "required": ["slug", "content"] + } +} +``` + +**Execution logic (`execute_skill_create`):** + +1. Validate `slug` — must be non-empty, lowercase alphanumeric + hyphens, no spaces +2. Determine kind: `31123` if scope is `"public"` or absent; `31124` if `"private"` +3. Build tags array: + - `["d", slug]` + - `["app", "didactyl"]` + - `["scope", scope]` + - `["description", description]` if provided +4. Call `nostr_handler_publish_kind_event(kind, content, tags, &result)` +5. If `auto_adopt` is true (default), update the kind `10123` adoption list: + - Query existing `10123` event for own pubkey (same pattern as `execute_nostr_list_manage`) + - Add `["a", "31123::"]` tag if not already present + - Republish the updated `10123` event +6. Return JSON with `success`, `event_id`, `naddr_uri`, `slug`, `adopted` + +**Key design decisions:** +- Auto-adopt defaults to `true` — creating a skill you don't adopt is unusual +- Private skills (31124) are NOT added to the public adoption list (10123) +- Republishing with the same slug replaces the previous version (replaceable event) + +--- + +### 2. `skill_list` + +**Purpose:** List the agent's own published skills. + +**OpenAI schema:** + +```json +{ + "name": "skill_list", + "description": "List skills published by this agent, optionally filtered by scope", + "parameters": { + "type": "object", + "properties": { + "scope": { "type": "string", "description": "Filter by public or private. Omit for both." } + } + } +} +``` + +**Execution logic (`execute_skill_list`):** + +1. Build filter based on scope: + - Both: `{"kinds": [31123, 31124], "authors": [own_pubkey]}` + - Public only: `{"kinds": [31123], "authors": [own_pubkey]}` + - Private only: `{"kinds": [31124], "authors": [own_pubkey]}` +2. Call `nostr_handler_query_json(filter, 8000)` +3. Parse results, extract for each event: + - `slug` (from d-tag) + - `kind` + - `scope` (from scope tag) + - `description` (from description tag, if present) + - `created_at` timestamp + - `content` preview (first 200 chars) +4. Return JSON array of skill summaries + +--- + +### 3. `skill_adopt` + +**Purpose:** Adopt a skill published by another author (or self) into the agent's adoption list. + +**OpenAI schema:** + +```json +{ + "name": "skill_adopt", + "description": "Add a skill to the agent's public adoption list (kind 10123)", + "parameters": { + "type": "object", + "properties": { + "pubkey": { "type": "string", "description": "Hex pubkey of the skill author" }, + "slug": { "type": "string", "description": "Skill slug (d-tag value)" }, + "kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" } + }, + "required": ["pubkey", "slug"] + } +} +``` + +**Execution logic (`execute_skill_adopt`):** + +1. Validate pubkey (64-char hex) and slug (non-empty) +2. Default kind to 31123 if not provided +3. Build the `a`-tag value: `"::"` +4. Query existing kind `10123` event for own pubkey +5. Check if `["a", "::"]` already exists — if so, return success with `already_adopted: true` +6. Add the tag, republish `10123` +7. Return JSON with `success`, `adopted_address`, `event_id` + +--- + +### 4. `skill_remove` + +**Purpose:** Remove a skill from the agent's adoption list. + +**OpenAI schema:** + +```json +{ + "name": "skill_remove", + "description": "Remove a skill from the agent's public adoption list (kind 10123)", + "parameters": { + "type": "object", + "properties": { + "pubkey": { "type": "string", "description": "Hex pubkey of the skill author. Defaults to own pubkey." }, + "slug": { "type": "string", "description": "Skill slug to remove" }, + "kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" } + }, + "required": ["slug"] + } +} +``` + +**Execution logic (`execute_skill_remove`):** + +1. Default pubkey to own pubkey if not provided +2. Default kind to 31123 +3. Build the `a`-tag value: `"::"` +4. Query existing kind `10123` event for own pubkey +5. Find and remove matching `["a", ...]` tag +6. Republish `10123` +7. Return JSON with `success`, `removed_address`, `event_id` + +--- + +### 5. `skill_search` + +**Purpose:** Search for skills across the agent's Web of Trust. + +**OpenAI schema:** + +```json +{ + "name": "skill_search", + "description": "Search for skills adopted by Web of Trust contacts, or query public skill definitions", + "parameters": { + "type": "object", + "properties": { + "query": { "type": "string", "description": "Optional keyword to filter skill slugs or descriptions" }, + "pubkey": { "type": "string", "description": "Search skills by a specific author pubkey" }, + "popular": { "type": "boolean", "description": "If true, query WoT adoption lists to find most-adopted skills" } + } + } +} +``` + +**Execution logic (`execute_skill_search`):** + +1. If `popular` is true: + - Query `{"kinds": [10123]}` from relays (with reasonable limit) + - Parse all `a`-tags from results + - Count occurrences of each `a`-tag address + - Sort by adoption count descending + - Return top N skill addresses with counts +2. If `pubkey` is provided: + - Query `{"kinds": [31123], "authors": [pubkey]}` + - Return skill summaries +3. If `query` is provided (keyword search): + - Query `{"kinds": [31123]}` with limit + - Filter results client-side by matching `query` against slug, description tag, or content + - Return matching skill summaries +4. Default (no params): return own adopted skills from `10123` + +--- + +## Implementation Architecture + +### Shared helper: adoption list update + +Since `skill_create`, `skill_adopt`, and `skill_remove` all modify the kind `10123` list, extract a shared helper: + +```c +// Fetch current 10123 event, return duplicated tags array (or empty array if none exists) +static cJSON* fetch_adoption_list_tags(tools_context_t* ctx); + +// Publish updated 10123 event with new tags +static int publish_adoption_list(tools_context_t* ctx, cJSON* tags, nostr_publish_result_t* result); +``` + +This is essentially the same pattern already used in `execute_nostr_list_manage()` but specialized for kind `10123`. + +### Shared helper: skill summary extraction + +```c +// Extract slug, kind, scope, description, created_at from a skill event JSON +static cJSON* extract_skill_summary(cJSON* event); +``` + +### Flow diagram + +```mermaid +flowchart TD + SC[skill_create] --> PUB[nostr_handler_publish_kind_event] + SC --> ADOPT_HELPER[update adoption list helper] + + SL[skill_list] --> QUERY[nostr_handler_query_json] + SL --> SUMMARY[extract_skill_summary] + + SA[skill_adopt] --> ADOPT_HELPER + SR[skill_remove] --> ADOPT_HELPER + + SS[skill_search] --> QUERY + SS --> SUMMARY + + ADOPT_HELPER --> QUERY + ADOPT_HELPER --> PUB +``` + +--- + +## Changes Required + +### `src/tools.c` + +1. **Schema registration** — Add 5 new tool definitions in `tools_build_openai_schema_json()` (t22–t26) +2. **Execution functions** — Add 5 new `execute_skill_*()` static functions +3. **Dispatch** — Add 5 new `strcmp` branches in `tools_execute()` +4. **Shared helpers** — Add `fetch_adoption_list_tags()`, `publish_adoption_list()`, `extract_skill_summary()`, and `validate_skill_slug()` + +### `README.md` + +1. Add skill tools to the Tooling Interface section under a new "Skill management" category + +### No changes needed to: +- `src/tools.h` — the `tools_context_t` already has `cfg` which provides `keys.public_key_hex` +- `src/nostr_handler.h` — all needed APIs already exist +- `src/config.h` — no new config fields needed + +--- + +## Slug Validation Rules + +A valid skill slug must: +- Be 1–64 characters +- Contain only lowercase letters, digits, and hyphens +- Not start or end with a hyphen +- Not contain consecutive hyphens + +```c +static int validate_skill_slug(const char* slug) { + if (!slug || slug[0] == '\0' || strlen(slug) > 64) return 0; + if (slug[0] == '-') return 0; + int prev_dash = 0; + for (size_t i = 0; slug[i]; i++) { + char c = slug[i]; + if (c == '-') { + if (prev_dash) return 0; + prev_dash = 1; + } else if (islower(c) || isdigit(c)) { + prev_dash = 0; + } else { + return 0; + } + } + if (slug[strlen(slug) - 1] == '-') return 0; + return 1; +} +``` + +--- + +## Implementation Order + +1. **Shared helpers** — `validate_skill_slug`, `fetch_adoption_list_tags`, `publish_adoption_list`, `extract_skill_summary` +2. **`skill_create`** — most important tool, enables the agent to author skills +3. **`skill_list`** — lets the agent see what it has published +4. **`skill_adopt`** — adopt skills from other authors +5. **`skill_remove`** — remove skills from adoption list +6. **`skill_search`** — discover skills across WoT +7. **Schema registration** — add all 5 tools to `tools_build_openai_schema_json()` +8. **Dispatch wiring** — add all 5 to `tools_execute()` +9. **README update** — document the new tools +10. **Build and test** — verify compilation and basic tool execution + +--- + +## Security Considerations + +- **Admin-only**: Skill tools inherit the existing ADMIN tier restriction — only the admin can trigger tool calls +- **Slug validation**: Prevents injection of malformed d-tags +- **No arbitrary kind**: `skill_create` only publishes kind 31123 or 31124, not arbitrary kinds +- **Adoption list integrity**: The helpers always fetch-then-update to avoid clobbering existing adoption entries +- **Content size**: No explicit limit on skill content size — relies on relay limits and LLM context window constraints diff --git a/plans/tool_testing.md b/plans/tool_testing.md new file mode 100644 index 0000000..2f59174 --- /dev/null +++ b/plans/tool_testing.md @@ -0,0 +1,139 @@ +# Didactyl Tool Testing Plan + +## Overview + +Testing agent-mediated tools is different from unit testing pure functions because the execution path spans multiple boundaries: + +``` +LLM schema interpretation → JSON argument generation → argument parsing → business logic → Nostr/network I/O → JSON response → LLM interpretation +``` + +This plan defines three testing layers, ordered by implementation priority. + +--- + +## Layer 1: Direct Tool Execution + +Call `tools_execute()` directly with known JSON arguments and assert the JSON response structure. + +### Implementation + +Add a `--test-tool ` CLI flag to `src/main.c` that: +1. Initializes config and nostr_handler (for network-dependent tools) +2. Calls `tools_execute(&ctx, name, args_json)` +3. Prints the raw JSON result to stdout +4. Exits with 0 if `success: true`, 1 otherwise + +### Pure Computation Tools (no network needed) + +| Tool | Test Command | Expected | +|------|-------------|----------| +| `nostr_encode` | `--test-tool nostr_encode '{"type":"npub","hex":"<64-char-hex>"}'` | `success: true`, uri starts with `nostr:npub1` | +| `nostr_decode` | `--test-tool nostr_decode '{"uri":"npub1..."}'` | `success: true`, pubkey is 64-char hex | +| `nostr_encrypt` | `--test-tool nostr_encrypt '{"recipient_pubkey":"","plaintext":"hello"}'` | `success: true`, ciphertext is base64 | +| `nostr_decrypt` | `--test-tool nostr_decrypt '{"sender_pubkey":"","ciphertext":""}'` | `success: true`, plaintext is "hello" | + +### Network-Dependent Tools (need relay or HTTP) + +| Tool | Test Command | Expected | +|------|-------------|----------| +| `nostr_nip05_lookup` | `--test-tool nostr_nip05_lookup '{"identifier":"_@laantungir.com"}'` | `success: true`, pubkey returned | +| `nostr_relay_info` | `--test-tool nostr_relay_info '{"relay_url":"wss://relay.damus.io"}'` | `success: true`, info.basic.name present | +| `nostr_relay_status` | `--test-tool nostr_relay_status '{}'` | `success: true`, relay_count > 0 | +| `nostr_dm_send` | `--test-tool nostr_dm_send '{"recipient_pubkey":"","message":"test"}'` | `success: true` | +| `nostr_post` | `--test-tool nostr_post '{"kind":1,"content":"test"}'` | `success: true`, event_id present | +| `nostr_delete` | `--test-tool nostr_delete '{"event_ids":["<64-char-hex>"]}'` | `success: true` | +| `nostr_react` | `--test-tool nostr_react '{"event_id":"","event_pubkey":""}'` | `success: true` | +| `nostr_profile_get` | `--test-tool nostr_profile_get '{"pubkey":""}'` | `success: true`, found: true/false | +| `nostr_dm_send_nip17` | `--test-tool nostr_dm_send_nip17 '{"recipient_pubkey":"","message":"test"}'` | `success: true` | +| `nostr_list_manage` | `--test-tool nostr_list_manage '{"list_kind":10000,"action":"add","items":[["p",""]]}'` | `success: true` | + +### Error Case Tests + +Each tool should also be tested with: +- Empty args: `'{}'` — should return descriptive error +- Missing required fields — should return specific error message +- Invalid hex strings — should reject gracefully +- Double-encoded JSON string args — should parse correctly (regression for the `invalid arguments JSON` bug) + +--- + +## Layer 2: Schema-Parse Fidelity + +Validates that the OpenAI function schema matches what executors actually accept. + +### Implementation + +A Python script (`tests/validate_schemas.py`) that: +1. Runs `didactyl_static --dump-schemas` (new flag) to get the tool schema JSON +2. For each tool in the schema: + - Generates a minimal valid argument payload from `required` + `properties` + - Runs `didactyl_static --test-tool ''` + - Asserts exit code 0 or expected network error +3. Reports mismatches between schema and executor expectations + +### What This Catches + +- Schema says `required: ["hex"]` but executor checks for `"pubkey"` — mismatch +- Schema says `type: "integer"` but executor reads it as string +- Schema advertises parameters the executor ignores +- Missing required parameters in schema that executor demands + +--- + +## Layer 3: Agent Integration Testing (Live DM) + +The most natural test — DM the running agent and verify it uses tools correctly. + +### Prerequisites + +- Local relay running at `ws://127.0.0.1:7777` +- Didactyl running with valid config +- A separate Nostr client (or script) to send/receive DMs + +### Test Prompts + +| # | Tool | DM Prompt | Assert in Response | +|---|------|-----------|-------------------| +| 1 | `nostr_encode` | "Encode this pubkey as npub: `<64-char hex>`" | Contains `nostr:npub1` | +| 2 | `nostr_decode` | "Decode this npub: `npub1...`" | Contains the hex pubkey | +| 3 | `nostr_dm_send` | "Send a DM to `` saying 'hello test'" | Confirms sent; you receive it | +| 4 | `nostr_encrypt` | "Encrypt 'secret message' for ``" | Contains base64 ciphertext | +| 5 | `nostr_decrypt` | "Decrypt this NIP-44 payload: ``" | Contains 'secret message' | +| 6 | `nostr_nip05_lookup` | "Look up `_@laantungir.com`" | Returns a pubkey | +| 7 | `nostr_relay_info` | "Get NIP-11 info for `wss://relay.damus.io`" | Returns relay name and supported NIPs | +| 8 | `nostr_relay_status` | "Show me relay connection status" | Lists connected relays with stats | +| 9 | `nostr_react` | "React with 🤙 to event `` from ``" | Confirms kind 7 published | +| 10 | `nostr_delete` | "Delete event ``" | Confirms kind 5 published | +| 11 | `nostr_profile_get` | "Look up the profile for ``" | Returns name/about/picture | +| 12 | `nostr_post` | "Post a kind 1 note saying 'tool test'" | Confirms published with event_id | +| 13 | `nostr_list_manage` | "Add `` to my mute list" | Confirms kind 10000 published | +| 14 | `nostr_dm_send_nip17` | "Send a private NIP-17 DM to `` saying 'gift wrap test'" | Confirms gift wrap sent | +| 15 | `nostr_post_readme` | "Publish the README to Nostr" | Confirms kind 30023 with d=readme.md | + +### Verification Methods + +- **stdout logs**: Watch `[didactyl] executing tool call: ` in terminal +- **context.log**: Full LLM conversation including tool calls and results +- **Relay inspection**: Query the local relay for published events +- **DM receipt**: For DM tools, verify the message arrives at the recipient + +--- + +## Implementation Priority + +1. **Immediate (no code changes)**: Run Layer 3 tests by DMing the live agent +2. **Next sprint**: Add `--test-tool` CLI flag for Layer 1 +3. **Later**: Add `--dump-schemas` flag and Python schema validator for Layer 2 +4. **CI integration**: Wrap Layer 1 pure-computation tests in a shell script that runs after `build_static.sh` + +--- + +## Local Relay Setup for Testing + +A local relay like `strfry` or `nostr-rs-relay` at `ws://127.0.0.1:7777` provides: +- All published events are observable and queryable +- No rate limits or content policies +- NIP-42 auth testing in isolation +- Gift-wrapped NIP-17 DMs stay in your test environment +- Database can be wiped between test runs diff --git a/src/main.h b/src/main.h index 9b0cb75..8704b57 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 20 -#define DIDACTYL_VERSION "v0.0.20" +#define DIDACTYL_VERSION_PATCH 21 +#define DIDACTYL_VERSION "v0.0.21" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/tools.c b/src/tools.c index 204310d..e2c4bd1 100644 --- a/src/tools.c +++ b/src/tools.c @@ -2,6 +2,7 @@ #include "tools.h" +#include #include #include #include @@ -9,8 +10,10 @@ #include #include #include +#include #include "cjson/cJSON.h" +#include "main.h" #include "nostr_handler.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" @@ -555,6 +558,71 @@ static cJSON* parse_tool_args_json(const char* args_json) { return args; } +typedef struct { + char* data; + size_t len; + size_t cap; + size_t max_bytes; + int truncated; +} http_fetch_buffer_t; + +static size_t http_fetch_write_cb(void* contents, size_t size, size_t nmemb, void* userp) { + http_fetch_buffer_t* rb = (http_fetch_buffer_t*)userp; + size_t total = size * nmemb; + if (!rb || total == 0) return total; + + if (rb->len >= rb->max_bytes) { + rb->truncated = 1; + return total; + } + + size_t allowed = rb->max_bytes - rb->len; + size_t to_copy = total <= allowed ? total : allowed; + + if (rb->len + to_copy + 1U > rb->cap) { + size_t new_cap = rb->cap == 0 ? 1024U : rb->cap; + while (new_cap < rb->len + to_copy + 1U) { + new_cap *= 2U; + } + char* bigger = (char*)realloc(rb->data, new_cap); + if (!bigger) return 0; + rb->data = bigger; + rb->cap = new_cap; + } + + memcpy(rb->data + rb->len, contents, to_copy); + rb->len += to_copy; + rb->data[rb->len] = '\0'; + + if (to_copy < total) { + rb->truncated = 1; + } + + return total; +} + +static const char* detect_ca_bundle_path_for_tools(void) { + const char* env = getenv("SSL_CERT_FILE"); + if (env && env[0] != '\0' && access(env, R_OK) == 0) { + return env; + } + + static const char* candidates[] = { + "/etc/ssl/certs/ca-certificates.crt", + "/etc/ssl/cert.pem", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem" + }; + + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + if (access(candidates[i], R_OK) == 0) { + return candidates[i]; + } + } + + return NULL; +} + static void free_string_array_heap(char** arr, int count) { if (!arr) return; for (int i = 0; i < count; i++) { @@ -605,6 +673,211 @@ static int remove_matching_tag_tuples(cJSON* tags, cJSON* tuple) { return removed; } +static cJSON* find_tag_value_string(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_slug(const char* slug) { + if (!slug) return 0; + size_t len = strlen(slug); + if (len == 0 || len > 64U) return 0; + if (slug[0] == '-' || slug[len - 1] == '-') return 0; + + int prev_dash = 0; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)slug[i]; + if (c == '-') { + if (prev_dash) return 0; + prev_dash = 1; + continue; + } + if (!islower(c) && !isdigit(c)) return 0; + prev_dash = 0; + } + + return 1; +} + +static int ci_contains(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(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; + + 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); + return -1; + } + + cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex)); + cJSON_AddItemToObject(filter, "authors", authors); + cJSON_AddNumberToObject(filter, "limit", 1); + + char* events_json = nostr_handler_query_json(filter, 8000); + cJSON_Delete(filter); + + 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(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 cJSON* extract_skill_summary(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", kind->valuedouble); + } + if (created_at && cJSON_IsNumber(created_at)) { + cJSON_AddNumberToObject(summary, "created_at", created_at->valuedouble); + } + 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(tags, "d"); + cJSON* scope = find_tag_value_string(tags, "scope"); + cJSON* description = find_tag_value_string(tags, "description"); + + if (d && cJSON_IsString(d) && d->valuestring) { + cJSON_AddStringToObject(summary, "slug", 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; +} + char* tools_build_openai_schema_json(const tools_context_t* ctx) { (void)ctx; @@ -1132,6 +1405,206 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) { cJSON_AddItemToObject(t19, "function", t19_fn); cJSON_AddItemToArray(tools, t19); + cJSON* t20 = cJSON_CreateObject(); + cJSON* t20_fn = cJSON_CreateObject(); + cJSON* t20_params = cJSON_CreateObject(); + cJSON* t20_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t20, "type", "function"); + cJSON_AddStringToObject(t20_fn, "name", "my_version"); + cJSON_AddStringToObject(t20_fn, "description", "Return current Didactyl version and metadata from build macros"); + cJSON_AddStringToObject(t20_params, "type", "object"); + cJSON_AddItemToObject(t20_params, "properties", t20_props); + + cJSON_AddItemToObject(t20_fn, "parameters", t20_params); + cJSON_AddItemToObject(t20, "function", t20_fn); + cJSON_AddItemToArray(tools, t20); + + cJSON* t21 = cJSON_CreateObject(); + cJSON* t21_fn = cJSON_CreateObject(); + cJSON* t21_params = cJSON_CreateObject(); + cJSON* t21_props = cJSON_CreateObject(); + cJSON* t21_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t21, "type", "function"); + cJSON_AddStringToObject(t21_fn, "name", "http_fetch"); + cJSON_AddStringToObject(t21_fn, "description", "Fetch HTTP(S) resources with optional method, headers, timeout, and body"); + cJSON_AddStringToObject(t21_params, "type", "object"); + cJSON_AddItemToObject(t21_params, "properties", t21_props); + cJSON_AddItemToObject(t21_params, "required", t21_required); + + cJSON* p_http_url = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_url, "type", "string"); + cJSON_AddItemToObject(t21_props, "url", p_http_url); + + cJSON* p_http_method = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_method, "type", "string"); + cJSON_AddItemToObject(t21_props, "method", p_http_method); + + cJSON* p_http_headers = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_headers, "type", "array"); + cJSON* p_http_headers_item = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_headers_item, "type", "string"); + cJSON_AddItemToObject(p_http_headers, "items", p_http_headers_item); + cJSON_AddItemToObject(t21_props, "headers", p_http_headers); + + cJSON* p_http_body = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_body, "type", "string"); + cJSON_AddItemToObject(t21_props, "body", p_http_body); + + cJSON* p_http_timeout = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_timeout, "type", "integer"); + cJSON_AddItemToObject(t21_props, "timeout_seconds", p_http_timeout); + + cJSON* p_http_max_bytes = cJSON_CreateObject(); + cJSON_AddStringToObject(p_http_max_bytes, "type", "integer"); + cJSON_AddItemToObject(t21_props, "max_bytes", p_http_max_bytes); + + cJSON_AddItemToArray(t21_required, cJSON_CreateString("url")); + + cJSON_AddItemToObject(t21_fn, "parameters", t21_params); + cJSON_AddItemToObject(t21, "function", t21_fn); + cJSON_AddItemToArray(tools, t21); + + cJSON* t22 = cJSON_CreateObject(); + cJSON* t22_fn = cJSON_CreateObject(); + cJSON* t22_params = cJSON_CreateObject(); + cJSON* t22_props = cJSON_CreateObject(); + cJSON* t22_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t22, "type", "function"); + cJSON_AddStringToObject(t22_fn, "name", "skill_create"); + cJSON_AddStringToObject(t22_fn, "description", "Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it"); + cJSON_AddStringToObject(t22_params, "type", "object"); + cJSON_AddItemToObject(t22_params, "properties", t22_props); + cJSON_AddItemToObject(t22_params, "required", t22_required); + + cJSON* p_skill_create_slug = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_slug, "type", "string"); + cJSON_AddItemToObject(t22_props, "slug", p_skill_create_slug); + cJSON* p_skill_create_content = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_content, "type", "string"); + cJSON_AddItemToObject(t22_props, "content", p_skill_create_content); + cJSON* p_skill_create_scope = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_scope, "type", "string"); + cJSON_AddItemToObject(t22_props, "scope", p_skill_create_scope); + cJSON* p_skill_create_desc = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_desc, "type", "string"); + cJSON_AddItemToObject(t22_props, "description", p_skill_create_desc); + cJSON* p_skill_create_auto = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_auto, "type", "boolean"); + cJSON_AddItemToObject(t22_props, "auto_adopt", p_skill_create_auto); + + cJSON_AddItemToArray(t22_required, cJSON_CreateString("slug")); + cJSON_AddItemToArray(t22_required, cJSON_CreateString("content")); + + cJSON_AddItemToObject(t22_fn, "parameters", t22_params); + cJSON_AddItemToObject(t22, "function", t22_fn); + cJSON_AddItemToArray(tools, t22); + + cJSON* t23 = cJSON_CreateObject(); + cJSON* t23_fn = cJSON_CreateObject(); + cJSON* t23_params = cJSON_CreateObject(); + cJSON* t23_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t23, "type", "function"); + cJSON_AddStringToObject(t23_fn, "name", "skill_list"); + cJSON_AddStringToObject(t23_fn, "description", "List this agent's published skills, optionally filtered by scope"); + cJSON_AddStringToObject(t23_params, "type", "object"); + cJSON_AddItemToObject(t23_params, "properties", t23_props); + + cJSON* p_skill_list_scope = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_list_scope, "type", "string"); + cJSON_AddItemToObject(t23_props, "scope", p_skill_list_scope); + + cJSON_AddItemToObject(t23_fn, "parameters", t23_params); + cJSON_AddItemToObject(t23, "function", t23_fn); + cJSON_AddItemToArray(tools, t23); + + cJSON* t24 = cJSON_CreateObject(); + cJSON* t24_fn = cJSON_CreateObject(); + cJSON* t24_params = cJSON_CreateObject(); + cJSON* t24_props = cJSON_CreateObject(); + cJSON* t24_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t24, "type", "function"); + cJSON_AddStringToObject(t24_fn, "name", "skill_adopt"); + cJSON_AddStringToObject(t24_fn, "description", "Adopt a skill by adding its address to kind 10123 adoption list"); + cJSON_AddStringToObject(t24_params, "type", "object"); + cJSON_AddItemToObject(t24_params, "properties", t24_props); + cJSON_AddItemToObject(t24_params, "required", t24_required); + + cJSON* p_skill_adopt_pubkey = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_adopt_pubkey, "type", "string"); + cJSON_AddItemToObject(t24_props, "pubkey", p_skill_adopt_pubkey); + cJSON* p_skill_adopt_slug = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_adopt_slug, "type", "string"); + cJSON_AddItemToObject(t24_props, "slug", p_skill_adopt_slug); + cJSON* p_skill_adopt_kind = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_adopt_kind, "type", "integer"); + cJSON_AddItemToObject(t24_props, "kind", p_skill_adopt_kind); + + cJSON_AddItemToArray(t24_required, cJSON_CreateString("pubkey")); + cJSON_AddItemToArray(t24_required, cJSON_CreateString("slug")); + + cJSON_AddItemToObject(t24_fn, "parameters", t24_params); + cJSON_AddItemToObject(t24, "function", t24_fn); + cJSON_AddItemToArray(tools, t24); + + cJSON* t25 = cJSON_CreateObject(); + cJSON* t25_fn = cJSON_CreateObject(); + cJSON* t25_params = cJSON_CreateObject(); + cJSON* t25_props = cJSON_CreateObject(); + cJSON* t25_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t25, "type", "function"); + cJSON_AddStringToObject(t25_fn, "name", "skill_remove"); + cJSON_AddStringToObject(t25_fn, "description", "Remove a skill address from kind 10123 adoption list"); + cJSON_AddStringToObject(t25_params, "type", "object"); + cJSON_AddItemToObject(t25_params, "properties", t25_props); + cJSON_AddItemToObject(t25_params, "required", t25_required); + + cJSON* p_skill_remove_pubkey = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_remove_pubkey, "type", "string"); + cJSON_AddItemToObject(t25_props, "pubkey", p_skill_remove_pubkey); + cJSON* p_skill_remove_slug = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_remove_slug, "type", "string"); + cJSON_AddItemToObject(t25_props, "slug", p_skill_remove_slug); + cJSON* p_skill_remove_kind = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_remove_kind, "type", "integer"); + cJSON_AddItemToObject(t25_props, "kind", p_skill_remove_kind); + + cJSON_AddItemToArray(t25_required, cJSON_CreateString("slug")); + + cJSON_AddItemToObject(t25_fn, "parameters", t25_params); + cJSON_AddItemToObject(t25, "function", t25_fn); + cJSON_AddItemToArray(tools, t25); + + cJSON* t26 = cJSON_CreateObject(); + cJSON* t26_fn = cJSON_CreateObject(); + cJSON* t26_params = cJSON_CreateObject(); + cJSON* t26_props = cJSON_CreateObject(); + + cJSON_AddStringToObject(t26, "type", "function"); + cJSON_AddStringToObject(t26_fn, "name", "skill_search"); + cJSON_AddStringToObject(t26_fn, "description", "Search public skills by query/author and optionally rank by adoption popularity"); + cJSON_AddStringToObject(t26_params, "type", "object"); + cJSON_AddItemToObject(t26_params, "properties", t26_props); + + cJSON* p_skill_search_query = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_search_query, "type", "string"); + cJSON_AddItemToObject(t26_props, "query", p_skill_search_query); + cJSON* p_skill_search_pubkey = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_search_pubkey, "type", "string"); + cJSON_AddItemToObject(t26_props, "pubkey", p_skill_search_pubkey); + cJSON* p_skill_search_popular = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_search_popular, "type", "boolean"); + cJSON_AddItemToObject(t26_props, "popular", p_skill_search_popular); + + cJSON_AddItemToObject(t26_fn, "parameters", t26_params); + cJSON_AddItemToObject(t26, "function", t26_fn); + cJSON_AddItemToArray(tools, t26); + char* out = cJSON_PrintUnformatted(tools); cJSON_Delete(tools); return out; @@ -2278,6 +2751,645 @@ static char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_js return json; } +static char* execute_skill_create(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug"); + 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"); + + if (!slug || !cJSON_IsString(slug) || !slug->valuestring || + !content || !cJSON_IsString(content) || !content->valuestring) { + cJSON_Delete(args); + return json_error("skill_create requires slug and content strings"); + } + if (!validate_skill_slug(slug->valuestring)) { + cJSON_Delete(args); + return json_error("skill_create slug must be lowercase letters/digits/hyphens (1-64 chars)"); + } + if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) { + cJSON_Delete(args); + return json_error("skill_create scope must be string when provided"); + } + if (description && (!cJSON_IsString(description) || !description->valuestring)) { + cJSON_Delete(args); + return json_error("skill_create description must be string 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("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("failed to create skill tags"); + } + + if (add_string_tag(tags, "d", slug->valuestring) != 0 || + add_string_tag(tags, "app", "didactyl") != 0 || + add_string_tag(tags, "scope", scope_str) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add required skill tags"); + } + + if (description && description->valuestring && description->valuestring[0] != '\0') { + if (add_string_tag(tags, "description", description->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(args); + return json_error("failed to add description 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("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(ctx, &adoption_tags, &adoption_content) == 0) { + char addr[256]; + snprintf(addr, sizeof(addr), "31123:%s:%s", ctx->cfg->keys.public_key_hex, slug->valuestring); + + cJSON* tuple = cJSON_CreateArray(); + if (tuple) { + cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); + cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); + + if (tags_contains_tuple(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(adoption_content, adoption_tags, &adoption_publish) == 0) { + adopted = 1; + } + } + } + + cJSON_Delete(tuple); + } + + cJSON_Delete(adoption_tags); + free(adoption_content); + } + } + + 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, "slug", slug->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); + + 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; +} + +static char* execute_skill_list(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope"); + if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) { + cJSON_Delete(args); + return json_error("skill_list scope must be string 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("skill_list scope must be public or private"); + } + } + + 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 skill_list filter"); + } + + if (include_public) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123)); + if (include_private) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex)); + 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("skill_list query failed"); + + cJSON* events = cJSON_Parse(events_json); + free(events_json); + if (!events || !cJSON_IsArray(events)) { + cJSON_Delete(events); + return json_error("skill_list returned invalid JSON"); + } + + cJSON* out = cJSON_CreateObject(); + cJSON* skills = cJSON_CreateArray(); + if (!out || !skills) { + cJSON_Delete(out); + cJSON_Delete(skills); + cJSON_Delete(events); + return NULL; + } + + int event_count = cJSON_GetArraySize(events); + for (int i = 0; i < event_count; i++) { + cJSON* ev = cJSON_GetArrayItem(events, i); + cJSON* summary = extract_skill_summary(ev); + if (summary) cJSON_AddItemToArray(skills, summary); + } + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddItemToObject(out, "skills", skills); + cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills)); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + cJSON_Delete(events); + return json; +} + +static char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); + cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug"); + cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind"); + + if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U) || + !slug || !cJSON_IsString(slug) || !slug->valuestring || !validate_skill_slug(slug->valuestring)) { + cJSON_Delete(args); + return json_error("skill_adopt requires pubkey (hex) and valid slug"); + } + + int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123; + if (kind_val != 31123 && kind_val != 31124) { + cJSON_Delete(args); + return json_error("skill_adopt kind must be 31123 or 31124"); + } + + char addr[256]; + snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey->valuestring, slug->valuestring); + + cJSON* tags = NULL; + char* content = NULL; + if (fetch_adoption_list_tags(ctx, &tags, &content) != 0) { + cJSON_Delete(args); + return json_error("skill_adopt failed to load adoption list"); + } + + cJSON* tuple = cJSON_CreateArray(); + if (!tuple) { + cJSON_Delete(tags); + free(content); + cJSON_Delete(args); + return json_error("allocation failure"); + } + cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); + cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); + + int already_adopted = tags_contains_tuple(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("failed to duplicate adoption tuple"); + } + cJSON_AddItemToArray(tags, dup); + if (publish_adoption_list(content, tags, &publish_result) != 0) { + cJSON_Delete(tuple); + cJSON_Delete(tags); + free(content); + cJSON_Delete(args); + return json_error("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; +} + +static char* execute_skill_remove(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey"); + cJSON* slug = cJSON_GetObjectItemCaseSensitive(args, "slug"); + 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(pubkey->valuestring, 64U)) { + cJSON_Delete(args); + return json_error("skill_remove pubkey must be 64-char hex when provided"); + } + pubkey_str = pubkey->valuestring; + } + + if (!slug || !cJSON_IsString(slug) || !slug->valuestring || !validate_skill_slug(slug->valuestring)) { + cJSON_Delete(args); + return json_error("skill_remove requires valid slug"); + } + + int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123; + if (kind_val != 31123 && kind_val != 31124) { + cJSON_Delete(args); + return json_error("skill_remove kind must be 31123 or 31124"); + } + + char addr[256]; + snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey_str, slug->valuestring); + + cJSON* tags = NULL; + char* content = NULL; + if (fetch_adoption_list_tags(ctx, &tags, &content) != 0) { + cJSON_Delete(args); + return json_error("skill_remove failed to load adoption list"); + } + + cJSON* tuple = cJSON_CreateArray(); + if (!tuple) { + cJSON_Delete(tags); + free(content); + cJSON_Delete(args); + return json_error("allocation failure"); + } + cJSON_AddItemToArray(tuple, cJSON_CreateString("a")); + cJSON_AddItemToArray(tuple, cJSON_CreateString(addr)); + + int removed = remove_matching_tag_tuples(tags, tuple); + + nostr_publish_result_t publish_result; + memset(&publish_result, 0, sizeof(publish_result)); + + if (removed > 0) { + if (publish_adoption_list(content, tags, &publish_result) != 0) { + cJSON_Delete(tuple); + cJSON_Delete(tags); + free(content); + cJSON_Delete(args); + return json_error("skill_remove 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, "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; +} + +static char* execute_skill_search(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("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("skill_search query must be string when provided"); + } + if (pubkey && (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U))) { + cJSON_Delete(args); + return json_error("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("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("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("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(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("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("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("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("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("skill_search query failed"); + + cJSON* events = cJSON_Parse(events_json); + free(events_json); + if (!events || !cJSON_IsArray(events)) { + cJSON_Delete(events); + return json_error("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(ev); + if (!summary) continue; + + if (query_str && query_str[0] != '\0') { + cJSON* slug = cJSON_GetObjectItemCaseSensitive(summary, "slug"); + cJSON* desc = cJSON_GetObjectItemCaseSensitive(summary, "description"); + cJSON* preview = cJSON_GetObjectItemCaseSensitive(summary, "content_preview"); + int match = 0; + if (slug && cJSON_IsString(slug) && slug->valuestring && ci_contains(slug->valuestring, query_str)) match = 1; + if (!match && desc && cJSON_IsString(desc) && desc->valuestring && ci_contains(desc->valuestring, query_str)) match = 1; + if (!match && preview && cJSON_IsString(preview) && preview->valuestring && ci_contains(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; +} + 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"); @@ -2322,6 +3434,172 @@ static char* execute_nostr_query(const char* args_json) { return json; } +static char* execute_my_version(const char* args_json) { + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + cJSON_Delete(args); + + cJSON* out = cJSON_CreateObject(); + if (!out) return NULL; + + cJSON_AddBoolToObject(out, "success", 1); + cJSON_AddStringToObject(out, "name", DIDACTYL_NAME); + cJSON_AddStringToObject(out, "description", DIDACTYL_DESCRIPTION); + cJSON_AddStringToObject(out, "software", DIDACTYL_SOFTWARE); + cJSON_AddStringToObject(out, "version", DIDACTYL_VERSION); + cJSON_AddNumberToObject(out, "version_major", DIDACTYL_VERSION_MAJOR); + cJSON_AddNumberToObject(out, "version_minor", DIDACTYL_VERSION_MINOR); + cJSON_AddNumberToObject(out, "version_patch", DIDACTYL_VERSION_PATCH); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + return json; +} + +static char* execute_http_fetch(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); + + cJSON* args = parse_tool_args_json(args_json); + if (!args) return json_error("invalid arguments JSON"); + + cJSON* url = cJSON_GetObjectItemCaseSensitive(args, "url"); + cJSON* method = cJSON_GetObjectItemCaseSensitive(args, "method"); + cJSON* headers = cJSON_GetObjectItemCaseSensitive(args, "headers"); + cJSON* body = cJSON_GetObjectItemCaseSensitive(args, "body"); + cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_seconds"); + cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes"); + + if (!url || !cJSON_IsString(url) || !url->valuestring || url->valuestring[0] == '\0') { + cJSON_Delete(args); + return json_error("http_fetch requires string url"); + } + if (headers && !cJSON_IsArray(headers)) { + cJSON_Delete(args); + return json_error("http_fetch headers must be an array when provided"); + } + if (body && !cJSON_IsString(body)) { + cJSON_Delete(args); + return json_error("http_fetch body must be a string when provided"); + } + + const char* method_str = (method && cJSON_IsString(method) && method->valuestring && method->valuestring[0] != '\0') + ? method->valuestring + : "GET"; + + if (body && cJSON_IsString(body) && body->valuestring && strcasecmp(method_str, "GET") == 0) { + cJSON_Delete(args); + return json_error("http_fetch GET requests cannot include body"); + } + + int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : 20; + if (timeout_seconds <= 0) timeout_seconds = 20; + if (timeout_seconds > 120) timeout_seconds = 120; + + int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536; + int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max; + if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max; + + CURL* curl = curl_easy_init(); + if (!curl) { + cJSON_Delete(args); + return json_error("http_fetch failed to initialize curl"); + } + + http_fetch_buffer_t rb; + memset(&rb, 0, sizeof(rb)); + rb.max_bytes = (size_t)max_bytes; + + struct curl_slist* req_headers = NULL; + req_headers = curl_slist_append(req_headers, "Accept: */*"); + + if (headers && cJSON_IsArray(headers)) { + int n = cJSON_GetArraySize(headers); + for (int i = 0; i < n; i++) { + cJSON* h = cJSON_GetArrayItem(headers, i); + if (h && cJSON_IsString(h) && h->valuestring && h->valuestring[0] != '\0') { + req_headers = curl_slist_append(req_headers, h->valuestring); + } + } + } + + curl_easy_setopt(curl, CURLOPT_URL, url->valuestring); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout_seconds); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_fetch_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "didactyl/http_fetch"); + + const char* ca_bundle = detect_ca_bundle_path_for_tools(); + if (ca_bundle) { + curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle); + } + + if (strcasecmp(method_str, "GET") == 0) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } else if (strcasecmp(method_str, "POST") == 0) { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + if (body && cJSON_IsString(body) && body->valuestring) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring)); + } + } else { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method_str); + if (body && cJSON_IsString(body) && body->valuestring) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring)); + } + } + + CURLcode res = curl_easy_perform(curl); + long status_code = 0; + char* content_type = NULL; + char* content_type_copy = NULL; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); + curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type); + if (content_type && content_type[0] != '\0') { + content_type_copy = strdup(content_type); + } + + curl_slist_free_all(req_headers); + curl_easy_cleanup(curl); + + cJSON* out = cJSON_CreateObject(); + if (!out) { + free(rb.data); + return NULL; + } + + int http_ok = (status_code >= 200 && status_code < 300) ? 1 : 0; + int success = (res == CURLE_OK && http_ok) ? 1 : 0; + + cJSON_AddBoolToObject(out, "success", success); + cJSON_AddStringToObject(out, "url", url->valuestring); + cJSON_AddStringToObject(out, "method", method_str); + cJSON_AddNumberToObject(out, "status_code", status_code); + cJSON_AddBoolToObject(out, "http_ok", http_ok); + cJSON_AddBoolToObject(out, "truncated", rb.truncated ? 1 : 0); + cJSON_AddNumberToObject(out, "bytes_received", (double)rb.len); + + if (content_type_copy && content_type_copy[0] != '\0') { + cJSON_AddStringToObject(out, "content_type", content_type_copy); + } + + if (res != CURLE_OK) { + cJSON_AddStringToObject(out, "curl_error", curl_easy_strerror(res)); + } + + cJSON_AddStringToObject(out, "body", rb.data ? rb.data : ""); + + free(rb.data); + + char* json = cJSON_PrintUnformatted(out); + cJSON_Delete(out); + cJSON_Delete(args); + free(content_type_copy); + return json; +} + static char* execute_shell_exec(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); if (!ctx->cfg->tools.shell.enabled) return json_error("shell tool disabled"); @@ -2532,6 +3810,12 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg if (strcmp(tool_name, "nostr_query") == 0) { return execute_nostr_query(args_json); } + if (strcmp(tool_name, "my_version") == 0) { + return execute_my_version(args_json); + } + if (strcmp(tool_name, "http_fetch") == 0) { + return execute_http_fetch(ctx, args_json); + } if (strcmp(tool_name, "shell_exec") == 0) { return execute_shell_exec(ctx, args_json); } @@ -2541,6 +3825,21 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg if (strcmp(tool_name, "file_write") == 0) { return execute_file_write(ctx, args_json); } + if (strcmp(tool_name, "skill_create") == 0) { + return execute_skill_create(ctx, args_json); + } + if (strcmp(tool_name, "skill_list") == 0) { + return execute_skill_list(ctx, args_json); + } + if (strcmp(tool_name, "skill_adopt") == 0) { + return execute_skill_adopt(ctx, args_json); + } + if (strcmp(tool_name, "skill_remove") == 0) { + return execute_skill_remove(ctx, args_json); + } + if (strcmp(tool_name, "skill_search") == 0) { + return execute_skill_search(args_json); + } if (strcmp(tool_name, "nostr_post_readme") == 0) { return execute_nostr_post_readme(ctx, args_json);