v0.0.66 - Complete tools refactor, move all tools-named sources/headers into src/tools, and update build wiring

This commit is contained in:
Your Name
2026-03-10 15:37:31 -04:00
parent 1d4cb9407d
commit 6dbd952023
29 changed files with 7376 additions and 6132 deletions

View File

@@ -92,10 +92,15 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
-D_GNU_SOURCE -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L -DMG_TLS=MG_TLS_BUILTIN \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Isrc -Inostr_core_lib -Inostr_core_lib/nostr_core \
-I. -Isrc -Isrc/tools -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
src/main.c src/config.c src/context.c src/llm.c \
src/nostr_handler.c src/agent.c src/tools.c src/trigger_manager.c \
src/nostr_handler.c src/agent.c src/tools/tools_common.c src/tools/tools_schema.c src/tools/tools_dispatch.c \
src/tools/tool_agent.c src/tools/tool_meta.c src/tools/tool_model.c \
src/tools/tool_nostr_query.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \
src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_local.c \
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/trigger_manager.c \
src/prompt_template.c src/http_api.c src/mongoose.c src/debug.c \
-o /build/didactyl_static \
nostr_core_lib/libnostr_core_x64.a \

View File

@@ -11,7 +11,23 @@ SRCS = \
$(SRC_DIR)/llm.c \
$(SRC_DIR)/nostr_handler.c \
$(SRC_DIR)/agent.c \
$(SRC_DIR)/tools.c \
$(SRC_DIR)/tools/tools_common.c \
$(SRC_DIR)/tools/tools_schema.c \
$(SRC_DIR)/tools/tools_dispatch.c \
$(SRC_DIR)/tools/tool_agent.c \
$(SRC_DIR)/tools/tool_meta.c \
$(SRC_DIR)/tools/tool_model.c \
$(SRC_DIR)/tools/tool_nostr_query.c \
$(SRC_DIR)/tools/tool_nostr_identity.c \
$(SRC_DIR)/tools/tool_nostr_social.c \
$(SRC_DIR)/tools/tool_nostr_relay.c \
$(SRC_DIR)/tools/tool_nostr_dm.c \
$(SRC_DIR)/tools/tool_admin.c \
$(SRC_DIR)/tools/tool_task.c \
$(SRC_DIR)/tools/tool_nostr_list.c \
$(SRC_DIR)/tools/tool_local.c \
$(SRC_DIR)/tools/tool_skill.c \
$(SRC_DIR)/tools/tool_nostr_post.c \
$(SRC_DIR)/trigger_manager.c \
$(SRC_DIR)/prompt_template.c \
$(SRC_DIR)/http_api.c \
@@ -20,6 +36,7 @@ SRCS = \
INCLUDES = \
-I$(SRC_DIR) \
-I$(SRC_DIR)/tools \
-I../nostr_core_lib \
-I../nostr_core_lib/cjson \
-I../nostr_core_lib/nostr_core

View File

@@ -55,11 +55,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa
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.65
## Current Status — v0.0.66
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.65Filter skill_list by 10123 adoption tags, gate live trigger registration on adoption, and allow underscores in skill d-tags
> Last release update: v0.0.66Complete tools refactor, move all tools-named sources/headers into src/tools, and update build wiring
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected

View File

