267 lines
9.2 KiB
C
267 lines
9.2 KiB
C
#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");
|
|
}
|