Compare commits

..
3 Commits
36 changed files with 22433 additions and 6073 deletions
+7 -2
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 \
+18 -1
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
+2 -2
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.64
## Current Status — v0.0.67
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.64Just keeping things fresh
> Last release update: v0.0.67Add agent self-context subscriptions, tools, aliases, and prompt template sections
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+70
View File
@@ -31,6 +31,25 @@
"temperature": 0.7 // sampling temperature (0.0 2.0)
},
// ─── Tools & Runtime Limits ───────────────────────────────────────
// Centralized turn and timeout limits used by DM agent loops,
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
"tools": {
"enabled": true,
"max_turns": 8, // default max turns for normal agent tool loops
"trigger_max_turns": 6, // max turns for triggered-skill executions
"api_default_max_turns": 4, // default when HTTP body omits max_turns
"api_max_turns_ceiling": 16, // hard cap applied to API-provided max_turns
"local_http_fetch_default_timeout_seconds": 20,
"local_http_fetch_max_timeout_seconds": 120,
"shell": {
"enabled": true,
"timeout_seconds": 30,
"max_output_bytes": 65536,
"working_directory": "."
}
},
// ─── Security Tiers ───────────────────────────────────────────────
// Controls who can interact with the agent and what they can do.
"security": {
@@ -246,6 +265,57 @@
]
},
// Kind 31124: Private triggered skill — webhook-echo
// Example webhook trigger that emits a fixed DM response.
{
"kind": 31124,
"content": "When this webhook trigger fires, DM the admin with exactly: WEBHOOK_SOURCE_POC_OK",
"tags": [
["d", "webhook-echo"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC webhook trigger skill"],
["trigger", "webhook"],
["filter", "{}"],
["action", "llm"],
["enabled", "true"]
]
},
// Kind 31124: Private triggered skill — cron_poc
// Example cron trigger that runs every minute.
{
"kind": 31124,
"content": "DM the administrator with the current time, and tell him that your cron skill triggered.",
"tags": [
["d", "cron_poc"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC cron trigger skill"],
["trigger", "cron"],
["filter", "*/1 * * * *"],
["action", "template"],
["enabled", "true"]
]
},
// Kind 31124: Private triggered skill — chain_from_webhook_poc
// Example chain trigger that runs after webhook-echo completes.
{
"kind": 31124,
"content": "LOG: chain_from_webhook_poc fired",
"tags": [
["d", "chain_from_webhook_poc"],
["app", "didactyl"],
["scope", "private"],
["description", "PoC chain trigger skill"],
["trigger", "chain"],
["filter", "webhook-echo"],
["action", "template"],
["enabled", "true"]
]
},
// Kind 10123: Skill adoption list
// References which public skills this agent has adopted.
{
+14152
View File
File diff suppressed because one or more lines are too long
+25
View File
@@ -26,6 +26,31 @@
tool: nostr_admin_notes
skip_if_empty: true
- section: agent_identity
role: system
tool: agent_identity
skip_if_empty: true
- section: agent_profile
role: system
tool: nostr_agent_profile
skip_if_empty: true
- section: agent_contacts
role: system
tool: nostr_agent_contacts
skip_if_empty: true
- section: agent_relays
role: system
tool: nostr_agent_relays
skip_if_empty: true
- section: agent_notes
role: system
tool: nostr_agent_notes
skip_if_empty: true
- section: tasks
role: system
tool: task_list
+204
View File
@@ -0,0 +1,204 @@
# Agent Self-Context: Know Thyself
## Problem
When the agent is asked about itself — its profile, contacts, relays, or recent notes — it has no idea. The context template currently injects **administrator** identity/profile/contacts/relays/notes into the system prompt, but nothing equivalent for the **agent's own** Nostr identity beyond a bare pubkey+npub from `agent_identity`.
The agent publishes its own kind 0 (profile), kind 3 (contacts), kind 10002 (relays), and kind 1 (notes) at startup, but never subscribes to or caches those events for self-awareness.
## Solution Overview
Mirror the admin context pattern for the agent itself:
1. **Subscribe** to the agent's own kind 0/3/10002/1 events from relays
2. **Cache** them in `nostr_handler.c` (same pattern as `g_admin_kind0_json` etc.)
3. **Expose** them via new `nostr_handler_get_agent_*` API functions
4. **Create context tools** that format the cached data into context blocks
5. **Add to context template** so the agent always knows about itself
6. **Add callable tool aliases** like `my_kind0_profile` for on-demand use
```mermaid
flowchart TD
A[Startup: publish kind 0/3/10002/1] --> B[Subscribe to own events]
B --> C[Cache in nostr_handler globals]
C --> D[Context tools read cache]
D --> E[Context template injects into system prompt]
D --> F[LLM can call tools on-demand]
subgraph Admin Context - existing
G[g_admin_kind0_json]
H[g_admin_kind3 contacts]
I[g_admin_kind10002_json]
J[g_admin_kind1_notes]
end
subgraph Agent Context - new
K[g_agent_kind0_json]
L[g_agent_kind3 contacts]
M[g_agent_kind10002_json]
N[g_agent_kind1_notes]
end
```
## Detailed Changes
### 1. nostr_handler.c — Agent Self-Context Cache
Add new static globals mirroring the admin pattern:
```c
static char* g_agent_kind0_json = NULL; // kind 0 profile JSON
static char* g_agent_kind10002_json = NULL; // kind 10002 relay list JSON
static char** g_agent_kind3_contacts = NULL; // kind 3 contact pubkeys
static int g_agent_kind3_contact_count = 0;
static admin_kind1_note_t* g_agent_kind1_notes = NULL; // reuse struct
static int g_agent_kind1_note_count = 0;
```
### 2. nostr_handler.c — Agent Self-Context Subscription
Create `nostr_handler_subscribe_agent_context()` that subscribes to kinds 0, 3, 10002, 1 filtered by the agent's own pubkey. This is separate from the self-skills subscription (which handles 31123/31124/10123).
The callback `on_agent_context_event()` will parse and cache events using the same logic as `on_admin_context_event()`.
**Alternative considered:** Expanding `nostr_handler_subscribe_self_skills()` to include these kinds. Rejected because the self-skills sub has different EOSE handling and a callback for skill loading. Keeping them separate is cleaner.
### 3. nostr_handler.h — New API Functions
```c
int nostr_handler_subscribe_agent_context(void);
char* nostr_handler_get_agent_kind0_context(void);
char* nostr_handler_get_agent_kind3_context(void);
char* nostr_handler_get_agent_kind10002_context(void);
char* nostr_handler_get_agent_kind1_notes_context(void);
```
### 4. main.c — Call Agent Context Subscription at Startup
Add `nostr_handler_subscribe_agent_context()` call after admin context subscription, before self-skills subscription.
### 5. tool_agent.c — New Context Tools
Create four new tool functions following the exact pattern from `tool_admin.c`:
| Tool Name | Description | Data Source |
|-----------|-------------|-------------|
| `nostr_agent_profile` | Agent's kind 0 profile metadata | `nostr_handler_get_agent_kind0_context()` |
| `nostr_agent_contacts` | Agent's kind 3 contact list | `nostr_handler_get_agent_kind3_context()` |
| `nostr_agent_relays` | Agent's kind 10002 relay list | `nostr_handler_get_agent_kind10002_context()` |
| `nostr_agent_notes` | Agent's recent kind 1 notes | `nostr_handler_get_agent_kind1_notes_context()` |
Each returns a `content` field with markdown-formatted context, e.g.:
```
## Agent Kind 0 Profile (source: nostr kind 0)
Agent kind 0 profile content (JSON): {"name":"Didactyl Agent","display_name":"Didactyl",...}
```
### 6. tools_internal.h — Declare New Functions
```c
char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json);
```
### 7. tools_dispatch.c — Register + Aliases
Add dispatch entries:
```c
if (strcmp(tool_name, "nostr_agent_profile") == 0) return execute_nostr_agent_profile(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_contacts") == 0) return execute_nostr_agent_contacts(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_relays") == 0) return execute_nostr_agent_relays(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_notes") == 0) return execute_nostr_agent_notes(ctx, args_json);
// Friendly aliases
if (strcmp(tool_name, "my_kind0_profile") == 0) return execute_nostr_agent_profile(ctx, args_json);
if (strcmp(tool_name, "my_contacts") == 0) return execute_nostr_agent_contacts(ctx, args_json);
if (strcmp(tool_name, "my_relays") == 0) return execute_nostr_agent_relays(ctx, args_json);
if (strcmp(tool_name, "my_notes") == 0) return execute_nostr_agent_notes(ctx, args_json);
```
### 8. tools_schema.c — OpenAI Function Schemas
Add 8 new tool schemas (4 canonical + 4 aliases), all with empty parameters (no-arg tools), following the pattern of `admin_identity`/`nostr_admin_profile` etc.
### 9. Context Template Update
Update the kind 31120 soul content in `config.jsonc` and `context_template.md` to add agent sections **after** admin sections:
```yaml
- section: admin_notes
role: system
tool: nostr_admin_notes
skip_if_empty: true
# NEW: Agent self-context sections
- section: agent_identity
role: system
tool: agent_identity
skip_if_empty: true
- section: agent_profile
role: system
tool: nostr_agent_profile
skip_if_empty: true
- section: agent_contacts
role: system
tool: nostr_agent_contacts
skip_if_empty: true
- section: agent_relays
role: system
tool: nostr_agent_relays
skip_if_empty: true
- section: agent_notes
role: system
tool: nostr_agent_notes
skip_if_empty: true
- section: tasks
role: system
tool: task_list
skip_if_empty: true
```
### 10. nostr_handler.c Cleanup
Add cleanup for agent context globals in `nostr_handler_cleanup()`, mirroring `free_admin_context_locked()`.
## File Change Summary
| File | Change Type | Description |
|------|-------------|-------------|
| `src/nostr_handler.h` | Modify | Add 5 new function declarations |
| `src/nostr_handler.c` | Modify | Add agent context cache globals, subscription, event handler, getter functions, cleanup |
| `src/main.c` | Modify | Call `nostr_handler_subscribe_agent_context()` at startup |
| `src/tools/tool_agent.c` | Modify | Add 4 new context tool execute functions |
| `src/tools/tools_internal.h` | Modify | Declare 4 new execute functions |
| `src/tools/tools_dispatch.c` | Modify | Add 8 dispatch entries (4 tools + 4 aliases) |
| `src/tools/tools_schema.c` | Modify | Add 8 OpenAI function schemas |
| `config.jsonc` | Modify | Update kind 31120 template section |
| `config.jsonc.example` | Modify | Update kind 31120 template section |
| `context_template.md` | Modify | Add agent_* sections after admin_* sections |
## Context Token Impact
Each agent context section adds roughly the same token count as its admin counterpart:
- agent_identity: ~40 tokens (already exists, just adding to template)
- agent_profile: ~80-150 tokens (depends on profile richness)
- agent_contacts: ~50-200 tokens (depends on contact count)
- agent_relays: ~50-100 tokens
- agent_notes: ~100-300 tokens (depends on note count/length)
Total additional context: ~320-790 tokens. With `skip_if_empty: true`, empty sections cost 0 tokens.
## Design Decisions
1. **Separate subscription vs expanding self-skills sub**: Separate is cleaner — different EOSE semantics, different callback needs.
2. **Cache from relay vs read from config**: Cache from relay is more accurate (reflects what's actually published, not just what config says). The startup_events in config are the *intent*; the relay data is the *reality*.
3. **Alias naming**: `my_kind0_profile` matches the existing `my_npub`/`my_pubkey` pattern. Also adding `my_contacts`, `my_relays`, `my_notes` for consistency.
4. **No new config section needed**: The agent context subscription is unconditional — an agent should always know about itself. No `agent_context.enabled` toggle needed.
+285
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
+4 -2
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"
@@ -1987,7 +1987,9 @@ void agent_on_trigger(const char* skill_d_tag,
free(system_prompt);
free(user_prompt);
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 6;
int max_turns = g_cfg->tools.trigger_max_turns > 0
? g_cfg->tools.trigger_max_turns
: (g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8);
for (int turn = 0; turn < max_turns; turn++) {
char* messages_json = cJSON_PrintUnformatted(messages);
+54
View File
@@ -222,12 +222,36 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled");
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns");
cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns");
cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns");
cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling");
cJSON* local_http_fetch_default_timeout_seconds =
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds");
cJSON* local_http_fetch_max_timeout_seconds =
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_max_timeout_seconds");
if (enabled && cJSON_IsBool(enabled)) {
config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
if (max_turns && cJSON_IsNumber(max_turns)) {
config->tools.max_turns = (int)max_turns->valuedouble;
}
if (trigger_max_turns && cJSON_IsNumber(trigger_max_turns)) {
config->tools.trigger_max_turns = (int)trigger_max_turns->valuedouble;
}
if (api_default_max_turns && cJSON_IsNumber(api_default_max_turns)) {
config->tools.api_default_max_turns = (int)api_default_max_turns->valuedouble;
}
if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) {
config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble;
}
if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) {
config->tools.local_http_fetch_default_timeout_seconds =
(int)local_http_fetch_default_timeout_seconds->valuedouble;
}
if (local_http_fetch_max_timeout_seconds && cJSON_IsNumber(local_http_fetch_max_timeout_seconds)) {
config->tools.local_http_fetch_max_timeout_seconds =
(int)local_http_fetch_max_timeout_seconds->valuedouble;
}
cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
if (!shell || !cJSON_IsObject(shell)) {
@@ -255,6 +279,31 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
return -1;
}
if (config->tools.max_turns < 1) {
config->tools.max_turns = 8;
}
if (config->tools.trigger_max_turns < 1) {
config->tools.trigger_max_turns = config->tools.max_turns;
}
if (config->tools.api_default_max_turns < 1) {
config->tools.api_default_max_turns = config->tools.max_turns;
}
if (config->tools.api_max_turns_ceiling < 1) {
config->tools.api_max_turns_ceiling = 16;
}
if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) {
config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling;
}
if (config->tools.local_http_fetch_default_timeout_seconds < 1) {
config->tools.local_http_fetch_default_timeout_seconds = 20;
}
if (config->tools.local_http_fetch_max_timeout_seconds < 1) {
config->tools.local_http_fetch_max_timeout_seconds = 120;
}
if (config->tools.local_http_fetch_default_timeout_seconds > config->tools.local_http_fetch_max_timeout_seconds) {
config->tools.local_http_fetch_default_timeout_seconds = config->tools.local_http_fetch_max_timeout_seconds;
}
return 0;
}
@@ -746,6 +795,11 @@ int config_load(const char* path, didactyl_config_t* config) {
config->tools.enabled = 1;
config->tools.max_turns = 8;
config->tools.trigger_max_turns = 6;
config->tools.api_default_max_turns = 4;
config->tools.api_max_turns_ceiling = 16;
config->tools.local_http_fetch_default_timeout_seconds = 20;
config->tools.local_http_fetch_max_timeout_seconds = 120;
config->tools.shell.enabled = 1;
config->tools.shell.timeout_seconds = 30;
config->tools.shell.max_output_bytes = 65536;
+5
View File
@@ -47,6 +47,11 @@ typedef struct {
typedef struct {
int enabled;
int max_turns;
int trigger_max_turns;
int api_default_max_turns;
int api_max_turns_ceiling;
int local_http_fetch_default_timeout_seconds;
int local_http_fetch_max_timeout_seconds;
shell_tools_config_t shell;
} tools_config_t;
+13 -2
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"
@@ -755,8 +755,13 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
const char* tool_limit_message) {
if (!convo || !cJSON_IsArray(convo)) return NULL;
int max_ceiling = 16;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) {
max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling;
}
if (max_turns < 1) max_turns = 1;
if (max_turns > 16) max_turns = 16;
if (max_turns > max_ceiling) max_turns = max_ceiling;
char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
if (!tools_json) {
@@ -888,6 +893,9 @@ static cJSON* run_prompt_with_tools(cJSON* body) {
}
int max_turns = 4;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
}
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
max_turns = (int)max_turns_node->valuedouble;
@@ -1104,6 +1112,9 @@ static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_me
}
int max_turns = 4;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
}
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
max_turns = (int)max_turns_node->valuedouble;
+7 -1
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"
@@ -284,6 +284,12 @@ int main(int argc, char** argv) {
}
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
DEBUG_INFO("[didactyl] startup phase: subscribe agent self context begin");
if (nostr_handler_subscribe_agent_context() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe agent self context failed (continuing)");
}
DEBUG_INFO("[didactyl] startup phase: subscribe agent self context end");
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
if (nostr_handler_subscribe_self_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
+2 -2
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 64
#define DIDACTYL_VERSION "v0.0.64"
#define DIDACTYL_VERSION_PATCH 67
#define DIDACTYL_VERSION "v0.0.67"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+441
View File
@@ -42,6 +42,13 @@ typedef struct {
static admin_kind1_note_t* g_admin_kind1_notes = NULL;
static int g_admin_kind1_note_count = 0;
static char* g_agent_kind0_json = NULL;
static char* g_agent_kind10002_json = NULL;
static char** g_agent_contacts = NULL;
static int g_agent_contact_count = 0;
static admin_kind1_note_t* g_agent_kind1_notes = NULL;
static int g_agent_kind1_note_count = 0;
static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
static cJSON* g_self_skill_events = NULL;
@@ -269,6 +276,11 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us
static int parse_kind3_wot_contacts(cJSON* tags);
static int parse_kind10002_relays(cJSON* tags);
static void upsert_kind1_note(time_t created_at, const char* content);
static void free_agent_context_locked(void);
static int parse_kind3_agent_contacts(cJSON* tags);
static int parse_kind10002_agent_relays(cJSON* tags);
static void upsert_agent_kind1_note(time_t created_at, const char* content);
static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data);
static int startup_self_kind1_exists(void);
static void load_startup_display_name(void);
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
@@ -456,6 +468,58 @@ static int parse_enabled_flag_local(const char* enabled_s) {
return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
}
static int self_skill_is_adopted_local(int kind, const char* pubkey_hex, const char* d_tag) {
if ((kind != 31123 && kind != 31124) || !pubkey_hex || !d_tag || d_tag[0] == '\0') {
return 0;
}
char addr[256];
snprintf(addr, sizeof(addr), "%d:%s:%s", kind, pubkey_hex, d_tag);
int adopted = 0;
pthread_mutex_lock(&g_self_skill_mutex);
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
int n = cJSON_GetArraySize(g_self_skill_events);
for (int i = 0; i < n && !adopted; i++) {
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != 10123 ||
!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, pubkey_hex) != 0 ||
!ev_tags || !cJSON_IsArray(ev_tags)) {
continue;
}
int tag_count = cJSON_GetArraySize(ev_tags);
for (int ti = 0; ti < tag_count; ti++) {
cJSON* tag = cJSON_GetArrayItem(ev_tags, ti);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
!cJSON_IsString(v) || !v->valuestring) {
continue;
}
if (strcmp(k->valuestring, "a") == 0 && strcmp(v->valuestring, addr) == 0) {
adopted = 1;
break;
}
}
}
}
pthread_mutex_unlock(&g_self_skill_mutex);
return adopted;
}
static void register_trigger_from_self_skill_event(cJSON* event) {
if (!event || !g_cfg || !g_trigger_manager) {
return;
@@ -499,6 +563,11 @@ static void register_trigger_from_self_skill_event(cJSON* event) {
return;
}
if (!self_skill_is_adopted_local(kind_val, pubkey->valuestring, d_tag)) {
DEBUG_INFO("[didactyl] live self-skill trigger ignored (not adopted) d_tag=%s", d_tag);
return;
}
if (action && strcmp(action, "template") == 0) {
DEBUG_WARN("[didactyl] live self-skill trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
}
@@ -1057,6 +1126,31 @@ static void free_admin_context_locked(void) {
g_admin_kind1_note_count = 0;
}
static void free_agent_context_locked(void) {
free(g_agent_kind0_json);
g_agent_kind0_json = NULL;
free(g_agent_kind10002_json);
g_agent_kind10002_json = NULL;
if (g_agent_contacts) {
for (int i = 0; i < g_agent_contact_count; i++) {
free(g_agent_contacts[i]);
}
free(g_agent_contacts);
}
g_agent_contacts = NULL;
g_agent_contact_count = 0;
if (g_agent_kind1_notes) {
for (int i = 0; i < g_agent_kind1_note_count; i++) {
free(g_agent_kind1_notes[i].content);
}
free(g_agent_kind1_notes);
}
g_agent_kind1_notes = NULL;
g_agent_kind1_note_count = 0;
}
static int parse_kind3_wot_contacts(cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
return 0;
@@ -1185,6 +1279,134 @@ static void upsert_kind1_note(time_t created_at, const char* content) {
}
}
static int parse_kind3_agent_contacts(cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
return 0;
}
if (g_agent_contacts) {
for (int i = 0; i < g_agent_contact_count; i++) {
free(g_agent_contacts[i]);
}
free(g_agent_contacts);
g_agent_contacts = NULL;
g_agent_contact_count = 0;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) {
continue;
}
char* dup = strdup(val->valuestring);
if (!dup) {
return -1;
}
char** grown = (char**)realloc(g_agent_contacts, (size_t)(g_agent_contact_count + 1) * sizeof(char*));
if (!grown) {
free(dup);
return -1;
}
g_agent_contacts = grown;
g_agent_contacts[g_agent_contact_count++] = dup;
}
return 0;
}
static int parse_kind10002_agent_relays(cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
free(g_agent_kind10002_json);
g_agent_kind10002_json = strdup("[]");
return g_agent_kind10002_json ? 0 : -1;
}
cJSON* relays = cJSON_CreateArray();
if (!relays) {
return -1;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') {
continue;
}
cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring));
}
char* relays_json = cJSON_PrintUnformatted(relays);
cJSON_Delete(relays);
if (!relays_json) {
return -1;
}
free(g_agent_kind10002_json);
g_agent_kind10002_json = relays_json;
return 0;
}
static void upsert_agent_kind1_note(time_t created_at, const char* content) {
if (!content) {
return;
}
int limit = g_cfg && g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
if (limit > 256) {
limit = 256;
}
char* dup = strdup(content);
if (!dup) {
return;
}
admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_agent_kind1_notes,
(size_t)(g_agent_kind1_note_count + 1) * sizeof(admin_kind1_note_t));
if (!grown) {
free(dup);
return;
}
g_agent_kind1_notes = grown;
g_agent_kind1_notes[g_agent_kind1_note_count].created_at = created_at;
g_agent_kind1_notes[g_agent_kind1_note_count].content = dup;
g_agent_kind1_note_count++;
while (g_agent_kind1_note_count > limit) {
free(g_agent_kind1_notes[0].content);
memmove(&g_agent_kind1_notes[0],
&g_agent_kind1_notes[1],
(size_t)(g_agent_kind1_note_count - 1) * sizeof(admin_kind1_note_t));
g_agent_kind1_note_count--;
}
}
static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data) {
(void)relay_url;
(void)user_data;
@@ -1230,6 +1452,51 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us
pthread_mutex_unlock(&g_admin_ctx_mutex);
}
static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) {
(void)relay_url;
(void)user_data;
if (!event || !g_cfg) {
return;
}
if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
return;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) {
return;
}
if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
return;
}
int k = (int)kind->valuedouble;
pthread_mutex_lock(&g_admin_ctx_mutex);
if (k == 0 && content && cJSON_IsString(content) && content->valuestring) {
free(g_agent_kind0_json);
g_agent_kind0_json = strdup(content->valuestring);
} else if (k == 3 && tags && cJSON_IsArray(tags)) {
(void)parse_kind3_agent_contacts(tags);
} else if (k == 10002 && tags && cJSON_IsArray(tags)) {
(void)parse_kind10002_agent_relays(tags);
} else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) {
time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL);
upsert_agent_kind1_note(ts, content->valuestring);
}
pthread_mutex_unlock(&g_admin_ctx_mutex);
}
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
(void)relay_url;
(void)user_data;
@@ -1412,6 +1679,96 @@ int nostr_handler_subscribe_admin_context(void) {
return rc;
}
int nostr_handler_subscribe_agent_context(void) {
if (!g_cfg || !g_pool) {
return -1;
}
int rc = 0;
cJSON* profile_filter = cJSON_CreateObject();
cJSON* profile_kinds = cJSON_CreateArray();
cJSON* profile_authors = cJSON_CreateArray();
if (!profile_filter || !profile_kinds || !profile_authors) {
cJSON_Delete(profile_filter);
cJSON_Delete(profile_kinds);
cJSON_Delete(profile_authors);
return -1;
}
cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0));
cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3));
cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002));
cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds);
cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
cJSON_AddNumberToObject(profile_filter, "limit", 64);
nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
profile_filter,
on_agent_context_event,
on_eose,
NULL,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(profile_filter);
if (!profile_sub) {
rc = -1;
}
cJSON* notes_filter = cJSON_CreateObject();
cJSON* notes_kinds = cJSON_CreateArray();
cJSON* notes_authors = cJSON_CreateArray();
if (!notes_filter || !notes_kinds || !notes_authors) {
cJSON_Delete(notes_filter);
cJSON_Delete(notes_kinds);
cJSON_Delete(notes_authors);
return -1;
}
int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
if (kind1_limit > 256) {
kind1_limit = 256;
}
cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds);
cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
notes_filter,
on_agent_context_event,
on_eose,
NULL,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(notes_filter);
if (!notes_sub) {
rc = -1;
}
if (rc == 0) {
DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
}
return rc;
}
void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager) {
g_trigger_manager = trigger_manager;
}
@@ -2546,6 +2903,89 @@ char* nostr_handler_get_admin_kind1_notes_context(void) {
return out;
}
char* nostr_handler_get_agent_kind0_context(void) {
if (!g_cfg) {
return NULL;
}
pthread_mutex_lock(&g_admin_ctx_mutex);
char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL;
pthread_mutex_unlock(&g_admin_ctx_mutex);
return out;
}
char* nostr_handler_get_agent_kind3_context(void) {
if (!g_cfg) {
return NULL;
}
pthread_mutex_lock(&g_admin_ctx_mutex);
cJSON* out = cJSON_CreateArray();
if (!out) {
pthread_mutex_unlock(&g_admin_ctx_mutex);
return NULL;
}
for (int i = 0; i < g_agent_contact_count; i++) {
const char* pk = g_agent_contacts[i];
if (!pk || pk[0] == '\0') continue;
cJSON_AddItemToArray(out, cJSON_CreateString(pk));
}
pthread_mutex_unlock(&g_admin_ctx_mutex);
char* out_json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return out_json;
}
char* nostr_handler_get_agent_kind10002_context(void) {
if (!g_cfg) {
return NULL;
}
pthread_mutex_lock(&g_admin_ctx_mutex);
char* out = g_agent_kind10002_json ? strdup(g_agent_kind10002_json) : NULL;
pthread_mutex_unlock(&g_admin_ctx_mutex);
return out;
}
char* nostr_handler_get_agent_kind1_notes_context(void) {
if (!g_cfg) {
return NULL;
}
pthread_mutex_lock(&g_admin_ctx_mutex);
if (g_agent_kind1_note_count <= 0 || !g_agent_kind1_notes) {
pthread_mutex_unlock(&g_admin_ctx_mutex);
return NULL;
}
size_t total = strlen("Agent recent public notes:\n") + 1U;
for (int i = 0; i < g_agent_kind1_note_count; i++) {
total += strlen("- ") + strlen(g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "") + 1U;
}
char* out = (char*)malloc(total);
if (!out) {
pthread_mutex_unlock(&g_admin_ctx_mutex);
return NULL;
}
out[0] = '\0';
strcat(out, "Agent recent public notes:\n");
for (int i = 0; i < g_agent_kind1_note_count; i++) {
strcat(out, "- ");
strcat(out, g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "");
strcat(out, "\n");
}
pthread_mutex_unlock(&g_admin_ctx_mutex);
return out;
}
int nostr_handler_is_wot_contact(const char* pubkey_hex) {
if (!pubkey_hex || strlen(pubkey_hex) != 64U) {
return 0;
@@ -2638,6 +3078,7 @@ void nostr_handler_cleanup(void) {
pthread_mutex_lock(&g_admin_ctx_mutex);
free_admin_context_locked();
free_agent_context_locked();
pthread_mutex_unlock(&g_admin_ctx_mutex);
pthread_mutex_lock(&g_self_skill_mutex);
+5
View File
@@ -35,6 +35,7 @@ typedef void (*nostr_self_skill_eose_cb_t)(int event_count, void* user_data);
int nostr_handler_init(didactyl_config_t* config);
void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager);
int nostr_handler_subscribe_admin_context(void);
int nostr_handler_subscribe_agent_context(void);
int nostr_handler_subscribe_self_skills(void);
char* nostr_handler_get_self_events_by_kind_json(int kind);
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data);
@@ -66,6 +67,10 @@ char* nostr_handler_get_admin_kind0_context(void);
char* nostr_handler_get_admin_kind3_context(void);
char* nostr_handler_get_admin_kind10002_context(void);
char* nostr_handler_get_admin_kind1_notes_context(void);
char* nostr_handler_get_agent_kind0_context(void);
char* nostr_handler_get_agent_kind3_context(void);
char* nostr_handler_get_agent_kind10002_context(void);
char* nostr_handler_get_agent_kind1_notes_context(void);
int nostr_handler_is_wot_contact(const char* pubkey_hex);
char* nostr_handler_relay_status_json(void);
char* nostr_handler_relay_info_json(const char* relay_url);
-6061
View File
File diff suppressed because it is too large Load Diff
+222
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;
}
+257
View File
@@ -0,0 +1,257 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../main.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;
}
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_nostr_agent_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_agent_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, "agent_kind0_json", profile_json);
size_t content_len = strlen("## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent 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,
"## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent 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_agent_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_agent_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, "agent_kind3_json", contacts_json);
size_t content_len = strlen("## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent 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,
"## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent 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_agent_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_agent_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, "agent_kind10002_json", relays_json);
size_t content_len = strlen("## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent 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,
"## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent 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_agent_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_agent_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, "agent_notes_content", notes_text);
size_t content_len = strlen("## Agent 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,
"## Agent 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;
}
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;
}
+515
View File
@@ -0,0 +1,515 @@
#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 default_timeout = ctx->cfg->tools.local_http_fetch_default_timeout_seconds > 0
? ctx->cfg->tools.local_http_fetch_default_timeout_seconds
: 20;
int max_timeout = ctx->cfg->tools.local_http_fetch_max_timeout_seconds > 0
? ctx->cfg->tools.local_http_fetch_max_timeout_seconds
: 120;
if (default_timeout > max_timeout) {
default_timeout = max_timeout;
}
int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : default_timeout;
if (timeout_seconds <= 0) timeout_seconds = default_timeout;
if (timeout_seconds > max_timeout) timeout_seconds = max_timeout;
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
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
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
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;
}
+192
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
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
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;
}
+62
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;
}
+94
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;
}
+302
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;
}
File diff suppressed because it is too large Load Diff
+565
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;
}
View File
+3
View File
@@ -0,0 +1,3 @@
#define _POSIX_C_SOURCE 200809L
#include "tools_internal.h"
+215
View File
@@ -0,0 +1,215 @@
#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);
}
if (strcmp(tool_name, "nostr_agent_profile") == 0) {
return execute_nostr_agent_profile(ctx, args_json);
}
if (strcmp(tool_name, "nostr_agent_contacts") == 0) {
return execute_nostr_agent_contacts(ctx, args_json);
}
if (strcmp(tool_name, "nostr_agent_relays") == 0) {
return execute_nostr_agent_relays(ctx, args_json);
}
if (strcmp(tool_name, "nostr_agent_notes") == 0) {
return execute_nostr_agent_notes(ctx, args_json);
}
if (strcmp(tool_name, "my_kind0_profile") == 0) {
return execute_nostr_agent_profile(ctx, args_json);
}
if (strcmp(tool_name, "my_contacts") == 0) {
return execute_nostr_agent_contacts(ctx, args_json);
}
if (strcmp(tool_name, "my_relays") == 0) {
return execute_nostr_agent_relays(ctx, args_json);
}
if (strcmp(tool_name, "my_notes") == 0) {
return execute_nostr_agent_notes(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);
}
+60
View File
@@ -0,0 +1,60 @@
#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_nostr_agent_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_notes(tools_context_t* ctx, 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
File diff suppressed because it is too large Load Diff