@@ -1,3 +1,231 @@
```text
Context Log - not seen by model
timestamp=2026-03-10 12:38:09
phase=llm_chat_with_tools_messages
sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
model=claude-opus-4.6
context_bytes=4012
approx_tokens=1003
```
Sections: 10
# system_prompt | role=system
## Didactyl Agent
You are Didactyl, a sovereign AI agent living on Nostr.
### Communication Rules
- You communicate through encrypted Nostr direct messages.
- Keep responses concise and clear.
### Behavior
- Be helpful and technically accurate.
- If unsure, state uncertainty directly.
- Prefer actionable, practical advice.
- Use the person's name when messaging them if you know it.
- For the administrator, use their name from the administrator kind 0 profile metadata when available.
### Tool Use Policy
- You have tools available and should use them when a request requires taking action.
- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.
- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.
- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.
- After a tool call, base your answer on the actual tool result.
- Never claim a tool was run if no tool was executed.
### Task Management
- Maintain and use your internal task list as short-term working memory.
- Break long or complex actions into clear tasks before executing them.
- Update task status as you complete steps so your plan stays accurate.
### Safety
- Do not claim to have executed actions you did not execute.
- You may share your public key (npub) with anyone.
- Never reveal your private key (nsec) under any circumstance.
# admin_identity | role=system
### Administrator Identity (source: config.admin.pubkey)
This is your administrator! Admin pubkey (hex): 8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
This message has been cryptographically verified as coming from your administrator.
# admin_profile | role=system
### Administrator Kind 0 Profile (source: nostr kind 0)
Administrator kind 0 profile content (JSON): {"name":"William S Burroughs","display_name":"WSB","about":"I like to write, and I like crank. They are good. Sometimes I'm not so sure.","banner":"https://www.grunge.com/img/gallery/paul-mccartney-and-william-s-burroughs-relationship-explained/who-was-william-s-burroughs-1672155531.jpg","website":"https://addicted.com","picture":"https://hips.hearstapps.com/hmg-prod/images/william-s-burroughs.jpg?resize=1200:*","lud16":"","nip05":""}
# admin_contacts | role=system
### Administrator Kind 3 Contacts (source: nostr kind 3)
Administrator contacts (JSON): ["fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c","82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2","4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f","8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e","52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8"]
# admin_relays | role=system
### Administrator Kind 10002 Relays (source: nostr kind 10002)
Administrator relay list (JSON): ["wss://relay.laantungir.net/","wss://relay.damus.io/","wss://nos.lol/","wss://relay.nostr.band/","wss://purplepag.es/","ws://127.0.0.1:7777/","wss://nostr.mom/","wss://relay.0xchat.com"]
# admin_notes | role=system
### Administrator Recent Kind 1 Notes
Administrator recent public notes:
- I love my agent.
- My agent ... he is a special kind of guy.
- Forever we live. We never die.
- What time is it world?
- The sky is so blue.
- Today is a great day.
- hello
- What a great day to be alive.
- Good day everyone.
- test
# tasks | role=system
### Current Task List
#### Current Tasks
Your active task list - short-term working memory for tracking plan steps.
- [ ] No active tasks yet.
# dm_history | role=chat
12:36 Agent Didactyl has started up and is online at 2026-03-10 12:36:51 (version v0.0.65, connected relays: 5/5).
12:38 WSB good morning
# conversation | role=user
good morning
---
```text
Context Log - not seen by model
timestamp=2026-03-10 10:43:10
phase=direct_tool_exec
sender=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e
model=claude-opus-4.6
context_bytes=7495
approx_tokens=1873
```
slash=/help
result_json=AVAILABLE TOOLS
- admin_identity — Build admin identity context block from cached runtime metadata
- agent_identity — Build agent identity context block with pubkey and npub
- agent_version — Return current Didactyl version and metadata from build macros
- local_file_read — Read a local file as text from the configured working directory
- local_file_write — Write text content to a local file in the configured working directory
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
- local_shell_exec — Execute a shell command and return stdout/stderr
- model_get — Get current active LLM runtime configuration (excluding API key)
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
- model_set — Update active LLM configuration and persist it to config.jsonc
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
- nostr_dm_send — Send a NIP-04 encrypted DM
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
- nostr_npub — Return this agent's pubkey encoded as npub bech32
- nostr_post — Publish a Nostr event to connected relays
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
- nostr_pubkey — Return this agent's pubkey in hex format
- nostr_query — Query events from relays using a Nostr filter
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
- nostr_relay_info — Fetch NIP-11 relay information document
- nostr_relay_status — Get connection status and statistics for all relays
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
- skill_list — List this agent's published skills, optionally filtered by scope
- skill_remove — Remove a skill address from kind 10123 adoption list
- skill_search — Search public skills by query/author and optionally rank by adoption popularity
- task_list — Build current task list context block from tasks.json
- task_manage — Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)
- tool_list — List available tools with name, description, and JSON parameter schema
- trigger_list — List active triggered skills and their runtime status
AVAILABLE SKILLS
- (none)
result_markdown=AVAILABLE TOOLS
- admin_identity — Build admin identity context block from cached runtime metadata
- agent_identity — Build agent identity context block with pubkey and npub
- agent_version — Return current Didactyl version and metadata from build macros
- local_file_read — Read a local file as text from the configured working directory
- local_file_write — Write text content to a local file in the configured working directory
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
- local_shell_exec — Execute a shell command and return stdout/stderr
- model_get — Get current active LLM runtime configuration (excluding API key)
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
- model_set — Update active LLM configuration and persist it to config.jsonc
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
- nostr_dm_send — Send a NIP-04 encrypted DM
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
- nostr_npub — Return this agent's pubkey encoded as npub bech32
- nostr_post — Publish a Nostr event to connected relays
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
- nostr_pubkey — Return this agent's pubkey in hex format
- nostr_query — Query events from relays using a Nostr filter
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
- nostr_relay_info — Fetch NIP-11 relay information document
- nostr_relay_status — Get connection status and statistics for all relays
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
- skill_list — List this agent's published skills, optionally filtered by scope
- skill_remove — Remove a skill address from kind 10123 adoption list
- skill_search — Search public skills by query/author and optionally rank by adoption popularity
- task_list — Build current task list context block from tasks.json
- task_manage — Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)
- tool_list — List available tools with name, description, and JSON parameter schema
- trigger_list — List active triggered skills and their runtime status
AVAILABLE SKILLS
- (none)
---
```text
Context Log - not seen by model
timestamp=2026-03-10 10:25:14

285
plans/tools_refactor.md Normal file
View File

@@ -0,0 +1,285 @@
# Plan: Refactor tools.c into Individual Tool Files
## Problem
`src/tools.c` is ~6,123 lines containing 44 `execute_*` functions, a ~650-line schema builder, shared helpers, and a dispatcher. It is the largest file in the project and difficult to navigate, review, and maintain.
## Goal
Split `src/tools.c` into focused files organized by tool group, with shared helpers in a common module. The public API (`tools.h`) remains unchanged — no callers need modification.
## Target File Structure
```
src/
├── tools.h # Public API (UNCHANGED)
├── tools_internal.h # Shared internal helpers + per-tool-file function declarations
├── tools_common.c # Shared helper implementations
├── tools_schema.c # tools_build_openai_schema_json()
├── tools_dispatch.c # tools_init, tools_cleanup, tools_execute dispatcher
└── tools/
├── tool_nostr_post.c # nostr_post, nostr_post_readme, nostr_file_md_to_longform_post
├── tool_nostr_query.c # nostr_query
├── tool_nostr_identity.c # nostr_pubkey, nostr_npub, nostr_encode, nostr_decode
├── tool_nostr_social.c # nostr_react, nostr_profile_get, nostr_nip05_lookup
├── tool_nostr_dm.c # nostr_dm_send, nostr_dm_send_nip17, nostr_encrypt, nostr_decrypt
├── tool_nostr_relay.c # nostr_relay_status, nostr_relay_info
├── tool_nostr_list.c # nostr_list_manage, nostr_delete
├── tool_skill.c # skill_create, skill_list, skill_adopt, skill_remove, skill_edit, skill_search
├── tool_task.c # task_list, task_manage
├── tool_local.c # local_shell_exec, local_file_read, local_file_write, local_http_fetch
├── tool_admin.c # admin_identity, nostr_admin_profile, nostr_admin_contacts, nostr_admin_relays, nostr_admin_notes
├── tool_agent.c # agent_identity, agent_version, message_current
├── tool_model.c # model_get, model_set, model_list
└── tool_meta.c # tool_list, trigger_list
```
## Shared Helpers → tools_common.c
These helpers are used by multiple tool groups and must be non-static in `tools_common.c`, declared in `tools_internal.h`:
| Helper | Current lines | Used by |
|---|---|---|
| `json_error()` | 27-35 | ALL tools |
| `repair_json_control_chars()` | 37-101 | nostr_post, nostr_post_readme |
| `find_key_start()` | 103-111 | nostr_post loose parser |
| `skip_ws()` | 113-116 | parse_tool_args_json, loose parser |
| `parse_loose_kind()` | 118-135 | nostr_post |
| `parse_loose_json_string_value()` | 137-186 | nostr_post |
| `parse_loose_nostr_post_args()` | 188-212 | nostr_post |
| `ensure_tags_array()` | 214-227 | nostr_post |
| `has_tag_key()` | 229-243 | longform post helpers |
| `add_string_tag()` | 245-255 | nostr_post, delete, react, skill, longform |
| `remove_tag_key_all()` | 257-270 | upsert_string_tag |
| `upsert_string_tag()` | 272-276 | skill_edit |
| `parse_tool_args_json()` | 590-564 | ALL tools |
| `is_hex_string_len()` | 581-588 | nostr tools, skill tools, DM tools |
| `find_tag_value_string()` | 855-872 | skill tools, list_manage, longform |
| `tag_tuple_equal()` | 813-828 | skill tools, list_manage |
| `tags_contains_tuple()` | 830-840 | skill tools, list_manage |
| `remove_matching_tag_tuples()` | 842-853 | skill tools, list_manage |
| `build_tool_path()` | (used by local tools + readme) | local tools, readme |
## Tool-Local Helpers (stay static in their tool file)
| Helper | Destination |
|---|---|
| `validate_skill_d_tag()` | `tool_skill.c` |
| `fetch_adoption_list_tags()` | `tool_skill.c` |
| `publish_adoption_list()` | `tool_skill.c` |
| `adoption_tags_contains_address()` | `tool_skill.c` |
| `extract_skill_summary()` | `tool_skill.c` |
| `ci_contains()` | `tool_skill.c` |
| `d_tagify_string()` | `tool_nostr_post.c` |
| `first_markdown_h1()` | `tool_nostr_post.c` |
| `first_markdown_paragraph()` | `tool_nostr_post.c` |
| `trim_copy()` | `tool_nostr_post.c` |
| `auto_enrich_longform_tags()` | `tool_nostr_post.c` |
| `basename_lowercase_dup()` | `tool_nostr_post.c` |
| `detect_ca_bundle_path_for_tools()` | `tool_local.c` |
| `local_http_fetch_write_cb()` | `tool_local.c` |
| `persist_llm_config()` | `tool_model.c` |
## Tool Function Signatures
Each tool file exposes its `execute_*` functions via `tools_internal.h`. Two signature patterns exist:
```c
// Pattern A: context-free (only needs args)
char* execute_nostr_post(const char* args_json);
// Pattern B: context-dependent
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
```
## tools_internal.h Structure
```c
#ifndef DIDACTYL_TOOLS_INTERNAL_H
#define DIDACTYL_TOOLS_INTERNAL_H
#include "tools.h"
#include "cjson/cJSON.h"
// ─── Shared helpers (tools_common.c) ───
char* json_error(const char* msg);
cJSON* parse_tool_args_json(const char* args_json);
int is_hex_string_len(const char* s, size_t expected_len);
int add_string_tag(cJSON* tags, const char* key, const char* value);
int remove_tag_key_all(cJSON* tags, const char* key);
int upsert_string_tag(cJSON* tags, const char* key, const char* value);
int has_tag_key(cJSON* tags, const char* key);
cJSON* ensure_tags_array(cJSON** tags_inout);
cJSON* find_tag_value_string(cJSON* tags, const char* key);
int tag_tuple_equal(cJSON* a, cJSON* b);
int tags_contains_tuple(cJSON* tags, cJSON* tuple);
int remove_matching_tag_tuples(cJSON* tags, cJSON* tuple);
char* repair_json_control_chars(const char* in);
cJSON* parse_loose_nostr_post_args(const char* in);
int build_tool_path(tools_context_t* ctx, const char* rel, char* out, size_t out_size);
// ─── Per-tool-file execute functions ───
// tool_nostr_post.c
char* execute_nostr_post(const char* args_json);
char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json);
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json);
// tool_nostr_query.c
char* execute_nostr_query(const char* args_json);
// tool_nostr_identity.c
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json);
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json);
char* execute_nostr_encode(const char* args_json);
char* execute_nostr_decode(const char* args_json);
// tool_nostr_social.c
char* execute_nostr_react(const char* args_json);
char* execute_nostr_profile_get(const char* args_json);
char* execute_nostr_nip05_lookup(const char* args_json);
// tool_nostr_dm.c
char* execute_nostr_dm_send(const char* args_json);
char* execute_nostr_dm_send_nip17(const char* args_json);
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json);
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
// tool_nostr_relay.c
char* execute_nostr_relay_status(const char* args_json);
char* execute_nostr_relay_info(const char* args_json);
// tool_nostr_list.c
char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_json);
char* execute_nostr_delete(const char* args_json);
// tool_skill.c
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
char* execute_skill_list(tools_context_t* ctx, const char* args_json);
char* execute_skill_adopt(tools_context_t* ctx, const char* args_json);
char* execute_skill_remove(tools_context_t* ctx, const char* args_json);
char* execute_skill_edit(tools_context_t* ctx, const char* args_json);
char* execute_skill_search(const char* args_json);
// tool_task.c
char* execute_task_list(tools_context_t* ctx, const char* args_json);
char* execute_task_manage(tools_context_t* ctx, const char* args_json);
// tool_local.c
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json);
char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json);
char* execute_local_file_read(tools_context_t* ctx, const char* args_json);
char* execute_local_file_write(tools_context_t* ctx, const char* args_json);
// tool_admin.c
char* execute_admin_identity(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json);
// tool_agent.c
char* execute_agent_identity(tools_context_t* ctx, const char* args_json);
char* execute_agent_version(const char* args_json);
char* execute_message_current(tools_context_t* ctx, const char* args_json);
// tool_model.c
char* execute_model_get(const char* args_json);
char* execute_model_set(tools_context_t* ctx, const char* args_json);
char* execute_model_list(const char* args_json);
// tool_meta.c
char* execute_tool_list(tools_context_t* ctx, const char* args_json);
char* execute_trigger_list(tools_context_t* ctx, const char* args_json);
#endif
```
## Makefile Changes
Replace the single `$(SRC_DIR)/tools.c` entry with:
```makefile
SRCS = \
$(SRC_DIR)/main.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/context.c \
$(SRC_DIR)/llm.c \
$(SRC_DIR)/nostr_handler.c \
$(SRC_DIR)/agent.c \
$(SRC_DIR)/tools_common.c \
$(SRC_DIR)/tools_schema.c \
$(SRC_DIR)/tools_dispatch.c \
$(SRC_DIR)/tools/tool_nostr_post.c \
$(SRC_DIR)/tools/tool_nostr_query.c \
$(SRC_DIR)/tools/tool_nostr_identity.c \
$(SRC_DIR)/tools/tool_nostr_social.c \
$(SRC_DIR)/tools/tool_nostr_dm.c \
$(SRC_DIR)/tools/tool_nostr_relay.c \
$(SRC_DIR)/tools/tool_nostr_list.c \
$(SRC_DIR)/tools/tool_skill.c \
$(SRC_DIR)/tools/tool_task.c \
$(SRC_DIR)/tools/tool_local.c \
$(SRC_DIR)/tools/tool_admin.c \
$(SRC_DIR)/tools/tool_agent.c \
$(SRC_DIR)/tools/tool_model.c \
$(SRC_DIR)/tools/tool_meta.c \
$(SRC_DIR)/trigger_manager.c \
$(SRC_DIR)/prompt_template.c \
$(SRC_DIR)/http_api.c \
$(SRC_DIR)/mongoose.c \
$(SRC_DIR)/debug.c
```
## Implementation Steps
### Phase 1: Create shared infrastructure
- [ ] Create `src/tools_internal.h` with shared helper declarations and all `execute_*` prototypes
- [ ] Create `src/tools_common.c` — move shared helpers from `tools.c`, remove `static`, add `#include "tools_internal.h"`
- [ ] Create `src/tools_schema.c` — move `tools_build_openai_schema_json()` from `tools.c`
- [ ] Create `src/tools_dispatch.c` — move `tools_init()`, `tools_cleanup()`, `tools_execute()` from `tools.c`
- [ ] Update `Makefile` SRCS to include new files (keep `tools.c` temporarily with remaining functions)
- [ ] Build and verify — all tests pass, binary runs
### Phase 2: Extract tool files one at a time
Each step: move `execute_*` functions + their local helpers into the new file, remove from `tools.c`, build, verify.
- [ ] Create `src/tools/` directory
- [ ] Extract `src/tools/tool_nostr_post.c``execute_nostr_post`, `execute_nostr_post_readme`, `execute_nostr_file_md_to_longform_post` + local helpers: `d_tagify_string`, `first_markdown_h1`, `first_markdown_paragraph`, `trim_copy`, `auto_enrich_longform_tags`, `basename_lowercase_dup`
- [ ] Extract `src/tools/tool_nostr_query.c``execute_nostr_query`
- [ ] Extract `src/tools/tool_nostr_identity.c``execute_nostr_pubkey`, `execute_nostr_npub`, `execute_nostr_encode`, `execute_nostr_decode`
- [ ] Extract `src/tools/tool_nostr_social.c``execute_nostr_react`, `execute_nostr_profile_get`, `execute_nostr_nip05_lookup`
- [ ] Extract `src/tools/tool_nostr_dm.c``execute_nostr_dm_send`, `execute_nostr_dm_send_nip17`, `execute_nostr_encrypt`, `execute_nostr_decrypt`
- [ ] Extract `src/tools/tool_nostr_relay.c``execute_nostr_relay_status`, `execute_nostr_relay_info`
- [ ] Extract `src/tools/tool_nostr_list.c``execute_nostr_list_manage`, `execute_nostr_delete`
- [ ] Extract `src/tools/tool_skill.c``execute_skill_create`, `execute_skill_list`, `execute_skill_adopt`, `execute_skill_remove`, `execute_skill_edit`, `execute_skill_search` + local helpers: `validate_skill_d_tag`, `ci_contains`, `fetch_adoption_list_tags`, `publish_adoption_list`, `adoption_tags_contains_address`, `extract_skill_summary`
- [ ] Extract `src/tools/tool_task.c``execute_task_list`, `execute_task_manage`
- [ ] Extract `src/tools/tool_local.c``execute_local_http_fetch`, `execute_local_shell_exec`, `execute_local_file_read`, `execute_local_file_write` + local helpers: `detect_ca_bundle_path_for_tools`, `local_http_fetch_write_cb`, `free_string_array_heap`
- [ ] Extract `src/tools/tool_admin.c``execute_admin_identity`, `execute_nostr_admin_profile`, `execute_nostr_admin_contacts`, `execute_nostr_admin_relays`, `execute_nostr_admin_notes`
- [ ] Extract `src/tools/tool_agent.c``execute_agent_identity`, `execute_agent_version`, `execute_message_current`
- [ ] Extract `src/tools/tool_model.c``execute_model_get`, `execute_model_set`, `execute_model_list` + local helper: `persist_llm_config`
- [ ] Extract `src/tools/tool_meta.c``execute_tool_list`, `execute_trigger_list`
### Phase 3: Cleanup
- [ ] Delete `src/tools.c` (now empty — all code moved)
- [ ] Remove `src/tools.c` from Makefile SRCS
- [ ] Final build + full test run
- [ ] Verify binary size is comparable (no accidental code duplication)
- [ ] Increment version and push
## Validation Criteria
After each extraction step:
1. `make -j4` compiles with zero warnings
2. `./didactyl --debug 5` starts and runs normally
3. Tool execution via DM or `--test-tool` produces identical results
## Risk Mitigation
- **Build after every file extraction** — catch issues immediately
- **No logic changes** — purely mechanical code movement
- **Keep `tools.h` unchanged** — zero impact on callers (`agent.c`, `http_api.c`, `main.c`)
- **Git commit after each phase** — easy rollback points

View File

@@ -14,7 +14,7 @@
#include "llm.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools.h"
#include "tools/tools.h"
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include "debug.h"

View File

@@ -12,7 +12,7 @@
#include "main.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools.h"
#include "tools/tools.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "mongoose.h"

View File

@@ -14,7 +14,7 @@
#include "nostr_handler.h"
#include "debug.h"
#include "trigger_manager.h"
#include "tools.h"
#include "tools/tools.h"
#include "cjson/cJSON.h"
#include "http_api.h"

View File

@@ -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 65
#define DIDACTYL_VERSION "v0.0.65"
#define DIDACTYL_VERSION_PATCH 66
#define DIDACTYL_VERSION "v0.0.66"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

File diff suppressed because it is too large Load Diff

222
src/tools/tool_admin.c Normal file
View File

