v0.0.1 - Added static MUSL build pipeline, Dockerfile.alpine-musl, versioned main.h integration, and release tooling updates

This commit is contained in:
Your Name
2026-02-27 12:02:07 -04:00
parent 36a2e63c6e
commit 7472809aa2
21 changed files with 1877 additions and 110 deletions
+83 -10
View File
@@ -8,9 +8,11 @@
#include "llm.h"
#include "nostr_handler.h"
#include "tools.h"
static didactyl_config_t* g_cfg = NULL;
static char* g_system_context = NULL;
static tools_context_t g_tools_ctx;
int agent_init(didactyl_config_t* config, const char* system_context) {
if (!config || !system_context) {
@@ -23,6 +25,13 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
return -1;
}
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
return -1;
}
return 0;
}
@@ -36,22 +45,86 @@ void agent_on_message(const char* sender_pubkey_hex, const char* message, void*
fprintf(stdout, "[didactyl] incoming message from %.16s...\n", sender_pubkey_hex);
fprintf(stdout, "[didactyl] calling llm for sender %.16s...\n", sender_pubkey_hex);
char* response = llm_chat(g_system_context, message);
if (!response) {
const char* fallback = "I could not get a response from the LLM right now.";
fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
(void)nostr_handler_send_dm(sender_pubkey_hex, fallback);
if (!g_cfg->tools.enabled) {
char* response = llm_chat(g_system_context, message);
if (!response) {
const char* fallback = "I could not get a response from the LLM right now.";
fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
(void)nostr_handler_send_dm(sender_pubkey_hex, fallback);
return;
}
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
response,
strlen(response) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
free(response);
return;
}
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
response,
strlen(response) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
free(response);
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
if (!tools_json) {
(void)nostr_handler_send_dm(sender_pubkey_hex, "Tool schema generation failed.");
return;
}
llm_response_t resp;
if (llm_chat_with_tools(g_system_context, message, tools_json, &resp) != 0) {
free(tools_json);
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM request failed.");
return;
}
free(tools_json);
if (resp.tool_call_count <= 0) {
const char* answer = resp.content ? resp.content : "No response content.";
fprintf(stdout, "[didactyl] llm response (no tool call): %.240s%s\n",
answer,
strlen(answer) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, answer);
llm_response_free(&resp);
return;
}
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
llm_response_t current = resp;
for (int turn = 0; turn < max_turns; turn++) {
if (current.tool_call_count <= 0) {
break;
}
llm_tool_call_t* tc = &current.tool_calls[0];
fprintf(stdout, "[didactyl] executing tool call: %s\n", tc->name ? tc->name : "<null>");
char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
if (!tool_result) {
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
}
llm_response_free(&current);
if (llm_chat_with_tools(g_system_context, tool_result, NULL, &current) != 0) {
free(tool_result);
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM failed after tool execution.");
return;
}
free(tool_result);
if (current.content && current.tool_call_count == 0) {
break;
}
}
const char* final_answer = current.content ? current.content : "I hit my tool-use limit for this request.";
fprintf(stdout, "[didactyl] final response: %.240s%s\n",
final_answer,
strlen(final_answer) > 240 ? "..." : "");
(void)nostr_handler_send_dm(sender_pubkey_hex, final_answer);
llm_response_free(&current);
}
void agent_cleanup(void) {
tools_cleanup(&g_tools_ctx);
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
+54
View File
@@ -117,6 +117,50 @@ static int decode_pubkey_hex_or_npub(const char* in, char out_hex[65]) {
return -1;
}
static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
cJSON* tools = cJSON_GetObjectItemCaseSensitive(root, "tools");
if (!tools || !cJSON_IsObject(tools)) {
return 0;
}
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled");
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns");
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;
}
cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
if (!shell || !cJSON_IsObject(shell)) {
return 0;
}
cJSON* shell_enabled = cJSON_GetObjectItemCaseSensitive(shell, "enabled");
cJSON* timeout_seconds = cJSON_GetObjectItemCaseSensitive(shell, "timeout_seconds");
cJSON* max_output_bytes = cJSON_GetObjectItemCaseSensitive(shell, "max_output_bytes");
if (shell_enabled && cJSON_IsBool(shell_enabled)) {
config->tools.shell.enabled = cJSON_IsTrue(shell_enabled) ? 1 : 0;
}
if (timeout_seconds && cJSON_IsNumber(timeout_seconds)) {
config->tools.shell.timeout_seconds = (int)timeout_seconds->valuedouble;
}
if (max_output_bytes && cJSON_IsNumber(max_output_bytes)) {
config->tools.shell.max_output_bytes = (int)max_output_bytes->valuedouble;
}
if (copy_json_string(shell,
"working_directory",
config->tools.shell.working_directory,
sizeof(config->tools.shell.working_directory),
0) != 0) {
return -1;
}
return 0;
}
static int parse_relays(cJSON* root, didactyl_config_t* config) {
cJSON* relays = cJSON_GetObjectItemCaseSensitive(root, "relays");
if (!relays || !cJSON_IsArray(relays)) {
@@ -171,6 +215,12 @@ int config_load(const char* path, didactyl_config_t* config) {
}
memset(config, 0, sizeof(*config));
config->tools.enabled = 1;
config->tools.max_turns = 8;
config->tools.shell.enabled = 1;
config->tools.shell.timeout_seconds = 30;
config->tools.shell.max_output_bytes = 65536;
strcpy(config->tools.shell.working_directory, ".");
char* json_buf = NULL;
size_t json_len = 0;
@@ -254,6 +304,10 @@ int config_load(const char* path, didactyl_config_t* config) {
config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512;
config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;
if (parse_tools_config(root, config) != 0) {
goto cleanup;
}
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
goto cleanup;
}
+14
View File
@@ -37,6 +37,19 @@ typedef struct {
double temperature;
} llm_config_t;
typedef struct {
int enabled;
int timeout_seconds;
int max_output_bytes;
char working_directory[OW_MAX_URL_LEN];
} shell_tools_config_t;
typedef struct {
int enabled;
int max_turns;
shell_tools_config_t shell;
} tools_config_t;
typedef struct {
agent_profile_t profile;
agent_keys_t keys;
@@ -44,6 +57,7 @@ typedef struct {
char** relays;
int relay_count;
llm_config_t llm;
tools_config_t tools;
} didactyl_config_t;
int config_load(const char* path, didactyl_config_t* config);
+45
View File
@@ -0,0 +1,45 @@
#include "debug.h"
#include <stdarg.h>
#include <string.h>
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
void debug_init(int level) {
if (level < 0) level = 0;
if (level > 5) level = 5;
g_debug_level = (debug_level_t)level;
}
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
const char* level_str = "UNKNOWN";
switch (level) {
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
default: break;
}
printf("[%s] [%s] ", timestamp, level_str);
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
const char* filename = strrchr(file, '/');
filename = filename ? filename + 1 : file;
printf("[%s:%d] ", filename, line);
}
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
fflush(stdout);
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef DIDACTYL_DEBUG_H
#define DIDACTYL_DEBUG_H
#include <stdio.h>
#include <time.h>
typedef enum {
DEBUG_LEVEL_NONE = 0,
DEBUG_LEVEL_ERROR = 1,
DEBUG_LEVEL_WARN = 2,
DEBUG_LEVEL_INFO = 3,
DEBUG_LEVEL_DEBUG = 4,
DEBUG_LEVEL_TRACE = 5
} debug_level_t;
extern debug_level_t g_debug_level;
void debug_init(int level);
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
#define DEBUG_ERROR(...) \
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_WARN(...) \
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_INFO(...) \
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_LOG(...) \
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_TRACE(...) \
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#endif
+189 -53
View File
@@ -41,6 +41,47 @@ static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
return total;
}
static char* perform_chat_request(const char* body) {
CURL* curl = curl_easy_init();
if (!curl || !body) {
if (curl) curl_easy_cleanup(curl);
return NULL;
}
char url[OW_MAX_URL_LEN + 64];
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
response_buffer_t rb = {0};
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
char auth_header[OW_MAX_KEY_LEN + 32];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK || status < 200 || status >= 300 || !rb.data) {
free(rb.data);
return NULL;
}
return rb.data;
}
static char* build_request_json(const char* system_prompt, const char* user_message) {
cJSON* root = cJSON_CreateObject();
cJSON* messages = cJSON_CreateArray();
@@ -76,29 +117,90 @@ static char* build_request_json(const char* system_prompt, const char* user_mess
return body;
}
static char* parse_response_content(const char* json) {
cJSON* root = cJSON_Parse(json);
if (!root) {
return NULL;
static int parse_tool_calls(cJSON* msg, llm_response_t* out) {
cJSON* tc = cJSON_GetObjectItemCaseSensitive(msg, "tool_calls");
if (!tc || !cJSON_IsArray(tc)) {
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
int n = cJSON_GetArraySize(tc);
if (n <= 0) {
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
llm_tool_call_t* calls = (llm_tool_call_t*)calloc((size_t)n, sizeof(llm_tool_call_t));
if (!calls) return -1;
int actual = 0;
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(tc, i);
cJSON* id = item ? cJSON_GetObjectItemCaseSensitive(item, "id") : NULL;
cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
cJSON* args = fn ? cJSON_GetObjectItemCaseSensitive(fn, "arguments") : NULL;
if (!id || !cJSON_IsString(id) || !id->valuestring ||
!name || !cJSON_IsString(name) || !name->valuestring) {
continue;
}
calls[actual].id = strdup(id->valuestring);
calls[actual].name = strdup(name->valuestring);
calls[actual].arguments_json = strdup((args && cJSON_IsString(args) && args->valuestring) ? args->valuestring : "{}");
if (!calls[actual].id || !calls[actual].name || !calls[actual].arguments_json) {
free(calls[actual].id);
free(calls[actual].name);
free(calls[actual].arguments_json);
continue;
}
actual++;
}
if (actual == 0) {
free(calls);
out->tool_calls = NULL;
out->tool_call_count = 0;
return 0;
}
out->tool_calls = calls;
out->tool_call_count = actual;
return 0;
}
static int parse_llm_response(const char* json, llm_response_t* out) {
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json);
if (!root) return -1;
cJSON* choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
if (!choices || !cJSON_IsArray(choices) || cJSON_GetArraySize(choices) == 0) {
cJSON* first = (choices && cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0)
? cJSON_GetArrayItem(choices, 0)
: NULL;
cJSON* msg = first ? cJSON_GetObjectItemCaseSensitive(first, "message") : NULL;
if (!msg || !cJSON_IsObject(msg)) {
cJSON_Delete(root);
return NULL;
return -1;
}
cJSON* first = cJSON_GetArrayItem(choices, 0);
cJSON* msg = cJSON_GetObjectItemCaseSensitive(first, "message");
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
if (!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(root);
return NULL;
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
if (content && cJSON_IsString(content) && content->valuestring) {
out->content = strdup(content->valuestring);
}
if (parse_tool_calls(msg, out) != 0) {
cJSON_Delete(root);
llm_response_free(out);
return -1;
}
char* out = strdup(content->valuestring);
cJSON_Delete(root);
return out;
return 0;
}
int llm_init(const llm_config_t* config) {
@@ -118,54 +220,88 @@ char* llm_chat(const char* system_prompt, const char* user_message) {
}
char* body = build_request_json(system_prompt, user_message);
if (!body) {
return NULL;
}
if (!body) return NULL;
CURL* curl = curl_easy_init();
if (!curl) {
free(body);
return NULL;
}
char url[OW_MAX_URL_LEN + 64];
snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
response_buffer_t rb = {0};
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
char auth_header[OW_MAX_KEY_LEN + 32];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
char* raw = perform_chat_request(body);
free(body);
if (!raw) return NULL;
if (res != CURLE_OK || status < 200 || status >= 300 || !rb.data) {
free(rb.data);
llm_response_t parsed;
if (parse_llm_response(raw, &parsed) != 0) {
free(raw);
return NULL;
}
free(raw);
char* answer = parse_response_content(rb.data);
free(rb.data);
char* answer = parsed.content ? strdup(parsed.content) : NULL;
llm_response_free(&parsed);
return answer;
}
int llm_chat_with_tools(const char* system_prompt,
const char* user_message,
const char* tools_json,
llm_response_t* out_response) {
if (!g_initialized || !out_response) {
return -1;
}
cJSON* root = cJSON_CreateObject();
cJSON* messages = cJSON_CreateArray();
if (!root || !messages) {
cJSON_Delete(root);
cJSON_Delete(messages);
return -1;
}
cJSON_AddStringToObject(root, "model", g_cfg.model);
cJSON_AddNumberToObject(root, "max_tokens", g_cfg.max_tokens);
cJSON_AddNumberToObject(root, "temperature", g_cfg.temperature);
cJSON* system_msg = cJSON_CreateObject();
cJSON* user_msg = cJSON_CreateObject();
cJSON_AddStringToObject(system_msg, "role", "system");
cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");
cJSON_AddStringToObject(user_msg, "role", "user");
cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");
cJSON_AddItemToArray(messages, system_msg);
cJSON_AddItemToArray(messages, user_msg);
cJSON_AddItemToObject(root, "messages", messages);
if (tools_json) {
cJSON* tools = cJSON_Parse(tools_json);
if (tools && cJSON_IsArray(tools)) {
cJSON_AddItemToObject(root, "tools", tools);
} else {
cJSON_Delete(tools);
}
}
char* body = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!body) return -1;
char* raw = perform_chat_request(body);
free(body);
if (!raw) return -1;
int rc = parse_llm_response(raw, out_response);
free(raw);
return rc;
}
void llm_response_free(llm_response_t* response) {
if (!response) return;
free(response->content);
for (int i = 0; i < response->tool_call_count; i++) {
free(response->tool_calls[i].id);
free(response->tool_calls[i].name);
free(response->tool_calls[i].arguments_json);
}
free(response->tool_calls);
memset(response, 0, sizeof(*response));
}
void llm_cleanup(void) {
if (!g_initialized) {
return;
+17
View File
@@ -3,8 +3,25 @@
#include "config.h"
typedef struct {
char* id;
char* name;
char* arguments_json;
} llm_tool_call_t;
typedef struct {
char* content;
llm_tool_call_t* tool_calls;
int tool_call_count;
} llm_response_t;
int llm_init(const llm_config_t* config);
char* llm_chat(const char* system_prompt, const char* user_message);
int llm_chat_with_tools(const char* system_prompt,
const char* user_message,
const char* tools_json,
llm_response_t* out_response);
void llm_response_free(llm_response_t* response);
void llm_cleanup(void);
#endif
+21 -4
View File
@@ -6,11 +6,13 @@
#include <string.h>
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "main.h"
#include "agent.h"
#include "config.h"
#include "context.h"
#include "llm.h"
#include "nostr_handler.h"
#include "debug.h"
static volatile sig_atomic_t g_running = 1;
@@ -23,17 +25,25 @@ int main(int argc, char** argv) {
const char* config_path = "./config.json";
const char* context_path = "./SYSTEM.md";
int debug_level = DEBUG_LEVEL_INFO;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
config_path = argv[++i];
} else if (strcmp(argv[i], "--context") == 0 && i + 1 < argc) {
context_path = argv[++i];
} else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
debug_level = atoi(argv[++i]);
} else {
fprintf(stderr, "Usage: %s [--config <path>] [--context <path>]\n", argv[0]);
fprintf(stderr, "Usage: %s [--config <path>] [--context <path>] [--debug <0-5>]\n", argv[0]);
return 1;
}
}
debug_init(debug_level);
DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION);
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize nostr core\n");
return 1;
@@ -81,11 +91,15 @@ int main(int argc, char** argv) {
return 1;
}
DEBUG_INFO("[didactyl] startup phase: publish profile begin");
if (nostr_handler_publish_profile() != 0) {
fprintf(stderr, "Warning: failed to publish profile\n");
DEBUG_WARN("[didactyl] publish profile deferred/failed (will continue startup)");
}
DEBUG_INFO("[didactyl] startup phase: publish profile end");
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
fprintf(stderr, "Failed to subscribe to DMs\n");
agent_cleanup();
nostr_handler_cleanup();
@@ -96,10 +110,13 @@ int main(int argc, char** argv) {
return 1;
}
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
fprintf(stdout, "[didactyl] running with pubkey %s\n", cfg.keys.public_key_hex);
DEBUG_INFO("[didactyl] entering main poll loop");
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
while (g_running) {
(void)nostr_handler_poll(100);
@@ -107,7 +124,7 @@ int main(int argc, char** argv) {
nanosleep(&ts, NULL);
}
fprintf(stdout, "[didactyl] shutting down\n");
DEBUG_INFO("[didactyl] shutting down");
agent_cleanup();
nostr_handler_cleanup();
+23
View File
@@ -0,0 +1,23 @@
/*
* Didactyl Main Header - Version and Metadata Information
*
* This header contains version information and agent metadata.
* Version macros are auto-updated by the build system.
*/
#ifndef DIDACTYL_MAIN_H
#define DIDACTYL_MAIN_H
// Version information (auto-updated by build system)
// 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 1
#define DIDACTYL_VERSION "v0.0.1"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
#define DIDACTYL_DESCRIPTION "A sovereign AI agent daemon on Nostr"
#define DIDACTYL_SOFTWARE "https://git.laantungir.net/laantungir/didactyl.git"
#endif /* DIDACTYL_MAIN_H */
+210 -25
View File
@@ -9,6 +9,7 @@
#include "../../nostr_core_lib/cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "debug.h"
static didactyl_config_t* g_cfg = NULL;
static nostr_relay_pool_t* g_pool = NULL;
@@ -16,6 +17,8 @@ static dm_callback_t g_dm_callback = NULL;
static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static time_t g_last_status_log_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static const char* relay_status_str(nostr_pool_relay_status_t status) {
switch (status) {
@@ -37,21 +40,55 @@ static void log_relay_statuses(const char* reason) {
return;
}
fprintf(stdout, "[didactyl] relay status snapshot (%s)\n", reason ? reason : "periodic");
DEBUG_INFO("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = g_cfg->relays[i];
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(g_pool, relay);
const char* last_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
double ping_ms = nostr_relay_pool_get_relay_ping_latency(g_pool, relay);
fprintf(stdout, "[didactyl] - %s => %s", relay, relay_status_str(status));
if (ping_ms > 0.0) {
fprintf(stdout, " (ping %.1f ms)", ping_ms);
DEBUG_INFO("[didactyl] - %s => %s (ping %.1f ms)",
relay,
relay_status_str(status),
ping_ms);
} else {
DEBUG_INFO("[didactyl] - %s => %s", relay, relay_status_str(status));
}
if (last_err && last_err[0] != '\0') {
fprintf(stdout, " [last_error: %s]", last_err);
DEBUG_WARN("[didactyl] - %s last_connection_error: %s", relay, last_err);
}
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, relay);
if (stats) {
DEBUG_LOG("[didactyl] - %s stats attempts=%d failures=%d recv=%d pub_ok=%d pub_fail=%d",
relay,
stats->connection_attempts,
stats->connection_failures,
stats->events_received,
stats->events_published_ok,
stats->events_published_failed);
}
}
}
static void log_relay_state_changes(void) {
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
return;
}
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = g_cfg->relays[i];
nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay);
nostr_pool_relay_status_t prev = g_last_relay_statuses[i];
if (now != prev) {
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
relay,
relay_status_str(prev),
relay_status_str(now));
g_last_relay_statuses[i] = now;
}
fprintf(stdout, "\n");
}
}
@@ -60,9 +97,9 @@ static void log_publish_targets(const char* action) {
return;
}
fprintf(stdout, "[didactyl] %s target relays (%d):\n", action ? action : "publish", g_cfg->relay_count);
DEBUG_INFO("[didactyl] %s target relays (%d):", action ? action : "publish", g_cfg->relay_count);
for (int i = 0; i < g_cfg->relay_count; i++) {
fprintf(stdout, "[didactyl] -> %s\n", g_cfg->relays[i]);
DEBUG_INFO("[didactyl] -> %s", g_cfg->relays[i]);
}
}
@@ -173,9 +210,9 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
return;
}
fprintf(stdout, "[didactyl] received DM from %.16s... via %s\n",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
DEBUG_INFO("[didactyl] received DM from %.16s... via %s",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
g_dm_callback(pubkey->valuestring, decrypted, g_dm_user_data);
free(decrypted);
}
@@ -195,7 +232,7 @@ int nostr_handler_init(didactyl_config_t* config) {
g_poll_counter = 0;
g_start_time = time(NULL);
fprintf(stdout, "[didactyl] initializing relay pool with %d relays\n", g_cfg->relay_count);
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
reconnect.enable_auto_reconnect = 1;
@@ -212,9 +249,19 @@ int nostr_handler_init(didactyl_config_t* config) {
fprintf(stderr, "[didactyl] failed to add relay: %s\n", g_cfg->relays[i]);
return -1;
}
fprintf(stdout, "[didactyl] added relay: %s\n", g_cfg->relays[i]);
DEBUG_INFO("[didactyl] added relay: %s", g_cfg->relays[i]);
}
free(g_last_relay_statuses);
g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count, sizeof(nostr_pool_relay_status_t));
if (!g_last_relay_statuses) {
return -1;
}
for (int i = 0; i < g_cfg->relay_count; i++) {
g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
}
g_last_status_log_time = time(NULL);
log_relay_statuses("after init");
return 0;
}
@@ -224,6 +271,7 @@ int nostr_handler_publish_profile(void) {
return -1;
}
DEBUG_INFO("[didactyl] publish_profile: build metadata payload");
cJSON* profile = cJSON_CreateObject();
if (!profile) {
return -1;
@@ -243,6 +291,7 @@ int nostr_handler_publish_profile(void) {
return -1;
}
DEBUG_INFO("[didactyl] publish_profile: sign kind-0 event");
cJSON* event = nostr_create_and_sign_event(0, content, NULL, g_cfg->keys.private_key, time(NULL));
free(content);
@@ -250,18 +299,38 @@ int nostr_handler_publish_profile(void) {
return -1;
}
log_publish_targets("publish profile");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
if (connected_count == 0) {
DEBUG_WARN("[didactyl] publish_profile: no connected relays yet, deferring publish");
free(connected_relays);
cJSON_Delete(event);
return 0;
}
DEBUG_INFO("[didactyl] publish_profile: publishing to %d connected relay(s)", connected_count);
int sent = nostr_relay_pool_publish_async(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
connected_relays,
connected_count,
event,
NULL,
NULL);
free(connected_relays);
cJSON_Delete(event);
fprintf(stdout, "[didactyl] publish profile result: sent_to=%d relays\n", sent);
DEBUG_INFO("[didactyl] publish profile result: sent_to=%d connected relay(s)", sent);
return sent > 0 ? 0 : -1;
}
@@ -310,7 +379,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
return -1;
}
fprintf(stdout, "[didactyl] DM subscription active for pubkey %.16s...\n", g_cfg->keys.public_key_hex);
DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
return 0;
}
@@ -351,17 +420,125 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
log_publish_targets("publish DM");
int sent = nostr_relay_pool_publish_async(
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
int sent = 0;
if (connected_count > 0) {
sent = nostr_relay_pool_publish_async(
g_pool,
connected_relays,
connected_count,
event,
NULL,
NULL);
}
free(connected_relays);
cJSON_Delete(event);
DEBUG_INFO("[didactyl] sent DM to %.16s... via %d connected relay(s)", recipient_pubkey_hex, sent);
return sent > 0 ? 0 : -1;
}
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags) {
if (!g_cfg || !g_pool || !content) {
return -1;
}
cJSON* tags_copy = NULL;
if (tags) {
tags_copy = cJSON_Duplicate(tags, 1);
if (!tags_copy) {
return -1;
}
}
cJSON* event = nostr_create_and_sign_event(kind, content, tags_copy, g_cfg->keys.private_key, time(NULL));
if (tags_copy) {
cJSON_Delete(tags_copy);
}
if (!event) {
return -1;
}
log_publish_targets("publish kind event");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
int sent = 0;
if (connected_count > 0) {
sent = nostr_relay_pool_publish_async(
g_pool,
connected_relays,
connected_count,
event,
NULL,
NULL);
}
free(connected_relays);
cJSON_Delete(event);
DEBUG_INFO("[didactyl] published kind %d event via %d connected relay(s)", kind, sent);
return sent > 0 ? 0 : -1;
}
char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
if (!g_cfg || !g_pool || !filter) {
return NULL;
}
int event_count = 0;
cJSON** events = nostr_relay_pool_query_sync(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
event,
NULL,
NULL);
filter,
&event_count,
timeout_ms);
cJSON_Delete(event);
fprintf(stdout, "[didactyl] sent DM to %.16s... via %d relay(s)\n", recipient_pubkey_hex, sent);
return sent > 0 ? 0 : -1;
cJSON* arr = cJSON_CreateArray();
if (!arr) {
return NULL;
}
if (events && event_count > 0) {
for (int i = 0; i < event_count; i++) {
if (!events[i]) {
continue;
}
cJSON* dup = cJSON_Duplicate(events[i], 1);
if (dup) {
cJSON_AddItemToArray(arr, dup);
}
cJSON_Delete(events[i]);
}
free(events);
}
char* out = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
return out;
}
int nostr_handler_poll(int timeout_ms) {
@@ -372,8 +549,12 @@ int nostr_handler_poll(int timeout_ms) {
int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
g_poll_counter++;
if ((g_poll_counter % 600) == 0) {
log_relay_state_changes();
time_t now = time(NULL);
if (g_last_status_log_time == 0 || difftime(now, g_last_status_log_time) >= 10.0) {
log_relay_statuses("periodic");
g_last_status_log_time = now;
}
return rc;
@@ -384,6 +565,10 @@ void nostr_handler_cleanup(void) {
nostr_relay_pool_destroy(g_pool);
}
free(g_last_relay_statuses);
g_last_relay_statuses = NULL;
g_last_status_log_time = 0;
g_pool = NULL;
g_cfg = NULL;
g_dm_callback = NULL;
+3
View File
@@ -2,6 +2,7 @@
#define OPEN_WING_NOSTR_HANDLER_H
#include "config.h"
#include "cjson/cJSON.h"
typedef void (*dm_callback_t)(const char* sender_pubkey_hex, const char* message, void* user_data);
@@ -9,6 +10,8 @@ int nostr_handler_init(didactyl_config_t* config);
int nostr_handler_publish_profile(void);
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags);
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
int nostr_handler_poll(int timeout_ms);
void nostr_handler_cleanup(void);
-13
View File
@@ -1,13 +0,0 @@
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_schnorrsig.h>
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char* sig64,
const unsigned char* msg32,
const secp256k1_keypair* keypair,
const unsigned char* aux_rand32
) {
return secp256k1_schnorrsig_sign(ctx, sig64, msg32, keypair, aux_rand32);
}
+266
View File
@@ -0,0 +1,266 @@
#define _POSIX_C_SOURCE 200809L
#include "tools.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "nostr_handler.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;
}
static char* json_success_with_message(const char* msg) {
cJSON* root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "message", msg ? msg : "ok");
char* out = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return out;
}
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) {
if (!ctx || !cfg) return -1;
memset(ctx, 0, sizeof(*ctx));
ctx->cfg = cfg;
return 0;
}
void tools_cleanup(tools_context_t* ctx) {
if (!ctx) return;
memset(ctx, 0, sizeof(*ctx));
}
char* tools_build_openai_schema_json(const tools_context_t* ctx) {
(void)ctx;
cJSON* tools = cJSON_CreateArray();
if (!tools) return NULL;
cJSON* t1 = cJSON_CreateObject();
cJSON* t1_fn = cJSON_CreateObject();
cJSON* t1_params = cJSON_CreateObject();
cJSON* t1_props = cJSON_CreateObject();
cJSON* t1_required = cJSON_CreateArray();
cJSON_AddStringToObject(t1, "type", "function");
cJSON_AddStringToObject(t1_fn, "name", "nostr_post");
cJSON_AddStringToObject(t1_fn, "description", "Publish a Nostr event to connected relays");
cJSON_AddStringToObject(t1_params, "type", "object");
cJSON_AddItemToObject(t1_params, "properties", t1_props);
cJSON_AddItemToObject(t1_params, "required", t1_required);
cJSON* p_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_kind, "type", "integer");
cJSON_AddItemToObject(t1_props, "kind", p_kind);
cJSON* p_content = cJSON_CreateObject();
cJSON_AddStringToObject(p_content, "type", "string");
cJSON_AddItemToObject(t1_props, "content", p_content);
cJSON_AddItemToArray(t1_required, cJSON_CreateString("kind"));
cJSON_AddItemToArray(t1_required, cJSON_CreateString("content"));
cJSON_AddItemToObject(t1_fn, "parameters", t1_params);
cJSON_AddItemToObject(t1, "function", t1_fn);
cJSON_AddItemToArray(tools, t1);
cJSON* t2 = cJSON_CreateObject();
cJSON* t2_fn = cJSON_CreateObject();
cJSON* t2_params = cJSON_CreateObject();
cJSON* t2_props = cJSON_CreateObject();
cJSON* t2_required = cJSON_CreateArray();
cJSON_AddStringToObject(t2, "type", "function");
cJSON_AddStringToObject(t2_fn, "name", "nostr_query");
cJSON_AddStringToObject(t2_fn, "description", "Query events from relays using a Nostr filter");
cJSON_AddStringToObject(t2_params, "type", "object");
cJSON_AddItemToObject(t2_params, "properties", t2_props);
cJSON_AddItemToObject(t2_params, "required", t2_required);
cJSON* p_filter = cJSON_CreateObject();
cJSON_AddStringToObject(p_filter, "type", "object");
cJSON_AddItemToObject(t2_props, "filter", p_filter);
cJSON* p_timeout = cJSON_CreateObject();
cJSON_AddStringToObject(p_timeout, "type", "integer");
cJSON_AddItemToObject(t2_props, "timeout_ms", p_timeout);
cJSON_AddItemToArray(t2_required, cJSON_CreateString("filter"));
cJSON_AddItemToObject(t2_fn, "parameters", t2_params);
cJSON_AddItemToObject(t2, "function", t2_fn);
cJSON_AddItemToArray(tools, t2);
cJSON* t3 = cJSON_CreateObject();
cJSON* t3_fn = cJSON_CreateObject();
cJSON* t3_params = cJSON_CreateObject();
cJSON* t3_props = cJSON_CreateObject();
cJSON* t3_required = cJSON_CreateArray();
cJSON_AddStringToObject(t3, "type", "function");
cJSON_AddStringToObject(t3_fn, "name", "shell_exec");
cJSON_AddStringToObject(t3_fn, "description", "Execute a shell command and return stdout/stderr");
cJSON_AddStringToObject(t3_params, "type", "object");
cJSON_AddItemToObject(t3_params, "properties", t3_props);
cJSON_AddItemToObject(t3_params, "required", t3_required);
cJSON* p_cmd = cJSON_CreateObject();
cJSON_AddStringToObject(p_cmd, "type", "string");
cJSON_AddItemToObject(t3_props, "command", p_cmd);
cJSON_AddItemToArray(t3_required, cJSON_CreateString("command"));
cJSON_AddItemToObject(t3_fn, "parameters", t3_params);
cJSON_AddItemToObject(t3, "function", t3_fn);
cJSON_AddItemToArray(tools, t3);
char* out = cJSON_PrintUnformatted(tools);
cJSON_Delete(tools);
return out;
}
static char* execute_nostr_post(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(args);
return json_error("nostr_post requires integer kind and string content");
}
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble, content->valuestring, NULL);
cJSON_Delete(args);
if (rc != 0) return json_error("nostr_post failed");
return json_success_with_message("nostr_post published");
}
static char* execute_nostr_query(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_ms");
if (!filter || !cJSON_IsObject(filter)) {
cJSON_Delete(args);
return json_error("nostr_query requires object filter");
}
cJSON* filter_dup = cJSON_Duplicate(filter, 1);
if (!filter_dup) {
cJSON_Delete(args);
return json_error("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("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("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;
}
static char* execute_shell_exec(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
if (!ctx->cfg->tools.shell.enabled) return json_error("shell tool disabled");
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command");
if (!command || !cJSON_IsString(command) || !command->valuestring) {
cJSON_Delete(args);
return json_error("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 cmd[4096];
snprintf(cmd,
sizeof(cmd),
"cd %s && timeout %ds sh -lc %s 2>&1",
cwd,
timeout_s,
command->valuestring);
FILE* fp = popen(cmd, "r");
cJSON_Delete(args);
if (!fp) return json_error("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("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 status = pclose(fp);
cJSON* out = cJSON_CreateObject();
if (!out) {
free(output);
return NULL;
}
cJSON_AddBoolToObject(out, "success", status == 0 ? 1 : 0);
cJSON_AddNumberToObject(out, "exit_status", status);
cJSON_AddStringToObject(out, "output", output);
free(output);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
char* tools_execute(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_query") == 0) {
return execute_nostr_query(args_json);
}
if (strcmp(tool_name, "shell_exec") == 0) {
return execute_shell_exec(ctx, args_json);
}
return json_error("unknown tool");
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef DIDACTYL_TOOLS_H
#define DIDACTYL_TOOLS_H
#include "config.h"
typedef struct {
didactyl_config_t* cfg;
} tools_context_t;
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg);
void tools_cleanup(tools_context_t* ctx);
char* tools_build_openai_schema_json(const tools_context_t* ctx);
char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json);
#endif