#define _POSIX_C_SOURCE 200809L #include "tools_internal.h" #include #include #include #include #include "cjson/cJSON.h" #include "../nostr_handler.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" #define MEMORY_KIND 30078 #define MEMORY_D_TAG "memory" #define MEMORY_APP_TAG "didactyl" #define MEMORY_MAX_CHARS 32000 static pthread_mutex_t g_memory_mutex = PTHREAD_MUTEX_INITIALIZER; static char* g_memory_plain = NULL; static int g_memory_loaded = 0; 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 char* strdup_nonnull_local(const char* s) { return strdup(s ? s : ""); } static void memory_cache_set_locked_local(const char* plaintext) { free(g_memory_plain); g_memory_plain = strdup_nonnull_local(plaintext); g_memory_loaded = 1; } static char* memory_cache_get_copy_locked_local(void) { return strdup(g_memory_plain ? g_memory_plain : ""); } static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext, char** out_ciphertext) { if (!ctx || !ctx->cfg || !out_ciphertext) return -1; const char* plain = plaintext ? plaintext : ""; size_t out_cap = (strlen(plain) * 4U) + 1024U; char* ciphertext = (char*)malloc(out_cap); if (!ciphertext) return -1; int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key, ctx->cfg->keys.public_key, plain, ciphertext, out_cap); if (rc != NOSTR_SUCCESS) { free(ciphertext); return -1; } *out_ciphertext = ciphertext; return 0; } static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext, char** out_plaintext) { if (!ctx || !ctx->cfg || !ciphertext || !out_plaintext) return -1; size_t out_cap = strlen(ciphertext) + 1024U; char* plaintext = (char*)malloc(out_cap); if (!plaintext) return -1; int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key, ctx->cfg->keys.public_key, ciphertext, plaintext, out_cap); if (rc != NOSTR_SUCCESS) { free(plaintext); return -1; } *out_plaintext = plaintext; return 0; } static int query_memory_ciphertext_local(tools_context_t* ctx, char** out_ciphertext) { if (!ctx || !ctx->cfg || !out_ciphertext) return -1; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); cJSON* d_vals = cJSON_CreateArray(); if (!filter || !kinds || !authors || !d_vals) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); cJSON_Delete(d_vals); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(MEMORY_KIND)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddItemToArray(d_vals, cJSON_CreateString(MEMORY_D_TAG)); cJSON_AddItemToObject(filter, "#d", d_vals); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 4000); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); *out_ciphertext = strdup(""); return (*out_ciphertext != NULL) ? 0 : -1; } cJSON* ev = cJSON_GetArrayItem(arr, 0); cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; const char* cipher = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; *out_ciphertext = strdup(cipher); cJSON_Delete(arr); return (*out_ciphertext != NULL) ? 0 : -1; } static int memory_load_local(tools_context_t* ctx) { char* ciphertext = NULL; if (query_memory_ciphertext_local(ctx, &ciphertext) != 0) { return -1; } char* plaintext = NULL; if (!ciphertext || ciphertext[0] == '\0') { plaintext = strdup(""); } else if (nip44_decrypt_self_local(ctx, ciphertext, &plaintext) != 0) { fprintf(stdout, "[didactyl] warning: failed to NIP-44 decrypt memory; resetting to empty\n"); plaintext = strdup(""); } free(ciphertext); if (!plaintext) return -1; pthread_mutex_lock(&g_memory_mutex); memory_cache_set_locked_local(plaintext); pthread_mutex_unlock(&g_memory_mutex); free(plaintext); return 0; } static char* trim_and_limit_entry_local(const char* in, int* out_truncated) { if (out_truncated) *out_truncated = 0; if (!in) return strdup(""); while (*in == ' ' || *in == '\t' || *in == '\r' || *in == '\n') in++; size_t n = strlen(in); while (n > 0 && (in[n - 1] == ' ' || in[n - 1] == '\t' || in[n - 1] == '\r' || in[n - 1] == '\n')) { n--; } if (n > MEMORY_MAX_CHARS) { n = MEMORY_MAX_CHARS; if (out_truncated) *out_truncated = 1; } char* out = (char*)malloc(n + 1U); if (!out) return NULL; if (n > 0) memcpy(out, in, n); out[n] = '\0'; return out; } static char* prepend_and_truncate_local(const char* existing, const char* incoming, int* out_truncated) { if (out_truncated) *out_truncated = 0; const char* old = existing ? existing : ""; const char* add = incoming ? incoming : ""; const char* sep = "\n\n---\n\n"; size_t add_len = strlen(add); size_t old_len = strlen(old); size_t sep_len = (add_len > 0 && old_len > 0) ? strlen(sep) : 0U; size_t total_len = add_len + sep_len + old_len; char* joined = (char*)malloc(total_len + 1U); if (!joined) return NULL; size_t used = 0U; if (add_len > 0) { memcpy(joined + used, add, add_len); used += add_len; } if (sep_len > 0) { memcpy(joined + used, sep, sep_len); used += sep_len; } if (old_len > 0) { memcpy(joined + used, old, old_len); used += old_len; } joined[used] = '\0'; if (used <= MEMORY_MAX_CHARS) { return joined; } if (out_truncated) *out_truncated = 1; char* clipped = (char*)malloc(MEMORY_MAX_CHARS + 1U); if (!clipped) { free(joined); return NULL; } memcpy(clipped, joined, MEMORY_MAX_CHARS); clipped[MEMORY_MAX_CHARS] = '\0'; free(joined); return clipped; } int memory_init(tools_context_t* ctx) { if (!ctx || !ctx->cfg) return -1; return memory_load_local(ctx); } void memory_cleanup(void) { pthread_mutex_lock(&g_memory_mutex); free(g_memory_plain); g_memory_plain = NULL; g_memory_loaded = 0; pthread_mutex_unlock(&g_memory_mutex); } char* execute_memory_save(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* content_j = cJSON_GetObjectItemCaseSensitive(args, "content"); if (!content_j || !cJSON_IsString(content_j) || !content_j->valuestring) { cJSON_Delete(args); return json_error_local("memory_save requires string content"); } int entry_truncated = 0; char* incoming = trim_and_limit_entry_local(content_j->valuestring, &entry_truncated); cJSON_Delete(args); if (!incoming) return NULL; if (incoming[0] == '\0') { free(incoming); return json_error_local("memory_save content cannot be empty"); } pthread_mutex_lock(&g_memory_mutex); char* old_copy = memory_cache_get_copy_locked_local(); pthread_mutex_unlock(&g_memory_mutex); if (!old_copy) { free(incoming); return NULL; } int body_truncated = 0; char* merged = prepend_and_truncate_local(old_copy, incoming, &body_truncated); free(old_copy); free(incoming); if (!merged) return NULL; char* ciphertext = NULL; if (nip44_encrypt_self_local(ctx, merged, &ciphertext) != 0) { free(merged); return json_error_local("memory_save NIP-44 encryption failed"); } cJSON* tags = cJSON_CreateArray(); if (!tags) { free(merged); free(ciphertext); return NULL; } cJSON* d_tag = cJSON_CreateArray(); cJSON* app_tag = cJSON_CreateArray(); if (!d_tag || !app_tag) { cJSON_Delete(d_tag); cJSON_Delete(app_tag); cJSON_Delete(tags); free(merged); free(ciphertext); return NULL; } cJSON_AddItemToArray(d_tag, cJSON_CreateString("d")); cJSON_AddItemToArray(d_tag, cJSON_CreateString(MEMORY_D_TAG)); cJSON_AddItemToArray(tags, d_tag); cJSON_AddItemToArray(app_tag, cJSON_CreateString("app")); cJSON_AddItemToArray(app_tag, cJSON_CreateString(MEMORY_APP_TAG)); cJSON_AddItemToArray(tags, app_tag); nostr_publish_result_t publish_result; memset(&publish_result, 0, sizeof(publish_result)); int rc = nostr_handler_publish_kind_event(MEMORY_KIND, ciphertext, tags, &publish_result); cJSON_Delete(tags); free(ciphertext); if (rc != 0) { free(merged); nostr_handler_publish_result_free(&publish_result); return json_error_local("memory_save publish failed"); } pthread_mutex_lock(&g_memory_mutex); memory_cache_set_locked_local(merged); pthread_mutex_unlock(&g_memory_mutex); cJSON* out = cJSON_CreateObject(); if (!out) { free(merged); nostr_handler_publish_result_free(&publish_result); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "d_tag", MEMORY_D_TAG); cJSON_AddNumberToObject(out, "kind", MEMORY_KIND); cJSON_AddNumberToObject(out, "entry_length", (double)strlen(merged)); cJSON_AddBoolToObject(out, "truncated", (entry_truncated || body_truncated) ? 1 : 0); cJSON_AddStringToObject(out, "event_id", publish_result.event_id); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); free(merged); nostr_handler_publish_result_free(&publish_result); return json; } char* execute_memory_recall(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); pthread_mutex_lock(&g_memory_mutex); int loaded = g_memory_loaded; pthread_mutex_unlock(&g_memory_mutex); if (!loaded) { (void)memory_load_local(ctx); } pthread_mutex_lock(&g_memory_mutex); char* memory = memory_cache_get_copy_locked_local(); pthread_mutex_unlock(&g_memory_mutex); if (!memory) return NULL; cJSON* out = cJSON_CreateObject(); if (!out) { free(memory); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "content", memory); cJSON_AddNumberToObject(out, "content_length", (double)strlen(memory)); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); free(memory); return json; } char* execute_my_memory(tools_context_t* ctx, const char* args_json) { return execute_memory_recall(ctx, args_json); } char* execute_memory_short_term_read(tools_context_t* ctx, const char* args_json) { (void)ctx; (void)args_json; cJSON* out = cJSON_CreateObject(); if (!out) return NULL; cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "content", ""); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; }