@@ -0,0 +1,222 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
char* execute_admin_identity(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
const char* tier_text = "Sender tier unknown.";
if (ctx->template_sender_tier == 1) {
tier_text = "This message has been cryptographically verified as coming from your administrator.";
} else if (ctx->template_sender_tier == 2) {
tier_text = "This message is from a web-of-trust contact (not the administrator).";
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_pubkey", ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown");
cJSON_AddStringToObject(out, "sender_verification", tier_text);
char content[768];
snprintf(content,
sizeof(content),
"## Administrator Identity (source: config.admin.pubkey)\n\n"
"This is your administrator! Admin pubkey (hex): %s\n\n"
"%s",
ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown",
tier_text);
cJSON_AddStringToObject(out, "content", content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char* kind0 = nostr_handler_get_admin_kind0_context();
const char* profile_json = (kind0 && kind0[0]) ? kind0 : "{}";
cJSON* out = cJSON_CreateObject();
if (!out) {
free(kind0);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind0_json", profile_json);
size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind0);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): %s",
profile_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(kind0);
return json;
}
char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char* kind3 = nostr_handler_get_admin_kind3_context();
const char* contacts_json = (kind3 && kind3[0]) ? kind3 : "[]";
cJSON* out = cJSON_CreateObject();
if (!out) {
free(kind3);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind3_json", contacts_json);
size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind3);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): %s",
contacts_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(kind3);
return json;
}
char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char* kind10002 = nostr_handler_get_admin_kind10002_context();
const char* relays_json = (kind10002 && kind10002[0]) ? kind10002 : "[]";
cJSON* out = cJSON_CreateObject();
if (!out) {
free(kind10002);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind10002_json", relays_json);
size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(kind10002);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): %s",
relays_json);
cJSON_AddStringToObject(out, "content", content);
free(content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(kind10002);
return json;
}
char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char* notes = nostr_handler_get_admin_kind1_notes_context();
const char* notes_text = (notes && notes[0]) ? notes : "";
cJSON* out = cJSON_CreateObject();
if (!out) {
free(notes);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_notes_content", notes_text);
size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
char* content = (char*)malloc(content_len);
if (!content) {
cJSON_Delete(out);
free(notes);
return NULL;
}
snprintf(content,
content_len,
"## Administrator Recent Kind 1 Notes\n\n%s",
notes_text);
cJSON_AddStringToObject(out, "content", content);
free(content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(notes);
return json;
}

99
src/tools/tool_agent.c Normal file
View File

@@ -0,0 +1,99 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../main.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
char* execute_message_current(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "content", ctx->template_current_user_message ? ctx->template_current_user_message : "");
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_agent_identity(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char npub[128] = {0};
if (nostr_key_to_bech32(ctx->cfg->keys.public_key, "npub", npub) != NOSTR_SUCCESS) {
strcpy(npub, "unknown");
}
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_pubkey", ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown");
cJSON_AddStringToObject(out, "agent_npub", npub);
char content[384];
snprintf(content,
sizeof(content),
"## Agent Identity\n\nYour pubkey (hex): %s\nYour npub: %s",
ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown",
npub);
cJSON_AddStringToObject(out, "content", content);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_agent_version(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("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;
}

505
src/tools/tool_local.c Normal file
View File

@@ -0,0 +1,505 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <curl/curl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <unistd.h>
#include "cjson/cJSON.h"
typedef struct {
char* data;
size_t len;
size_t cap;
size_t max_bytes;
int truncated;
} local_http_fetch_buffer_t;
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_safe_relative_path_local(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
if (strstr(path, "..") != NULL) return 0;
if (strchr(path, '\\') != NULL) return 0;
return 1;
}
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
if (!is_safe_relative_path_local(rel_path)) return -1;
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int n = 0;
if (strcmp(cwd, ".") == 0) {
n = snprintf(out, out_size, "%s", rel_path);
} else {
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
}
if (n < 0 || (size_t)n >= out_size) return -1;
return 0;
}
static char* shell_quote_single_local(const char* in) {
if (!in) return NULL;
size_t len = strlen(in);
size_t extra = 2U;
for (size_t i = 0; i < len; i++) {
if (in[i] == '\'') {
extra += 4U;
} else {
extra += 1U;
}
}
char* out = (char*)malloc(extra + 1U);
if (!out) return NULL;
size_t j = 0;
out[j++] = '\'';
for (size_t i = 0; i < len; i++) {
if (in[i] == '\'') {
out[j++] = '\'';
out[j++] = '\\';
out[j++] = '\'';
out[j++] = '\'';
} else {
out[j++] = in[i];
}
}
out[j++] = '\'';
out[j] = '\0';
return out;
}
static size_t local_http_fetch_write_cb_local(void* contents, size_t size, size_t nmemb, void* userp) {
local_http_fetch_buffer_t* rb = (local_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_local(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;
}
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* 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_local("local_http_fetch requires string url");
}
if (headers && !cJSON_IsArray(headers)) {
cJSON_Delete(args);
return json_error_local("local_http_fetch headers must be an array when provided");
}
if (body && !cJSON_IsString(body)) {
cJSON_Delete(args);
return json_error_local("local_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_local("local_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_local("local_http_fetch failed to initialize curl");
}
local_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, local_http_fetch_write_cb_local);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "didactyl/local_http_fetch");
const char* ca_bundle = detect_ca_bundle_path_for_tools_local();
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;
}
char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
if (!ctx->cfg->tools.shell.enabled) return json_error_local("shell tool disabled");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command");
if (!command || !cJSON_IsString(command) || !command->valuestring || command->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("local_shell_exec requires string command");
}
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int timeout_s = ctx->cfg->tools.shell.timeout_seconds > 0 ? ctx->cfg->tools.shell.timeout_seconds : 30;
char* quoted_cwd = shell_quote_single_local(cwd);
char* quoted_cmd = shell_quote_single_local(command->valuestring);
cJSON_Delete(args);
if (!quoted_cwd || !quoted_cmd) {
free(quoted_cwd);
free(quoted_cmd);
return json_error_local("allocation failure");
}
int needed = snprintf(NULL,
0,
"cd %s && timeout %ds sh -lc %s 2>&1",
quoted_cwd,
timeout_s,
quoted_cmd);
if (needed <= 0) {
free(quoted_cwd);
free(quoted_cmd);
return json_error_local("failed to build shell command");
}
char* cmd = (char*)malloc((size_t)needed + 1U);
if (!cmd) {
free(quoted_cwd);
free(quoted_cmd);
return json_error_local("allocation failure");
}
snprintf(cmd,
(size_t)needed + 1U,
"cd %s && timeout %ds sh -lc %s 2>&1",
quoted_cwd,
timeout_s,
quoted_cmd);
free(quoted_cwd);
free(quoted_cmd);
FILE* fp = popen(cmd, "r");
free(cmd);
if (!fp) return json_error_local("failed to execute command");
int max_bytes = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
char* output = (char*)calloc((size_t)max_bytes + 1U, 1U);
if (!output) {
pclose(fp);
return json_error_local("allocation failure");
}
size_t used = 0;
while (!feof(fp) && used < (size_t)max_bytes) {
size_t n = fread(output + used, 1, (size_t)max_bytes - used, fp);
used += n;
if (n == 0) break;
}
int raw_status = pclose(fp);
int exit_status = raw_status;
if (raw_status != -1) {
if (WIFEXITED(raw_status)) {
exit_status = WEXITSTATUS(raw_status);
} else if (WIFSIGNALED(raw_status)) {
exit_status = 128 + WTERMSIG(raw_status);
}
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(output);
return NULL;
}
cJSON_AddBoolToObject(out, "success", exit_status == 0 ? 1 : 0);
cJSON_AddNumberToObject(out, "exit_status", exit_status);
cJSON_AddStringToObject(out, "output", output);
free(output);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_local_file_read(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");
if (!path || !cJSON_IsString(path) || !path->valuestring) {
cJSON_Delete(args);
return json_error_local("local_file_read requires string path");
}
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;
char file_path[PATH_MAX];
if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
cJSON_Delete(args);
return json_error_local("local_file_read path is not allowed");
}
FILE* fp = fopen(file_path, "rb");
cJSON_Delete(args);
if (!fp) return json_error_local("local_file_read failed to open file");
char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U);
if (!buf) {
fclose(fp);
return json_error_local("allocation failure");
}
size_t n = fread(buf, 1, (size_t)max_bytes, fp);
int truncated = !feof(fp) ? 1 : 0;
fclose(fp);
buf[n] = '\0';
cJSON* out = cJSON_CreateObject();
if (!out) {
free(buf);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "path", file_path);
cJSON_AddNumberToObject(out, "bytes_read", (double)n);
cJSON_AddBoolToObject(out, "truncated", truncated);
cJSON_AddStringToObject(out, "content", buf);
free(buf);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_local_file_write(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append");
if (!path || !cJSON_IsString(path) || !path->valuestring ||
!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
return json_error_local("local_file_write requires string path and content");
}
char file_path[PATH_MAX];
if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
cJSON_Delete(args);
return json_error_local("local_file_write path is not allowed");
}
const char* content_str = content->valuestring;
size_t len = strlen(content_str);
int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0;
FILE* fp = fopen(file_path, do_append ? "ab" : "wb");
cJSON_Delete(args);
if (!fp) return json_error_local("local_file_write failed to open file");
size_t n = fwrite(content_str, 1, len, fp);
fclose(fp);
if (n != len) return json_error_local("local_file_write failed to write all bytes");
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "path", file_path);
cJSON_AddNumberToObject(out, "bytes_written", (double)n);
cJSON_AddBoolToObject(out, "append", do_append);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

103
src/tools/tool_meta.c Normal file
View File

@@ -0,0 +1,103 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdlib.h>
#include "cjson/cJSON.h"
#include "../trigger_manager.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
char* execute_tool_list(tools_context_t* ctx, const char* args_json) {
(void)args_json;
if (!ctx) {
return json_error_local("tool context unavailable");
}
char* schema_json = tools_build_openai_schema_json(ctx);
if (!schema_json) {
return json_error_local("failed to build tool schemas");
}
cJSON* schema_arr = cJSON_Parse(schema_json);
free(schema_json);
if (!schema_arr || !cJSON_IsArray(schema_arr)) {
cJSON_Delete(schema_arr);
return json_error_local("failed to parse tool schemas");
}
cJSON* out = cJSON_CreateObject();
cJSON* tools = cJSON_CreateArray();
if (!out || !tools) {
cJSON_Delete(schema_arr);
cJSON_Delete(out);
cJSON_Delete(tools);
return json_error_local("out of memory");
}
cJSON* item = NULL;
cJSON_ArrayForEach(item, schema_arr) {
cJSON* fn = cJSON_GetObjectItemCaseSensitive(item, "function");
if (!fn || !cJSON_IsObject(fn)) {
continue;
}
cJSON* name = cJSON_GetObjectItemCaseSensitive(fn, "name");
if (!name || !cJSON_IsString(name) || !name->valuestring) {
continue;
}
cJSON* row = cJSON_CreateObject();
if (!row) {
continue;
}
cJSON_AddStringToObject(row, "name", name->valuestring);
cJSON* description = cJSON_GetObjectItemCaseSensitive(fn, "description");
if (description && cJSON_IsString(description) && description->valuestring) {
cJSON_AddStringToObject(row, "description", description->valuestring);
} else {
cJSON_AddStringToObject(row, "description", "");
}
cJSON* parameters = cJSON_GetObjectItemCaseSensitive(fn, "parameters");
if (parameters) {
cJSON_AddItemToObject(row, "parameters", cJSON_Duplicate(parameters, 1));
} else {
cJSON_AddItemToObject(row, "parameters", cJSON_CreateObject());
}
cJSON_AddItemToArray(tools, row);
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddNumberToObject(out, "count", (double)cJSON_GetArraySize(tools));
cJSON_AddItemToObject(out, "tools", tools);
char* result = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(schema_arr);
return result;
}
char* execute_trigger_list(tools_context_t* ctx, const char* args_json) {
(void)args_json;
if (!ctx || !ctx->trigger_manager) {
return json_error_local("trigger manager unavailable");
}
char* status = trigger_manager_status_json(ctx->trigger_manager);
if (!status) {
return json_error_local("failed to build trigger status");
}
return status;
}

325
src/tools/tool_model.c Normal file
View File

@@ -0,0 +1,325 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../config.h"
#include "../llm.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int json_object_set_string(cJSON* obj, const char* key, const char* value) {
if (!obj || !key || !value) return -1;
cJSON_DeleteItemFromObjectCaseSensitive(obj, key);
cJSON* v = cJSON_CreateString(value);
if (!v) return -1;
cJSON_AddItemToObject(obj, key, v);
return 0;
}
static char* read_entire_file_local(const char* file_path, size_t* out_bytes) {
if (!file_path) return NULL;
FILE* fp = fopen(file_path, "rb");
if (!fp) return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL;
}
char* buf = (char*)malloc((size_t)len + 1U);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)len, fp);
fclose(fp);
if (n != (size_t)len) {
free(buf);
return NULL;
}
buf[len] = '\0';
if (out_bytes) *out_bytes = (size_t)len;
return buf;
}
static int persist_llm_config(tools_context_t* ctx, const llm_config_t* cfg) {
if (!ctx || !ctx->cfg || !cfg) return -1;
if (ctx->cfg->config_path[0] == '\0') return -1;
size_t src_len = 0;
char* raw = read_entire_file_local(ctx->cfg->config_path, &src_len);
if (!raw) return -1;
char* src = jsonc_strip_comments(raw, src_len);
free(raw);
if (!src) return -1;
cJSON* root = cJSON_ParseWithLength(src, strlen(src));
free(src);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
if (!llm || !cJSON_IsObject(llm)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "llm");
llm = cJSON_CreateObject();
if (!llm) {
cJSON_Delete(root);
return -1;
}
cJSON_AddItemToObject(root, "llm", llm);
}
if (json_object_set_string(llm, "provider", cfg->provider) != 0 ||
json_object_set_string(llm, "api_key", cfg->api_key) != 0 ||
json_object_set_string(llm, "model", cfg->model) != 0 ||
json_object_set_string(llm, "base_url", cfg->base_url) != 0) {
cJSON_Delete(root);
return -1;
}
cJSON_DeleteItemFromObjectCaseSensitive(llm, "max_tokens");
cJSON_AddNumberToObject(llm, "max_tokens", cfg->max_tokens);
cJSON_DeleteItemFromObjectCaseSensitive(llm, "temperature");
cJSON_AddNumberToObject(llm, "temperature", cfg->temperature);
char* out = cJSON_Print(root);
cJSON_Delete(root);
if (!out) return -1;
FILE* fp = fopen(ctx->cfg->config_path, "wb");
if (!fp) {
free(out);
return -1;
}
size_t out_len = strlen(out);
size_t n = fwrite(out, 1, out_len, fp);
fclose(fp);
free(out);
return n == out_len ? 0 : -1;
}
static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) {
if (!item) return 0;
if (!cJSON_IsString(item) || !item->valuestring) return -1;
size_t n = strlen(item->valuestring);
if (n >= dst_size) return -1;
memcpy(dst, item->valuestring, n + 1U);
if (changed) *changed = 1;
return 0;
}
char* execute_model_get(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
llm_config_t cfg;
if (llm_get_config(&cfg) != 0) {
return json_error_local("llm runtime unavailable");
}
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "provider", cfg.provider);
cJSON_AddStringToObject(out, "model", cfg.model);
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_model_set(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
llm_config_t cfg;
if (llm_get_config(&cfg) != 0) {
cJSON_Delete(args);
return json_error_local("llm runtime unavailable");
}
int changed = 0;
cJSON* provider = cJSON_GetObjectItemCaseSensitive(args, "provider");
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(args, "api_key");
cJSON* model = cJSON_GetObjectItemCaseSensitive(args, "model");
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(args, "base_url");
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens");
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature");
if (assign_string_field(provider, cfg.provider, sizeof(cfg.provider), &changed) != 0 ||
assign_string_field(api_key, cfg.api_key, sizeof(cfg.api_key), &changed) != 0 ||
assign_string_field(model, cfg.model, sizeof(cfg.model), &changed) != 0 ||
assign_string_field(base_url, cfg.base_url, sizeof(cfg.base_url), &changed) != 0) {
cJSON_Delete(args);
return json_error_local("model_set string field invalid or too long");
}
if (max_tokens) {
if (!cJSON_IsNumber(max_tokens)) {
cJSON_Delete(args);
return json_error_local("model_set max_tokens must be a number");
}
cfg.max_tokens = (int)max_tokens->valuedouble;
changed = 1;
}
if (temperature) {
if (!cJSON_IsNumber(temperature)) {
cJSON_Delete(args);
return json_error_local("model_set temperature must be a number");
}
cfg.temperature = temperature->valuedouble;
changed = 1;
}
cJSON_Delete(args);
if (!changed) {
return json_error_local("model_set requires at least one field to update");
}
if (llm_set_config(&cfg) != 0) {
return json_error_local("failed to update runtime llm config");
}
ctx->cfg->llm = cfg;
if (persist_llm_config(ctx, &cfg) != 0) {
return json_error_local("failed to persist llm config to config file");
}
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "provider", cfg.provider);
cJSON_AddStringToObject(out, "model", cfg.model);
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static void append_model_id(cJSON* ids, cJSON* item) {
if (!ids || !item) return;
if (cJSON_IsString(item) && item->valuestring) {
cJSON_AddItemToArray(ids, cJSON_CreateString(item->valuestring));
return;
}
if (cJSON_IsObject(item)) {
cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id");
if (id && cJSON_IsString(id) && id->valuestring) {
cJSON_AddItemToArray(ids, cJSON_CreateString(id->valuestring));
}
}
}
char* execute_model_list(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(args, "base_url");
if (base_url && (!cJSON_IsString(base_url) || !base_url->valuestring)) {
cJSON_Delete(args);
return json_error_local("model_list base_url must be a string");
}
const char* base_url_override = (base_url && base_url->valuestring && base_url->valuestring[0] != '\0')
? base_url->valuestring
: NULL;
char* raw = llm_list_models_json(base_url_override);
cJSON_Delete(args);
if (!raw) {
return json_error_local("model_list request failed");
}
cJSON* root = cJSON_Parse(raw);
free(raw);
if (!root) {
cJSON_Delete(root);
return json_error_local("model_list returned invalid JSON");
}
cJSON* ids = cJSON_CreateArray();
cJSON* out = cJSON_CreateObject();
if (!ids || !out) {
cJSON_Delete(ids);
cJSON_Delete(out);
cJSON_Delete(root);
return NULL;
}
if (cJSON_IsArray(root)) {
int n = cJSON_GetArraySize(root);
for (int i = 0; i < n; i++) {
append_model_id(ids, cJSON_GetArrayItem(root, i));
}
} else if (cJSON_IsObject(root)) {
cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data");
if (data && cJSON_IsArray(data)) {
int n = cJSON_GetArraySize(data);
for (int i = 0; i < n; i++) {
append_model_id(ids, cJSON_GetArrayItem(data, i));
}
}
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(ids));
cJSON_AddItemToObject(out, "models", ids);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(root);
return json;
}

218
src/tools/tool_nostr_dm.c Normal file
View File

@@ -0,0 +1,218 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_hex_string_len_local(const char* s, size_t expected_len) {
if (!s) return 0;
if (strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
if (!((s[i] >= '0' && s[i] <= '9') ||
(s[i] >= 'a' && s[i] <= 'f') ||
(s[i] >= 'A' && s[i] <= 'F'))) {
return 0;
}
}
return 1;
}
char* execute_nostr_dm_send(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* message = cJSON_GetObjectItemCaseSensitive(args, "message");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len_local(recipient->valuestring, 64U) ||
!message || !cJSON_IsString(message) || !message->valuestring || message->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_dm_send requires recipient_pubkey hex and non-empty message");
}
int rc = nostr_handler_send_dm(recipient->valuestring, message->valuestring);
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == 0 ? 1 : 0);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddNumberToObject(out, "message_length", (double)strlen(message->valuestring));
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* plaintext = cJSON_GetObjectItemCaseSensitive(args, "plaintext");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len_local(recipient->valuestring, 64U) ||
!plaintext || !cJSON_IsString(plaintext) || !plaintext->valuestring || plaintext->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_encrypt requires recipient_pubkey hex and plaintext");
}
unsigned char recipient_pubkey[32];
if (nostr_hex_to_bytes(recipient->valuestring, recipient_pubkey, sizeof(recipient_pubkey)) != 0) {
cJSON_Delete(args);
return json_error_local("nostr_encrypt invalid recipient_pubkey");
}
size_t out_cap = (strlen(plaintext->valuestring) * 4U) + 1024U;
char* ciphertext = (char*)malloc(out_cap);
if (!ciphertext) {
cJSON_Delete(args);
return json_error_local("allocation failure");
}
int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key,
recipient_pubkey,
plaintext->valuestring,
ciphertext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(ciphertext);
cJSON_Delete(args);
return json_error_local("nostr_encrypt failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(ciphertext);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddStringToObject(out, "ciphertext", ciphertext);
free(ciphertext);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* sender = cJSON_GetObjectItemCaseSensitive(args, "sender_pubkey");
cJSON* ciphertext = cJSON_GetObjectItemCaseSensitive(args, "ciphertext");
if (!sender || !cJSON_IsString(sender) || !sender->valuestring || !is_hex_string_len_local(sender->valuestring, 64U) ||
!ciphertext || !cJSON_IsString(ciphertext) || !ciphertext->valuestring || ciphertext->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_decrypt requires sender_pubkey hex and ciphertext");
}
unsigned char sender_pubkey[32];
if (nostr_hex_to_bytes(sender->valuestring, sender_pubkey, sizeof(sender_pubkey)) != 0) {
cJSON_Delete(args);
return json_error_local("nostr_decrypt invalid sender_pubkey");
}
size_t out_cap = strlen(ciphertext->valuestring) + 1024U;
char* plaintext = (char*)malloc(out_cap);
if (!plaintext) {
cJSON_Delete(args);
return json_error_local("allocation failure");
}
int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key,
sender_pubkey,
ciphertext->valuestring,
plaintext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(plaintext);
cJSON_Delete(args);
return json_error_local("nostr_decrypt failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(plaintext);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "sender_pubkey", sender->valuestring);
cJSON_AddStringToObject(out, "plaintext", plaintext);
free(plaintext);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_dm_send_nip17(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* message = cJSON_GetObjectItemCaseSensitive(args, "message");
cJSON* subject = cJSON_GetObjectItemCaseSensitive(args, "subject");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len_local(recipient->valuestring, 64U) ||
!message || !cJSON_IsString(message) || !message->valuestring || message->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_dm_send_nip17 requires recipient_pubkey hex and non-empty message");
}
if (subject && !cJSON_IsString(subject)) {
cJSON_Delete(args);
return json_error_local("nostr_dm_send_nip17 subject must be a string when provided");
}
int rc = nostr_handler_send_dm_nip17(recipient->valuestring,
message->valuestring,
(subject && subject->valuestring) ? subject->valuestring : NULL);
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == 0 ? 1 : 0);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddStringToObject(out, "protocol", "nip17");
cJSON_AddNumberToObject(out, "message_length", (double)strlen(message->valuestring));
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

View File

@@ -0,0 +1,192 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_hex_string_len_local(const char* s, size_t expected_len) {
if (!s) return 0;
if (strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
if (!((s[i] >= '0' && s[i] <= '9') ||
(s[i] >= 'a' && s[i] <= 'f') ||
(s[i] >= 'A' && s[i] <= 'F'))) {
return 0;
}
}
return 1;
}
char* execute_nostr_encode(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* type = cJSON_GetObjectItemCaseSensitive(args, "type");
cJSON* hex = cJSON_GetObjectItemCaseSensitive(args, "hex");
if (!type || !cJSON_IsString(type) || !type->valuestring ||
!hex || !cJSON_IsString(hex) || !hex->valuestring || !is_hex_string_len_local(hex->valuestring, 64U)) {
cJSON_Delete(args);
return json_error_local("nostr_encode requires type string and hex 64-char string");
}
char type_name[32];
snprintf(type_name, sizeof(type_name), "%s", type->valuestring);
unsigned char key_bytes[32];
if (nostr_hex_to_bytes(hex->valuestring, key_bytes, sizeof(key_bytes)) != 0) {
cJSON_Delete(args);
return json_error_local("nostr_encode invalid hex");
}
char bech32[256] = {0};
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strcmp(type_name, "npub") == 0) {
rc = nostr_key_to_bech32(key_bytes, "npub", bech32);
} else if (strcmp(type_name, "nsec") == 0) {
rc = nostr_key_to_bech32(key_bytes, "nsec", bech32);
} else if (strcmp(type_name, "note") == 0) {
rc = nostr_key_to_bech32(key_bytes, "note", bech32);
} else {
cJSON_Delete(args);
return json_error_local("nostr_encode currently supports npub, nsec, note");
}
cJSON_Delete(args);
if (rc != NOSTR_SUCCESS) {
return json_error_local("nostr_encode failed");
}
char uri[320] = {0};
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "type", type_name);
cJSON_AddStringToObject(out, "uri", uri);
cJSON_AddBoolToObject(out, "limited_support", 1);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_decode(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* uri_in = cJSON_GetObjectItemCaseSensitive(args, "uri");
if (!uri_in || !cJSON_IsString(uri_in) || !uri_in->valuestring || uri_in->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_decode requires uri string");
}
const char* in = uri_in->valuestring;
if (strncmp(in, "nostr:", 6) == 0) {
in += 6;
}
unsigned char key[32];
char key_hex[65] = {0};
const char* type = NULL;
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strncmp(in, "npub1", 5) == 0) {
type = "npub";
rc = nostr_decode_npub(in, key);
} else if (strncmp(in, "nsec1", 5) == 0) {
type = "nsec";
rc = nostr_decode_nsec(in, key);
} else {
cJSON_Delete(args);
return json_error_local("nostr_decode currently supports npub and nsec");
}
cJSON_Delete(args);
if (rc != NOSTR_SUCCESS) {
return json_error_local("nostr_decode failed");
}
nostr_bytes_to_hex(key, 32, key_hex);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "type", type);
if (strcmp(type, "npub") == 0) {
cJSON_AddStringToObject(out, "pubkey", key_hex);
} else {
cJSON_AddStringToObject(out, "private_key", key_hex);
}
cJSON_AddBoolToObject(out, "limited_support", 1);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "pubkey", ctx->cfg->keys.public_key_hex);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char npub[256] = {0};
if (nostr_key_to_bech32(ctx->cfg->keys.public_key, "npub", npub) != NOSTR_SUCCESS) {
return json_error_local("failed to encode npub");
}
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "npub", npub);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

361
src/tools/tool_nostr_list.c Normal file
View File

@@ -0,0 +1,361 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_hex_string_len_local(const char* s, size_t expected_len) {
if (!s) return 0;
if (strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
if (!((s[i] >= '0' && s[i] <= '9') ||
(s[i] >= 'a' && s[i] <= 'f') ||
(s[i] >= 'A' && s[i] <= 'F'))) {
return 0;
}
}
return 1;
}
static int add_string_tag_local(cJSON* tags, const char* key, const char* value) {
if (!tags || !cJSON_IsArray(tags) || !key || !value) return -1;
cJSON* tuple = cJSON_CreateArray();
if (!tuple) return -1;
cJSON_AddItemToArray(tuple, cJSON_CreateString(key));
cJSON_AddItemToArray(tuple, cJSON_CreateString(value));
cJSON_AddItemToArray(tags, tuple);
return 0;
}
static int tag_tuple_equal_local(cJSON* a, cJSON* b) {
if (!a || !b || !cJSON_IsArray(a) || !cJSON_IsArray(b)) return 0;
int an = cJSON_GetArraySize(a);
int bn = cJSON_GetArraySize(b);
if (an != bn) return 0;
for (int i = 0; i < an; i++) {
cJSON* av = cJSON_GetArrayItem(a, i);
cJSON* bv = cJSON_GetArrayItem(b, i);
if (!av || !bv || !cJSON_IsString(av) || !cJSON_IsString(bv) || !av->valuestring || !bv->valuestring) {
return 0;
}
if (strcmp(av->valuestring, bv->valuestring) != 0) return 0;
}
return 1;
}
static int tags_contains_tuple_local(cJSON* tags, cJSON* tuple) {
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* t = cJSON_GetArrayItem(tags, i);
if (tag_tuple_equal_local(t, tuple)) return 1;
}
return 0;
}
static int remove_matching_tag_tuples_local(cJSON* tags, cJSON* tuple) {
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
int removed = 0;
for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
cJSON* t = cJSON_GetArrayItem(tags, i);
if (tag_tuple_equal_local(t, tuple)) {
cJSON_DeleteItemFromArray(tags, i);
removed++;
}
}
return removed;
}
char* execute_nostr_delete(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* event_ids = cJSON_GetObjectItemCaseSensitive(args, "event_ids");
cJSON* kinds = cJSON_GetObjectItemCaseSensitive(args, "kinds");
cJSON* reason = cJSON_GetObjectItemCaseSensitive(args, "reason");
if (!event_ids || !cJSON_IsArray(event_ids) || cJSON_GetArraySize(event_ids) <= 0) {
cJSON_Delete(args);
return json_error_local("nostr_delete requires non-empty event_ids array");
}
if (kinds && !cJSON_IsArray(kinds)) {
cJSON_Delete(args);
return json_error_local("nostr_delete kinds must be an array when provided");
}
if (reason && !cJSON_IsString(reason)) {
cJSON_Delete(args);
return json_error_local("nostr_delete reason must be a string when provided");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
cJSON_Delete(args);
return json_error_local("failed to create tags");
}
int event_id_count = cJSON_GetArraySize(event_ids);
for (int i = 0; i < event_id_count; i++) {
cJSON* id = cJSON_GetArrayItem(event_ids, i);
if (!id || !cJSON_IsString(id) || !id->valuestring || !is_hex_string_len_local(id->valuestring, 64U)) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("nostr_delete event_ids must contain 64-char hex strings");
}
if (add_string_tag_local(tags, "e", id->valuestring) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("failed to add e tag");
}
}
if (kinds) {
int kind_count = cJSON_GetArraySize(kinds);
for (int i = 0; i < kind_count; i++) {
cJSON* k = cJSON_GetArrayItem(kinds, i);
if (!k || !cJSON_IsNumber(k)) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("nostr_delete kinds must contain integers");
}
char kind_buf[32];
snprintf(kind_buf, sizeof(kind_buf), "%d", (int)k->valuedouble);
if (add_string_tag_local(tags, "k", kind_buf) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("failed to add k tag");
}
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
const char* reason_content = (reason && reason->valuestring) ? reason->valuestring : "";
int rc = nostr_handler_publish_kind_event(5, reason_content, tags, &publish_result);
cJSON_Delete(tags);
cJSON_Delete(args);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_delete failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_delete published");
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "requested_event_count", event_id_count);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* list_kind = cJSON_GetObjectItemCaseSensitive(args, "list_kind");
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
cJSON* items = cJSON_GetObjectItemCaseSensitive(args, "items");
if (!list_kind || !cJSON_IsNumber(list_kind) ||
!action || !cJSON_IsString(action) || !action->valuestring ||
!items || !cJSON_IsArray(items)) {
cJSON_Delete(args);
return json_error_local("nostr_list_manage requires list_kind, action, and items");
}
if (strcmp(action->valuestring, "add") != 0 && strcmp(action->valuestring, "remove") != 0) {
cJSON_Delete(args);
return json_error_local("nostr_list_manage action must be add or remove");
}
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_local("failed to create list query filter");
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber((int)list_kind->valuedouble));
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* events = events_json ? cJSON_Parse(events_json) : NULL;
free(events_json);
char* list_content = strdup("");
if (!list_content) {
cJSON_Delete(events);
cJSON_Delete(args);
return json_error_local("allocation failure");
}
cJSON* updated_tags = cJSON_CreateArray();
if (!updated_tags) {
free(list_content);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error_local("failed to create list tags");
}
if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(events, 0);
if (ev0 && cJSON_IsObject(ev0)) {
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev0, "content");
if (content && cJSON_IsString(content) && content->valuestring) {
free(list_content);
list_content = strdup(content->valuestring);
if (!list_content) {
cJSON_Delete(updated_tags);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error_local("allocation failure");
}
}
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev0, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON_Delete(updated_tags);
updated_tags = cJSON_Duplicate(tags, 1);
if (!updated_tags) {
free(list_content);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error_local("failed to duplicate existing list tags");
}
}
}
}
int items_affected = 0;
int item_count = cJSON_GetArraySize(items);
for (int i = 0; i < item_count; i++) {
cJSON* tuple = cJSON_GetArrayItem(items, i);
if (!tuple || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) <= 0) {
continue;
}
int tuple_ok = 1;
int tuple_size = cJSON_GetArraySize(tuple);
for (int j = 0; j < tuple_size; j++) {
cJSON* part = cJSON_GetArrayItem(tuple, j);
if (!part || !cJSON_IsString(part) || !part->valuestring) {
tuple_ok = 0;
break;
}
}
if (!tuple_ok) {
continue;
}
if (strcmp(action->valuestring, "add") == 0) {
if (!tags_contains_tuple_local(updated_tags, tuple)) {
cJSON* dup = cJSON_Duplicate(tuple, 1);
if (!dup) {
free(list_content);
cJSON_Delete(updated_tags);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error_local("failed to duplicate list item");
}
cJSON_AddItemToArray(updated_tags, dup);
items_affected++;
}
} else {
items_affected += remove_matching_tag_tuples_local(updated_tags, tuple);
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event((int)list_kind->valuedouble,
list_content,
updated_tags,
&publish_result);
free(list_content);
cJSON_Delete(updated_tags);
cJSON_Delete(events);
if (rc != 0) {
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_list_manage failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddNumberToObject(out, "list_kind", publish_result.kind);
cJSON_AddStringToObject(out, "action", action->valuestring);
cJSON_Delete(args);
cJSON_AddNumberToObject(out, "items_affected", items_affected);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}

927
src/tools/tool_nostr_post.c Normal file
View File

@@ -0,0 +1,927 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static const char* skip_ws_local(const char* p) {
while (p && *p && isspace((unsigned char)*p)) p++;
return p;
}
static char* sanitize_json_string_controls_local(const char* in) {
if (!in) return NULL;
size_t len = strlen(in);
size_t cap = (len * 2U) + 1U;
char* out = (char*)malloc(cap);
if (!out) return NULL;
int in_string = 0;
int escaping = 0;
size_t j = 0;
for (size_t i = 0; i < len; i++) {
char c = in[i];
if (escaping) {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
escaping = 0;
continue;
}
if (c == '\\') {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
if (in_string) escaping = 1;
continue;
}
if (c == '"') {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
in_string = !in_string;
continue;
}
if (in_string && (c == '\n' || c == '\r' || c == '\t')) {
if (j + 2U >= cap) {
free(out);
return NULL;
}
out[j++] = '\\';
out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't';
continue;
}
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
}
out[j] = '\0';
return out;
}
static cJSON* parse_tool_args_json_local(const char* args_json) {
const char* raw_args_json = args_json ? args_json : "{}";
raw_args_json = skip_ws_local(raw_args_json);
if (!raw_args_json || raw_args_json[0] == '\0') {
raw_args_json = "{}";
}
cJSON* args = cJSON_Parse(raw_args_json);
char* repaired_args_json = NULL;
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
if (!args) {
repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
if (repaired_args_json) {
args = cJSON_Parse(repaired_args_json);
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
}
}
free(repaired_args_json);
return args;
}
static const char* find_key_start_local(const char* in, const char* key) {
if (!in || !key) return NULL;
char pattern[64];
int n = snprintf(pattern, sizeof(pattern), "\"%s\"", key);
if (n <= 0 || (size_t)n >= sizeof(pattern)) return NULL;
return strstr(in, pattern);
}
static int parse_loose_kind_local(const char* in, int* out_kind) {
if (!in || !out_kind) return -1;
const char* p = find_key_start_local(in, "kind");
if (!p) return -1;
p = strchr(p, ':');
if (!p) return -1;
p = skip_ws_local(p + 1);
if (!p || !*p) return -1;
char* end = NULL;
long v = strtol(p, &end, 10);
if (end == p) return -1;
*out_kind = (int)v;
return 0;
}
static char* parse_loose_json_string_value_local(const char* in, const char* key) {
if (!in || !key) return NULL;
const char* p = find_key_start_local(in, key);
if (!p) return NULL;
p = strchr(p, ':');
if (!p) return NULL;
p = skip_ws_local(p + 1);
if (!p || *p != '"') return NULL;
p++;
size_t max_len = strlen(p);
char* out = (char*)malloc(max_len + 1U);
if (!out) return NULL;
size_t j = 0;
int escaping = 0;
for (size_t i = 0; p[i] != '\0'; i++) {
char c = p[i];
if (escaping) {
switch (c) {
case 'n': out[j++] = '\n'; break;
case 'r': out[j++] = '\r'; break;
case 't': out[j++] = '\t'; break;
case '"': out[j++] = '"'; break;
case '\\': out[j++] = '\\'; break;
default: out[j++] = c; break;
}
escaping = 0;
continue;
}
if (c == '\\') {
escaping = 1;
continue;
}
if (c == '"') {
out[j] = '\0';
return out;
}
out[j++] = c;
}
out[j] = '\0';
return out;
}
static cJSON* parse_loose_nostr_post_args_local(const char* in) {
if (!in) return NULL;
int kind = 0;
if (parse_loose_kind_local(in, &kind) != 0) {
return NULL;
}
char* content = parse_loose_json_string_value_local(in, "content");
if (!content) {
return NULL;
}
cJSON* args = cJSON_CreateObject();
if (!args) {
free(content);
return NULL;
}
cJSON_AddNumberToObject(args, "kind", kind);
cJSON_AddStringToObject(args, "content", content);
free(content);
return args;
}
static cJSON* ensure_tags_array_local(cJSON** tags_inout) {
if (!tags_inout) return NULL;
if (*tags_inout) {
if (cJSON_IsArray(*tags_inout)) {
return *tags_inout;
}
cJSON_Delete(*tags_inout);
*tags_inout = NULL;
}
*tags_inout = cJSON_CreateArray();
return *tags_inout;
}
static int has_tag_key_local(cJSON* tags, const char* key) {
if (!tags || !cJSON_IsArray(tags) || !key) return 0;
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag)) continue;
cJSON* k = cJSON_GetArrayItem(tag, 0);
if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) {
return 1;
}
}
return 0;
}
static int add_string_tag_local(cJSON* tags, const char* key, const char* value) {
if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1;
cJSON* tag = cJSON_CreateArray();
if (!tag) return -1;
cJSON_AddItemToArray(tag, cJSON_CreateString(key));
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
cJSON_AddItemToArray(tags, tag);
return 0;
}
static char* trim_copy_local(const char* start, size_t len) {
while (len > 0 && isspace((unsigned char)start[0])) {
start++;
len--;
}
while (len > 0 && isspace((unsigned char)start[len - 1])) {
len--;
}
char* out = (char*)malloc(len + 1U);
if (!out) return NULL;
memcpy(out, start, len);
out[len] = '\0';
return out;
}
static char* first_markdown_h1_local(const char* content) {
if (!content) return NULL;
const char* p = content;
while (*p) {
const char* line = p;
const char* nl = strchr(line, '\n');
size_t len = nl ? (size_t)(nl - line) : strlen(line);
while (len > 0 && line[len - 1] == '\r') len--;
size_t i = 0;
while (i < len && isspace((unsigned char)line[i])) i++;
if (i < len && line[i] == '#') {
size_t j = i;
while (j < len && line[j] == '#') j++;
if (j < len && isspace((unsigned char)line[j])) {
while (j < len && isspace((unsigned char)line[j])) j++;
if (j < len) return trim_copy_local(line + j, len - j);
}
}
if (!nl) break;
p = nl + 1;
}
return NULL;
}
static int is_paragraph_line_local(const char* line, size_t len) {
size_t i = 0;
while (i < len && isspace((unsigned char)line[i])) i++;
if (i >= len) return 0;
char c = line[i];
if (c == '#' || c == '>' || c == '-' || c == '*' || c == '`' || c == '|') return 0;
if (isdigit((unsigned char)c)) {
size_t j = i;
while (j < len && isdigit((unsigned char)line[j])) j++;
if (j < len && line[j] == '.') return 0;
}
return 1;
}
static char* first_markdown_paragraph_local(const char* content) {
if (!content) return NULL;
const char* p = content;
while (*p) {
const char* line = p;
const char* nl = strchr(line, '\n');
size_t len = nl ? (size_t)(nl - line) : strlen(line);
while (len > 0 && line[len - 1] == '\r') len--;
if (is_paragraph_line_local(line, len)) {
size_t cap = 1024;
size_t used = 0;
char* out = (char*)malloc(cap);
if (!out) return NULL;
const char* q = line;
const char* qnl = nl;
size_t qlen = len;
while (1) {
if (!is_paragraph_line_local(q, qlen)) break;
size_t start = 0;
while (start < qlen && isspace((unsigned char)q[start])) start++;
size_t end = qlen;
while (end > start && isspace((unsigned char)q[end - 1])) end--;
size_t part_len = end - start;
if (used + part_len + 2 >= cap) {
cap = (cap * 2U) + part_len + 16U;
char* bigger = (char*)realloc(out, cap);
if (!bigger) {
free(out);
return NULL;
}
out = bigger;
}
if (part_len > 0) {
if (used > 0) out[used++] = ' ';
memcpy(out + used, q + start, part_len);
used += part_len;
}
if (!qnl) break;
q = qnl + 1;
qnl = strchr(q, '\n');
qlen = qnl ? (size_t)(qnl - q) : strlen(q);
while (qlen > 0 && q[qlen - 1] == '\r') qlen--;
}
out[used] = '\0';
return out;
}
if (!nl) break;
p = nl + 1;
}
return NULL;
}
static char* first_markdown_image_url_local(const char* content) {
if (!content) return NULL;
const char* p = content;
while ((p = strstr(p, "![")) != NULL) {
const char* close_bracket = strchr(p + 2, ']');
if (!close_bracket || close_bracket[1] != '(') {
p += 2;
continue;
}
const char* url_start = close_bracket + 2;
const char* url_end = strchr(url_start, ')');
if (!url_end || url_end <= url_start) {
p += 2;
continue;
}
return trim_copy_local(url_start, (size_t)(url_end - url_start));
}
return NULL;
}
static char* d_tagify_string_local(const char* in, const char* fallback) {
if (!in || in[0] == '\0') {
return fallback ? strdup(fallback) : NULL;
}
size_t len = strlen(in);
char* out = (char*)malloc(len + 1U);
if (!out) return NULL;
size_t j = 0;
int prev_dash = 0;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)in[i];
if (isalnum(c)) {
out[j++] = (char)tolower(c);
prev_dash = 0;
} else if (!prev_dash && j > 0) {
out[j++] = '-';
prev_dash = 1;
}
}
while (j > 0 && out[j - 1] == '-') j--;
out[j] = '\0';
if (j == 0) {
free(out);
return fallback ? strdup(fallback) : NULL;
}
return out;
}
static void ensure_nip23_metadata_tags_local(int kind, const char* content, cJSON** tags_inout) {
if (!content || (kind != 30023 && kind != 30024) || !tags_inout) {
return;
}
cJSON* tags = ensure_tags_array_local(tags_inout);
if (!tags) return;
char* title = has_tag_key_local(tags, "title") ? NULL : first_markdown_h1_local(content);
char* summary = has_tag_key_local(tags, "summary") ? NULL : first_markdown_paragraph_local(content);
char* image = has_tag_key_local(tags, "image") ? NULL : first_markdown_image_url_local(content);
if (title) (void)add_string_tag_local(tags, "title", title);
if (summary) (void)add_string_tag_local(tags, "summary", summary);
if (image) (void)add_string_tag_local(tags, "image", image);
if (!has_tag_key_local(tags, "published_at")) {
char published_at[32];
snprintf(published_at, sizeof(published_at), "%lld", (long long)time(NULL));
(void)add_string_tag_local(tags, "published_at", published_at);
}
if (!has_tag_key_local(tags, "d")) {
char* d_tag = NULL;
if (title) {
d_tag = d_tagify_string_local(title, "long-form-note");
} else {
char* derived_title = first_markdown_h1_local(content);
d_tag = d_tagify_string_local(derived_title, "long-form-note");
free(derived_title);
}
if (d_tag) {
(void)add_string_tag_local(tags, "d", d_tag);
free(d_tag);
}
}
free(title);
free(summary);
free(image);
}
static int is_safe_relative_path_local(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
if (strstr(path, "..") != NULL) return 0;
if (strchr(path, '\\') != NULL) return 0;
return 1;
}
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
if (!is_safe_relative_path_local(rel_path)) return -1;
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int n = 0;
if (strcmp(cwd, ".") == 0) {
n = snprintf(out, out_size, "%s", rel_path);
} else {
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
}
if (n < 0 || (size_t)n >= out_size) return -1;
return 0;
}
static char* read_entire_file_local(const char* file_path, size_t* out_bytes) {
if (!file_path) return NULL;
FILE* fp = fopen(file_path, "rb");
if (!fp) return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long size = ftell(fp);
if (size < 0) {
fclose(fp);
return NULL;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL;
}
char* buf = (char*)malloc((size_t)size + 1U);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)size, fp);
fclose(fp);
buf[n] = '\0';
if (out_bytes) *out_bytes = n;
return buf;
}
static char* basename_lowercase_dup_local(const char* path) {
if (!path || path[0] == '\0') return NULL;
const char* base = strrchr(path, '/');
base = base ? (base + 1) : path;
if (base[0] == '\0') return NULL;
char* out = strdup(base);
if (!out) return NULL;
for (size_t i = 0; out[i] != '\0'; i++) {
out[i] = (char)tolower((unsigned char)out[i]);
}
return out;
}
char* execute_nostr_post(const char* args_json) {
const char* raw_args_json = args_json ? args_json : "{}";
raw_args_json = skip_ws_local(raw_args_json);
if (!raw_args_json || raw_args_json[0] == '\0') {
raw_args_json = "{}";
}
cJSON* args = cJSON_Parse(raw_args_json);
char* repaired_args_json = NULL;
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
if (!args && raw_args_json) {
repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
if (repaired_args_json) {
args = cJSON_Parse(repaired_args_json);
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
}
}
if (!args && raw_args_json) {
args = parse_loose_nostr_post_args_local(raw_args_json);
}
if (!args) {
free(repaired_args_json);
return json_error_local("invalid arguments JSON");
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(args, "tags");
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
free(repaired_args_json);
return json_error_local("nostr_post requires integer kind and string content");
}
if (tags && !cJSON_IsArray(tags)) {
cJSON_Delete(args);
free(repaired_args_json);
return json_error_local("nostr_post tags must be an array when provided");
}
cJSON* tags_dup = NULL;
if (tags) {
tags_dup = cJSON_Duplicate(tags, 1);
if (!tags_dup) {
cJSON_Delete(args);
free(repaired_args_json);
return json_error_local("nostr_post failed to duplicate tags");
}
}
ensure_nip23_metadata_tags_local((int)kind->valuedouble, content->valuestring, &tags_dup);
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
content->valuestring,
tags_dup,
&publish_result);
cJSON_Delete(tags_dup);
cJSON_Delete(args);
free(repaired_args_json);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_post failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_post published");
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
if (publish_result.d_tag[0] != '\0') cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_tool_args_json_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char readme_path[PATH_MAX];
if (build_tool_path_local(ctx, "README.md", readme_path, sizeof(readme_path)) != 0) {
return json_error_local("failed to resolve README path");
}
size_t bytes_read = 0;
char* content = read_entire_file_local(readme_path, &bytes_read);
if (!content) {
return json_error_local("failed to read README.md");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
free(content);
return json_error_local("failed to create tags");
}
if (add_string_tag_local(tags, "d", "readme.md") != 0 ||
add_string_tag_local(tags,
"image",
"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg") != 0) {
cJSON_Delete(tags);
free(content);
return json_error_local("failed to set required tags");
}
ensure_nip23_metadata_tags_local(30023, content, &tags);
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(30023, content, tags, &publish_result);
cJSON_Delete(tags);
free(content);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_post_readme failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_post_readme published");
cJSON_AddStringToObject(out, "path", readme_path);
cJSON_AddNumberToObject(out, "bytes_read", (double)bytes_read);
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
if (publish_result.d_tag[0] != '\0') cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_tool_args_json_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* file = cJSON_GetObjectItemCaseSensitive(args, "file");
cJSON* title = cJSON_GetObjectItemCaseSensitive(args, "title");
cJSON* image = cJSON_GetObjectItemCaseSensitive(args, "image");
cJSON* summary = cJSON_GetObjectItemCaseSensitive(args, "summary");
if (!file || !cJSON_IsString(file) || !file->valuestring || file->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_file_md_to_longform_post requires string file");
}
if (title && !cJSON_IsString(title)) {
cJSON_Delete(args);
return json_error_local("nostr_file_md_to_longform_post title must be string when provided");
}
if (image && !cJSON_IsString(image)) {
cJSON_Delete(args);
return json_error_local("nostr_file_md_to_longform_post image must be string when provided");
}
if (summary && !cJSON_IsString(summary)) {
cJSON_Delete(args);
return json_error_local("nostr_file_md_to_longform_post summary must be string when provided");
}
char file_path[PATH_MAX];
if (build_tool_path_local(ctx, file->valuestring, file_path, sizeof(file_path)) != 0) {
cJSON_Delete(args);
return json_error_local("failed to resolve markdown file path");
}
size_t bytes_read = 0;
char* content = read_entire_file_local(file_path, &bytes_read);
if (!content) {
cJSON_Delete(args);
return json_error_local("failed to read markdown file");
}
char* d_tag = basename_lowercase_dup_local(file->valuestring);
if (!d_tag || d_tag[0] == '\0') {
free(d_tag);
free(content);
cJSON_Delete(args);
return json_error_local("failed to derive d tag from filename");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
free(d_tag);
free(content);
cJSON_Delete(args);
return json_error_local("failed to create tags");
}
if (add_string_tag_local(tags, "d", d_tag) != 0) {
free(d_tag);
cJSON_Delete(tags);
free(content);
cJSON_Delete(args);
return json_error_local("failed to set d tag");
}
if (title && title->valuestring && title->valuestring[0] != '\0') {
if (add_string_tag_local(tags, "title", title->valuestring) != 0) {
free(d_tag);
cJSON_Delete(tags);
free(content);
cJSON_Delete(args);
return json_error_local("failed to set title tag");
}
}
if (image && image->valuestring && image->valuestring[0] != '\0') {
if (add_string_tag_local(tags, "image", image->valuestring) != 0) {
free(d_tag);
cJSON_Delete(tags);
free(content);
cJSON_Delete(args);
return json_error_local("failed to set image tag");
}
}
if (summary && summary->valuestring && summary->valuestring[0] != '\0') {
if (add_string_tag_local(tags, "summary", summary->valuestring) != 0) {
free(d_tag);
cJSON_Delete(tags);
free(content);
cJSON_Delete(args);
return json_error_local("failed to set summary tag");
}
}
ensure_nip23_metadata_tags_local(30023, content, &tags);
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(30023, content, tags, &publish_result);
cJSON_Delete(tags);
free(content);
cJSON_Delete(args);
if (rc != 0) {
free(d_tag);
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_file_md_to_longform_post failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(d_tag);
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_file_md_to_longform_post published");
cJSON_AddStringToObject(out, "path", file_path);
cJSON_AddStringToObject(out, "d_tag", d_tag);
cJSON_AddNumberToObject(out, "bytes_read", (double)bytes_read);
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
free(d_tag);
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(d_tag);
nostr_handler_publish_result_free(&publish_result);
return json;
}

View File

@@ -0,0 +1,62 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdlib.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
char* execute_nostr_query(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error_local("invalid arguments JSON");
cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_ms");
if (!filter || !cJSON_IsObject(filter)) {
cJSON_Delete(args);
return json_error_local("nostr_query requires object filter");
}
cJSON* filter_dup = cJSON_Duplicate(filter, 1);
if (!filter_dup) {
cJSON_Delete(args);
return json_error_local("failed to duplicate filter");
}
int timeout_ms = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : 8000;
char* events_json = nostr_handler_query_json(filter_dup, timeout_ms);
cJSON_Delete(filter_dup);
cJSON_Delete(args);
if (!events_json) return json_error_local("nostr_query failed");
cJSON* out = cJSON_CreateObject();
if (!out) {
free(events_json);
return NULL;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events) {
cJSON_Delete(out);
return json_error_local("nostr_query returned invalid JSON");
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "events", events);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

View File

@@ -0,0 +1,94 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdlib.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
char* execute_nostr_relay_status(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON_Delete(args);
char* status_json = nostr_handler_relay_status_json();
if (!status_json) {
return json_error_local("nostr_relay_status failed");
}
cJSON* status = cJSON_Parse(status_json);
free(status_json);
if (!status) {
return json_error_local("nostr_relay_status returned invalid JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(status);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "status", status);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_relay_info(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* relay_url = cJSON_GetObjectItemCaseSensitive(args, "relay_url");
if (!relay_url || !cJSON_IsString(relay_url) || !relay_url->valuestring || relay_url->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("nostr_relay_info requires relay_url");
}
char* info_json = nostr_handler_relay_info_json(relay_url->valuestring);
cJSON_Delete(args);
if (!info_json) {
return json_error_local("nostr_relay_info failed");
}
cJSON* info = cJSON_Parse(info_json);
free(info_json);
if (!info || !cJSON_IsObject(info)) {
cJSON_Delete(info);
return json_error_local("nostr_relay_info returned invalid JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(info);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "info", info);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

View File

@@ -0,0 +1,302 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_hex_string_len_local(const char* s, size_t expected_len) {
if (!s) return 0;
if (strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
if (!((s[i] >= '0' && s[i] <= '9') ||
(s[i] >= 'a' && s[i] <= 'f') ||
(s[i] >= 'A' && s[i] <= 'F'))) {
return 0;
}
}
return 1;
}
static int add_string_tag_local(cJSON* tags, const char* key, const char* value) {
if (!tags || !cJSON_IsArray(tags) || !key || !value) return -1;
cJSON* tuple = cJSON_CreateArray();
if (!tuple) return -1;
cJSON_AddItemToArray(tuple, cJSON_CreateString(key));
cJSON_AddItemToArray(tuple, cJSON_CreateString(value));
cJSON_AddItemToArray(tags, tuple);
return 0;
}
static void free_string_array_heap_local(char** items, int count) {
if (!items) return;
for (int i = 0; i < count; i++) free(items[i]);
free(items);
}
char* execute_nostr_react(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(args, "event_id");
cJSON* event_pubkey = cJSON_GetObjectItemCaseSensitive(args, "event_pubkey");
cJSON* event_kind = cJSON_GetObjectItemCaseSensitive(args, "event_kind");
cJSON* reaction = cJSON_GetObjectItemCaseSensitive(args, "reaction");
if (!event_id || !cJSON_IsString(event_id) || !event_id->valuestring || !is_hex_string_len_local(event_id->valuestring, 64U) ||
!event_pubkey || !cJSON_IsString(event_pubkey) || !event_pubkey->valuestring || !is_hex_string_len_local(event_pubkey->valuestring, 64U)) {
cJSON_Delete(args);
return json_error_local("nostr_react requires event_id and event_pubkey as 64-char hex strings");
}
if (event_kind && !cJSON_IsNumber(event_kind)) {
cJSON_Delete(args);
return json_error_local("nostr_react event_kind must be an integer when provided");
}
if (reaction && !cJSON_IsString(reaction)) {
cJSON_Delete(args);
return json_error_local("nostr_react reaction must be a string when provided");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
cJSON_Delete(args);
return json_error_local("failed to create tags");
}
if (add_string_tag_local(tags, "e", event_id->valuestring) != 0 ||
add_string_tag_local(tags, "p", event_pubkey->valuestring) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("failed to add required reaction tags");
}
if (event_kind) {
char kind_buf[32];
snprintf(kind_buf, sizeof(kind_buf), "%d", (int)event_kind->valuedouble);
if (add_string_tag_local(tags, "k", kind_buf) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error_local("failed to add k tag");
}
}
const char* reaction_content = (reaction && reaction->valuestring && reaction->valuestring[0] != '\0') ? reaction->valuestring : "+";
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(7, reaction_content, tags, &publish_result);
cJSON_Delete(tags);
cJSON_Delete(args);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error_local("nostr_react failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_react published");
cJSON_AddStringToObject(out, "reaction", reaction_content);
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
char* execute_nostr_profile_get(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U)) {
cJSON_Delete(args);
return json_error_local("nostr_profile_get requires pubkey as 64-char hex string");
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
cJSON_Delete(args);
return json_error_local("failed to create profile query filter");
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey->valuestring));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 1);
char* events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
cJSON_Delete(args);
if (!events_json) {
return json_error_local("nostr_profile_get query failed");
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return json_error_local("nostr_profile_get returned invalid events JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(events);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
int found = cJSON_GetArraySize(events) > 0;
cJSON_AddBoolToObject(out, "found", found ? 1 : 0);
if (found) {
cJSON* ev = cJSON_GetArrayItem(events, 0);
cJSON* ev_dup = ev ? cJSON_Duplicate(ev, 1) : NULL;
if (ev_dup) {
cJSON_AddItemToObject(out, "event", ev_dup);
}
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
if (content && cJSON_IsString(content) && content->valuestring) {
cJSON* profile = cJSON_Parse(content->valuestring);
if (profile && cJSON_IsObject(profile)) {
cJSON_AddItemToObject(out, "profile", profile);
} else {
cJSON_Delete(profile);
cJSON_AddStringToObject(out, "profile_raw", content->valuestring);
}
}
}
cJSON_Delete(events);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* execute_nostr_nip05_lookup(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* identifier = cJSON_GetObjectItemCaseSensitive(args, "identifier");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
if (!identifier || !cJSON_IsString(identifier) || !identifier->valuestring || strchr(identifier->valuestring, '@') == NULL) {
cJSON_Delete(args);
return json_error_local("nostr_nip05_lookup requires identifier in user@domain format");
}
if (pubkey && (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U))) {
cJSON_Delete(args);
return json_error_local("nostr_nip05_lookup pubkey must be a 64-char hex string when provided");
}
char found_pubkey[65] = {0};
char** relays = NULL;
int relay_count = 0;
int rc = 0;
if (pubkey) {
rc = nostr_nip05_verify(identifier->valuestring,
pubkey->valuestring,
&relays,
&relay_count,
10);
} else {
rc = nostr_nip05_lookup(identifier->valuestring,
found_pubkey,
&relays,
&relay_count,
10);
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free_string_array_heap_local(relays, relay_count);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == NOSTR_SUCCESS ? 1 : 0);
cJSON_AddStringToObject(out, "identifier", identifier->valuestring);
if (pubkey) {
cJSON_AddBoolToObject(out, "verified", rc == NOSTR_SUCCESS ? 1 : 0);
cJSON_AddStringToObject(out, "pubkey", pubkey->valuestring);
} else if (rc == NOSTR_SUCCESS) {
cJSON_AddStringToObject(out, "pubkey", found_pubkey);
}
cJSON* relay_arr = cJSON_CreateArray();
if (!relay_arr) {
cJSON_Delete(out);
free_string_array_heap_local(relays, relay_count);
cJSON_Delete(args);
return NULL;
}
for (int i = 0; i < relay_count; i++) {
cJSON_AddItemToArray(relay_arr, cJSON_CreateString(relays[i] ? relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relay_arr);
if (rc != NOSTR_SUCCESS) {
cJSON_AddNumberToObject(out, "error_code", rc);
}
free_string_array_heap_local(relays, relay_count);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}

1447
src/tools/tool_skill.c Normal file

File diff suppressed because it is too large Load Diff

565
src/tools/tool_task.c Normal file
View File

@@ -0,0 +1,565 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "cjson/cJSON.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
static cJSON* parse_args_local(const char* args_json) {
const char* raw = args_json ? args_json : "{}";
cJSON* args = cJSON_Parse(raw);
if (!args || !cJSON_IsObject(args)) {
cJSON_Delete(args);
return NULL;
}
return args;
}
static int is_safe_relative_path_local(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
if (strstr(path, "..") != NULL) return 0;
if (strchr(path, '\\') != NULL) return 0;
return 1;
}
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
if (!is_safe_relative_path_local(rel_path)) return -1;
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int n = 0;
if (strcmp(cwd, ".") == 0) {
n = snprintf(out, out_size, "%s", rel_path);
} else {
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
}
if (n < 0 || (size_t)n >= out_size) return -1;
return 0;
}
static cJSON* tasks_create_empty_root_local(void) {
cJSON* root = cJSON_CreateObject();
cJSON* tasks = cJSON_CreateArray();
if (!root || !tasks) {
cJSON_Delete(root);
cJSON_Delete(tasks);
return NULL;
}
cJSON_AddItemToObject(root, "tasks", tasks);
cJSON_AddNumberToObject(root, "next_id", 1);
return root;
}
static const char* normalize_task_status_local(const char* status) {
if (!status) return NULL;
if (strcmp(status, "pending") == 0) return "pending";
if (strcmp(status, "active") == 0) return "active";
if (strcmp(status, "done") == 0) return "done";
return NULL;
}
static cJSON* tasks_load_root_local(const char* path) {
if (!path) return NULL;
FILE* fp = fopen(path, "rb");
if (!fp) {
if (access(path, F_OK) == 0) {
return NULL;
}
return tasks_create_empty_root_local();
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL;
}
char* buf = (char*)malloc((size_t)len + 1U);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)len, fp);
fclose(fp);
if (n != (size_t)len) {
free(buf);
return NULL;
}
buf[len] = '\0';
cJSON* root = cJSON_Parse(buf);
free(buf);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return NULL;
}
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
if (!tasks || !cJSON_IsArray(tasks)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "tasks");
cJSON_AddItemToObject(root, "tasks", cJSON_CreateArray());
}
cJSON* next_id = cJSON_GetObjectItemCaseSensitive(root, "next_id");
if (!next_id || !cJSON_IsNumber(next_id) || next_id->valuedouble < 1) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id");
cJSON_AddNumberToObject(root, "next_id", 1);
}
return root;
}
static int tasks_save_root_local(const char* path, cJSON* root) {
if (!path || !root) return -1;
char* raw = cJSON_PrintUnformatted(root);
if (!raw) return -1;
FILE* fp = fopen(path, "wb");
if (!fp) {
free(raw);
return -1;
}
size_t len = strlen(raw);
size_t n = fwrite(raw, 1, len, fp);
fclose(fp);
free(raw);
return (n == len) ? 0 : -1;
}
static cJSON* task_find_by_id_local(cJSON* tasks, int id, int* out_index) {
if (!tasks || !cJSON_IsArray(tasks) || id <= 0) return NULL;
int n = cJSON_GetArraySize(tasks);
for (int i = 0; i < n; i++) {
cJSON* task = cJSON_GetArrayItem(tasks, i);
cJSON* tid = task ? cJSON_GetObjectItemCaseSensitive(task, "id") : NULL;
if (tid && cJSON_IsNumber(tid) && (int)tid->valuedouble == id) {
if (out_index) *out_index = i;
return task;
}
}
return NULL;
}
char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
if (!action || !cJSON_IsString(action) || !action->valuestring || action->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error_local("task_manage requires string action");
}
char tasks_path[PATH_MAX];
if (build_tool_path_local(ctx, "tasks.json", tasks_path, sizeof(tasks_path)) != 0) {
cJSON_Delete(args);
return json_error_local("failed to resolve tasks file path");
}
cJSON* root = tasks_load_root_local(tasks_path);
if (!root) {
cJSON_Delete(args);
return json_error_local("failed to load tasks file");
}
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
if (!tasks || !cJSON_IsArray(tasks)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "tasks");
tasks = cJSON_CreateArray();
cJSON_AddItemToObject(root, "tasks", tasks);
}
cJSON* next_id_item = cJSON_GetObjectItemCaseSensitive(root, "next_id");
int next_id = (next_id_item && cJSON_IsNumber(next_id_item) && next_id_item->valuedouble >= 1)
? (int)next_id_item->valuedouble
: 1;
const char* action_s = action->valuestring;
int mutated = 0;
if (strcmp(action_s, "list") == 0) {
} else if (strcmp(action_s, "add") == 0) {
cJSON* text = cJSON_GetObjectItemCaseSensitive(args, "text");
cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status");
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage add requires non-empty string text");
}
const char* normalized_status = "pending";
if (status && !cJSON_IsNull(status)) {
if (!cJSON_IsString(status) || !status->valuestring) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage add status must be string when provided");
}
normalized_status = normalize_task_status_local(status->valuestring);
if (!normalized_status) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage add status must be pending, active, or done");
}
}
cJSON* task = cJSON_CreateObject();
if (!task) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("allocation failure");
}
time_t now = time(NULL);
cJSON_AddNumberToObject(task, "id", next_id++);
cJSON_AddStringToObject(task, "text", text->valuestring);
cJSON_AddStringToObject(task, "status", normalized_status);
cJSON_AddNumberToObject(task, "created_at", (double)now);
cJSON_AddNumberToObject(task, "updated_at", (double)now);
cJSON_AddItemToArray(tasks, task);
mutated = 1;
} else if (strcmp(action_s, "update") == 0) {
cJSON* id = cJSON_GetObjectItemCaseSensitive(args, "id");
cJSON* text = cJSON_GetObjectItemCaseSensitive(args, "text");
cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status");
if (!id || !cJSON_IsNumber(id) || id->valuedouble < 1) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage update requires integer id");
}
if ((!text || cJSON_IsNull(text)) && (!status || cJSON_IsNull(status))) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage update requires text and/or status");
}
cJSON* task = task_find_by_id_local(tasks, (int)id->valuedouble, NULL);
if (!task) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task not found");
}
if (text && !cJSON_IsNull(text)) {
if (!cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage update text must be non-empty string when provided");
}
cJSON_DeleteItemFromObjectCaseSensitive(task, "text");
cJSON_AddStringToObject(task, "text", text->valuestring);
mutated = 1;
}
if (status && !cJSON_IsNull(status)) {
if (!cJSON_IsString(status) || !status->valuestring) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage update status must be string when provided");
}
const char* normalized_status = normalize_task_status_local(status->valuestring);
if (!normalized_status) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage update status must be pending, active, or done");
}
cJSON_DeleteItemFromObjectCaseSensitive(task, "status");
cJSON_AddStringToObject(task, "status", normalized_status);
mutated = 1;
}
if (mutated) {
time_t now = time(NULL);
cJSON_DeleteItemFromObjectCaseSensitive(task, "updated_at");
cJSON_AddNumberToObject(task, "updated_at", (double)now);
}
} else if (strcmp(action_s, "remove") == 0) {
cJSON* id = cJSON_GetObjectItemCaseSensitive(args, "id");
if (!id || !cJSON_IsNumber(id) || id->valuedouble < 1) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage remove requires integer id");
}
int idx = -1;
cJSON* task = task_find_by_id_local(tasks, (int)id->valuedouble, &idx);
if (!task || idx < 0) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task not found");
}
cJSON_DeleteItemFromArray(tasks, idx);
mutated = 1;
} else if (strcmp(action_s, "clear") == 0) {
cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status");
if (!status || cJSON_IsNull(status)) {
while (cJSON_GetArraySize(tasks) > 0) {
cJSON_DeleteItemFromArray(tasks, 0);
}
mutated = 1;
} else {
if (!cJSON_IsString(status) || !status->valuestring) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage clear status must be string when provided");
}
const char* normalized_status = normalize_task_status_local(status->valuestring);
if (!normalized_status) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage clear status must be pending, active, or done");
}
int i = 0;
while (i < cJSON_GetArraySize(tasks)) {
cJSON* task = cJSON_GetArrayItem(tasks, i);
cJSON* task_status = task ? cJSON_GetObjectItemCaseSensitive(task, "status") : NULL;
const char* task_status_s = (task_status && cJSON_IsString(task_status) && task_status->valuestring)
? task_status->valuestring
: "pending";
if (strcmp(task_status_s, normalized_status) == 0) {
cJSON_DeleteItemFromArray(tasks, i);
mutated = 1;
} else {
i++;
}
}
}
} else if (strcmp(action_s, "replace") == 0) {
cJSON* tasks_in = cJSON_GetObjectItemCaseSensitive(args, "tasks");
if (!tasks_in || !cJSON_IsArray(tasks_in)) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage replace requires array tasks");
}
while (cJSON_GetArraySize(tasks) > 0) {
cJSON_DeleteItemFromArray(tasks, 0);
}
int n = cJSON_GetArraySize(tasks_in);
time_t now = time(NULL);
for (int i = 0; i < n; i++) {
cJSON* text = cJSON_GetArrayItem(tasks_in, i);
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
continue;
}
cJSON* task = cJSON_CreateObject();
if (!task) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("allocation failure");
}
cJSON_AddNumberToObject(task, "id", next_id++);
cJSON_AddStringToObject(task, "text", text->valuestring);
cJSON_AddStringToObject(task, "status", "pending");
cJSON_AddNumberToObject(task, "created_at", (double)now);
cJSON_AddNumberToObject(task, "updated_at", (double)now);
cJSON_AddItemToArray(tasks, task);
}
mutated = 1;
} else {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("task_manage action must be one of: list, add, update, remove, clear, replace");
}
if (mutated) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id");
cJSON_AddNumberToObject(root, "next_id", next_id);
if (tasks_save_root_local(tasks_path, root) != 0) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("failed to save tasks file");
}
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
cJSON_Delete(root);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "action", action_s);
cJSON_AddStringToObject(out, "path", tasks_path);
cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks));
cJSON* tasks_dup = cJSON_Duplicate(tasks, 1);
if (!tasks_dup) {
cJSON_Delete(args);
cJSON_Delete(root);
cJSON_Delete(out);
return NULL;
}
cJSON_AddItemToObject(out, "tasks", tasks_dup);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(args);
cJSON_Delete(root);
return json;
}
char* execute_task_list(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
char* listed = execute_task_manage(ctx, "{\"action\":\"list\"}");
if (!listed) return json_error_local("task_list failed to list tasks");
cJSON* listed_root = cJSON_Parse(listed);
if (!listed_root || !cJSON_IsObject(listed_root)) {
cJSON_Delete(listed_root);
free(listed);
return json_error_local("task_list received invalid task_manage response");
}
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(listed_root, "tasks");
int tcount = (tasks && cJSON_IsArray(tasks)) ? cJSON_GetArraySize(tasks) : 0;
size_t cap = 512U;
char* content = (char*)malloc(cap);
if (!content) {
cJSON_Delete(listed_root);
free(listed);
return NULL;
}
size_t used = 0U;
int n = snprintf(content + used,
cap - used,
"## Current Task List\n\n### Current Tasks\n\nYour active task list - short-term working memory for tracking plan steps.\n\n");
if (n < 0) {
free(content);
cJSON_Delete(listed_root);
free(listed);
return NULL;
}
used += (size_t)n;
int written = 0;
for (int i = 0; i < tcount; i++) {
cJSON* task = cJSON_GetArrayItem(tasks, i);
cJSON* id = task ? cJSON_GetObjectItemCaseSensitive(task, "id") : NULL;
cJSON* text = task ? cJSON_GetObjectItemCaseSensitive(task, "text") : NULL;
cJSON* status = task ? cJSON_GetObjectItemCaseSensitive(task, "status") : NULL;
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
continue;
}
const char* status_s = (status && cJSON_IsString(status) && status->valuestring) ? status->valuestring : "pending";
const char* mark = " ";
if (strcmp(status_s, "done") == 0) mark = "x";
else if (strcmp(status_s, "active") == 0) mark = "-";
int id_value = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : (i + 1);
int needed = snprintf(NULL, 0, "- [%s] %d. %s\n", mark, id_value, text->valuestring);
if (needed < 0) continue;
if (used + (size_t)needed + 1U > cap) {
size_t next = cap;
while (used + (size_t)needed + 1U > next) next *= 2U;
char* grown = (char*)realloc(content, next);
if (!grown) {
free(content);
cJSON_Delete(listed_root);
free(listed);
return NULL;
}
content = grown;
cap = next;
}
snprintf(content + used, cap - used, "- [%s] %d. %s\n", mark, id_value, text->valuestring);
used += (size_t)needed;
written++;
}
if (written == 0) {
const char* fallback = "- [ ] No active tasks yet.\n";
size_t need = strlen(fallback);
if (used + need + 1U > cap) {
size_t next = cap;
while (used + need + 1U > next) next *= 2U;
char* grown = (char*)realloc(content, next);
if (!grown) {
free(content);
cJSON_Delete(listed_root);
free(listed);
return NULL;
}
content = grown;
cap = next;
}
memcpy(content + used, fallback, need + 1U);
used += need;
}
cJSON* args = parse_args_local(args_json);
if (!args) {
free(content);
cJSON_Delete(listed_root);
free(listed);
return json_error_local("invalid arguments JSON");
}
cJSON_Delete(args);
cJSON* out = cJSON_CreateObject();
if (!out) {
free(content);
cJSON_Delete(listed_root);
free(listed);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "content", content);
cJSON* tasks_dup = tasks ? cJSON_Duplicate(tasks, 1) : cJSON_CreateArray();
if (!tasks_dup) tasks_dup = cJSON_CreateArray();
cJSON_AddItemToObject(out, "tasks", tasks_dup);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(content);
cJSON_Delete(listed_root);
free(listed);
return json;
}

3
src/tools/tools_common.c Normal file
View File

@@ -0,0 +1,3 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"

191
src/tools/tools_dispatch.c Normal file
View File

@@ -0,0 +1,191 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
static char* json_error(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
int tools_init_legacy(tools_context_t* ctx, didactyl_config_t* cfg) {
if (!ctx || !cfg) return -1;
memset(ctx, 0, sizeof(*ctx));
ctx->cfg = cfg;
ctx->trigger_manager = NULL;
ctx->template_sender_tier = 0;
ctx->template_current_user_message = NULL;
return 0;
}
void tools_cleanup_legacy(tools_context_t* ctx) {
if (!ctx) return;
memset(ctx, 0, sizeof(*ctx));
}
char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const char* args_json) {
if (!tool_name) return json_error("missing tool name");
if (strcmp(tool_name, "nostr_post") == 0) {
return execute_nostr_post(args_json);
}
if (strcmp(tool_name, "nostr_delete") == 0) {
return execute_nostr_delete(args_json);
}
if (strcmp(tool_name, "nostr_react") == 0) {
return execute_nostr_react(args_json);
}
if (strcmp(tool_name, "nostr_profile_get") == 0) {
return execute_nostr_profile_get(args_json);
}
if (strcmp(tool_name, "nostr_relay_status") == 0) {
return execute_nostr_relay_status(args_json);
}
if (strcmp(tool_name, "nostr_nip05_lookup") == 0) {
return execute_nostr_nip05_lookup(args_json);
}
if (strcmp(tool_name, "nostr_encode") == 0) {
return execute_nostr_encode(args_json);
}
if (strcmp(tool_name, "nostr_decode") == 0) {
return execute_nostr_decode(args_json);
}
if (strcmp(tool_name, "nostr_dm_send") == 0) {
return execute_nostr_dm_send(args_json);
}
if (strcmp(tool_name, "nostr_relay_info") == 0) {
return execute_nostr_relay_info(args_json);
}
if (strcmp(tool_name, "nostr_encrypt") == 0) {
return execute_nostr_encrypt(ctx, args_json);
}
if (strcmp(tool_name, "nostr_decrypt") == 0) {
return execute_nostr_decrypt(ctx, args_json);
}
if (strcmp(tool_name, "nostr_dm_send_nip17") == 0) {
return execute_nostr_dm_send_nip17(args_json);
}
if (strcmp(tool_name, "nostr_list_manage") == 0) {
return execute_nostr_list_manage(ctx, args_json);
}
if (strcmp(tool_name, "nostr_query") == 0) {
return execute_nostr_query(args_json);
}
if (strcmp(tool_name, "agent_version") == 0) {
return execute_agent_version(args_json);
}
if (strcmp(tool_name, "nostr_pubkey") == 0) {
return execute_nostr_pubkey(ctx, args_json);
}
if (strcmp(tool_name, "nostr_npub") == 0) {
return execute_nostr_npub(ctx, args_json);
}
if (strcmp(tool_name, "my_pubkey") == 0) {
return execute_nostr_pubkey(ctx, args_json);
}
if (strcmp(tool_name, "my_npub") == 0) {
return execute_nostr_npub(ctx, args_json);
}
if (strcmp(tool_name, "local_http_fetch") == 0) {
return execute_local_http_fetch(ctx, args_json);
}
if (strcmp(tool_name, "local_shell_exec") == 0) {
return execute_local_shell_exec(ctx, args_json);
}
if (strcmp(tool_name, "local_file_read") == 0) {
return execute_local_file_read(ctx, args_json);
}
if (strcmp(tool_name, "local_file_write") == 0) {
return execute_local_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, "skill_edit") == 0) {
return execute_skill_edit(ctx, args_json);
}
if (strcmp(tool_name, "trigger_list") == 0) {
return execute_trigger_list(ctx, args_json);
}
if (strcmp(tool_name, "tool_list") == 0) {
return execute_tool_list(ctx, args_json);
}
if (strcmp(tool_name, "model_get") == 0) {
return execute_model_get(args_json);
}
if (strcmp(tool_name, "model_set") == 0) {
return execute_model_set(ctx, args_json);
}
if (strcmp(tool_name, "model_list") == 0) {
return execute_model_list(args_json);
}
if (strcmp(tool_name, "nostr_post_readme") == 0) {
return execute_nostr_post_readme(ctx, args_json);
}
if (strcmp(tool_name, "nostr_file_md_to_longform_post") == 0) {
return execute_nostr_file_md_to_longform_post(ctx, args_json);
}
if (strcmp(tool_name, "task_manage") == 0) {
return execute_task_manage(ctx, args_json);
}
if (strcmp(tool_name, "admin_identity") == 0) {
return execute_admin_identity(ctx, args_json);
}
if (strcmp(tool_name, "nostr_admin_profile") == 0) {
return execute_nostr_admin_profile(ctx, args_json);
}
if (strcmp(tool_name, "nostr_admin_contacts") == 0) {
return execute_nostr_admin_contacts(ctx, args_json);
}
if (strcmp(tool_name, "nostr_admin_relays") == 0) {
return execute_nostr_admin_relays(ctx, args_json);
}
if (strcmp(tool_name, "nostr_admin_notes") == 0) {
return execute_nostr_admin_notes(ctx, args_json);
}
if (strcmp(tool_name, "task_list") == 0) {
return execute_task_list(ctx, args_json);
}
if (strcmp(tool_name, "message_current") == 0) {
return execute_message_current(ctx, args_json);
}
if (strcmp(tool_name, "agent_identity") == 0) {
return execute_agent_identity(ctx, args_json);
}
return json_error("unknown tool");
}
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) {
return tools_init_legacy(ctx, cfg);
}
void tools_cleanup(tools_context_t* ctx) {
tools_cleanup_legacy(ctx);
}
char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json) {
return tools_execute_legacy(ctx, tool_name, args_json);
}

View File

@@ -0,0 +1,56 @@
#ifndef DIDACTYL_TOOLS_INTERNAL_H
#define DIDACTYL_TOOLS_INTERNAL_H
#include "tools.h"
int tools_init_legacy(tools_context_t* ctx, didactyl_config_t* cfg);
void tools_cleanup_legacy(tools_context_t* ctx);
char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx);
char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const char* args_json);
char* execute_tool_list(tools_context_t* ctx, const char* args_json);
char* execute_trigger_list(tools_context_t* ctx, const char* args_json);
char* execute_message_current(tools_context_t* ctx, const char* args_json);
char* execute_agent_identity(tools_context_t* ctx, const char* args_json);
char* execute_agent_version(const char* args_json);
char* execute_model_get(const char* args_json);
char* execute_model_set(tools_context_t* ctx, const char* args_json);
char* execute_model_list(const char* args_json);
char* execute_nostr_query(const char* args_json);
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json);
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json);
char* execute_nostr_encode(const char* args_json);
char* execute_nostr_decode(const char* args_json);
char* execute_nostr_react(const char* args_json);
char* execute_nostr_profile_get(const char* args_json);
char* execute_nostr_nip05_lookup(const char* args_json);
char* execute_nostr_relay_status(const char* args_json);
char* execute_nostr_relay_info(const char* args_json);
char* execute_nostr_dm_send(const char* args_json);
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json);
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
char* execute_nostr_dm_send_nip17(const char* args_json);
char* execute_nostr_delete(const char* args_json);
char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_json);
char* execute_admin_identity(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json);
char* execute_task_list(tools_context_t* ctx, const char* args_json);
char* execute_task_manage(tools_context_t* ctx, const char* args_json);
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json);
char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json);
char* execute_local_file_read(tools_context_t* ctx, const char* args_json);
char* execute_local_file_write(tools_context_t* ctx, const char* args_json);
char* execute_nostr_post(const char* args_json);
char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json);
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json);
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
char* execute_skill_list(tools_context_t* ctx, const char* args_json);
char* execute_skill_adopt(tools_context_t* ctx, const char* args_json);
char* execute_skill_remove(tools_context_t* ctx, const char* args_json);
char* execute_skill_edit(tools_context_t* ctx, const char* args_json);
char* execute_skill_search(const char* args_json);
#endif

1159
src/tools/tools_schema.c Normal file

File diff suppressed because it is too large Load Diff