| #define _GNU_SOURCE |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <stdint.h> |
| #include <string.h> |
| #include <stdbool.h> |
| #include <unistd.h> |
| #include <time.h> |
| #include <curl/curl.h> |
| #include <dirent.h> |
| #include <errno.h> |
| #include <signal.h> |
| #include <sys/stat.h> |
| #include <limits.h> |
| #include <ctype.h> |
| #include "cJSON.h" |
| #include "interpreter.h" |
|
|
| |
| |
| |
| #define SHADOWCLAW_VERSION "3.4" |
| #define COLOR_RESET "\033[0m" |
| #define COLOR_RED "\033[31m" |
| #define COLOR_GREEN "\033[32m" |
| #define COLOR_YELLOW "\033[33m" |
| #define COLOR_BLUE "\033[34m" |
| #define COLOR_CYAN "\033[36m" |
| #define COLOR_BOLD "\033[1m" |
|
|
| |
| |
| |
| typedef enum { |
| BLOB_SYSTEM = 1, |
| BLOB_USER, |
| BLOB_ASSISTANT, |
| BLOB_TOOL_CALL, |
| BLOB_TOOL_RESULT, |
| BLOB_CORE_MEMORY, |
| BLOB_SKILL, |
| BLOB_CRONJOB, |
| BLOB_WEBHOOK |
| } BlobKind; |
|
|
| typedef struct Blob { |
| uint32_t kind; |
| uint64_t id; |
| size_t size; |
| char *data; |
| struct Blob *next; |
| } Blob; |
|
|
| static Blob *blob_list = NULL; |
| static uint64_t next_id = 1; |
| static int dirty = 0; |
|
|
| void blob_append(BlobKind kind, const char *data) { |
| Blob *b = malloc(sizeof(Blob)); |
| if (!b) abort(); |
| b->kind = kind; |
| b->id = next_id++; |
| b->size = strlen(data); |
| b->data = malloc(b->size + 1); |
| if (!b->data) abort(); |
| strcpy(b->data, data); |
| b->next = blob_list; |
| blob_list = b; |
| dirty = 1; |
| } |
|
|
| void blob_foreach_reverse(void (*cb)(Blob*, void*), void *user) { |
| |
| Blob *prev = NULL, *curr = blob_list, *next; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| curr = blob_list; |
| while (curr) { |
| cb(curr, user); |
| curr = curr->next; |
| } |
| |
| prev = NULL; |
| curr = blob_list; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| } |
|
|
| void blob_clear(void) { |
| Blob *b = blob_list; |
| while (b) { |
| Blob *next = b->next; |
| free(b->data); |
| free(b); |
| b = next; |
| } |
| blob_list = NULL; |
| next_id = 1; |
| dirty = 1; |
| } |
|
|
| int save_state(const char *filename) { |
| FILE *f = fopen(filename, "wb"); |
| if (!f) return -1; |
| uint32_t magic = 0x53484C42; |
| uint32_t version = 1; |
| fwrite(&magic, 4, 1, f); |
| fwrite(&version, 4, 1, f); |
| fwrite(&next_id, 8, 1, f); |
| |
| Blob *prev = NULL, *curr = blob_list, *next; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| curr = blob_list; |
| while (curr) { |
| fwrite(&curr->kind, 4, 1, f); |
| fwrite(&curr->id, 8, 1, f); |
| fwrite(&curr->size, 8, 1, f); |
| fwrite(curr->data, 1, curr->size + 1, f); |
| curr = curr->next; |
| } |
| |
| prev = NULL; |
| curr = blob_list; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| fclose(f); |
| dirty = 0; |
| return 0; |
| } |
|
|
| int load_state(const char *filename) { |
| FILE *f = fopen(filename, "rb"); |
| if (!f) return -1; |
| uint32_t magic, version; |
| if (fread(&magic, 4, 1, f) != 1 || magic != 0x53484C42) { |
| fclose(f); |
| return -1; |
| } |
| if (fread(&version, 4, 1, f) != 1 || version != 1) { |
| fclose(f); |
| return -1; |
| } |
| if (fread(&next_id, 8, 1, f) != 1) { |
| fclose(f); |
| return -1; |
| } |
| blob_clear(); |
| while (!feof(f)) { |
| uint32_t kind; |
| uint64_t id; |
| size_t size; |
| if (fread(&kind, 4, 1, f) != 1) break; |
| if (fread(&id, 8, 1, f) != 1) break; |
| if (fread(&size, 8, 1, f) != 1) break; |
| char *data = malloc(size + 1); |
| if (!data) { fclose(f); return -1; } |
| if (fread(data, 1, size + 1, f) != size + 1) { |
| free(data); |
| fclose(f); |
| return -1; |
| } |
| Blob *b = malloc(sizeof(Blob)); |
| if (!b) { free(data); fclose(f); return -1; } |
| b->kind = kind; |
| b->id = id; |
| b->size = size; |
| b->data = data; |
| b->next = blob_list; |
| blob_list = b; |
| } |
| fclose(f); |
| dirty = 0; |
| return 0; |
| } |
|
|
| void sync_soul_file(const char *soul_file) { |
| FILE *f = fopen(soul_file, "w"); |
| if (!f) return; |
| fprintf(f, "# ShadowSoul - Unified Memory\n"); |
| time_t now = time(NULL); |
| fprintf(f, "Last updated: %s", ctime(&now)); |
| fprintf(f, "\n## Core Memory\n"); |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CORE_MEMORY) |
| fprintf(f, "```json\n%s\n```\n\n", b->data); |
| b = b->next; |
| } |
| fprintf(f, "## Skills\n"); |
| b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_SKILL) |
| fprintf(f, "- %s\n", b->data); |
| b = b->next; |
| } |
| fprintf(f, "\n## Cron Jobs\n"); |
| b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CRONJOB) |
| fprintf(f, "- %s\n", b->data); |
| b = b->next; |
| } |
| fprintf(f, "\n## Webhooks\n"); |
| b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_WEBHOOK) |
| fprintf(f, "- %s\n", b->data); |
| b = b->next; |
| } |
| fprintf(f, "\n## Conversation Log\n"); |
| |
| Blob *prev = NULL, *curr = blob_list, *next; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| curr = blob_list; |
| while (curr) { |
| if (curr->kind == BLOB_USER || curr->kind == BLOB_ASSISTANT || |
| curr->kind == BLOB_TOOL_CALL || curr->kind == BLOB_TOOL_RESULT) { |
| const char *role = ""; |
| switch (curr->kind) { |
| case BLOB_USER: role = "User"; break; |
| case BLOB_ASSISTANT: role = "Assistant"; break; |
| case BLOB_TOOL_CALL: role = "Tool Call"; break; |
| case BLOB_TOOL_RESULT: role = "Tool Result"; break; |
| default: break; |
| } |
| fprintf(f, "### %s\n%s\n\n", role, curr->data); |
| } |
| curr = curr->next; |
| } |
| |
| prev = NULL; |
| curr = blob_list; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| fclose(f); |
| } |
|
|
| |
| |
| |
| typedef struct Tool { |
| const char *name; |
| char *(*func)(const char *args); |
| const char *description; |
| int enabled; |
| } Tool; |
|
|
| |
| char* tool_shell(const char *args); |
| char* tool_file_read(const char *args); |
| char* tool_file_write(const char *args); |
| char* tool_http_get(const char *args); |
| char* tool_math(const char *args); |
| char* tool_list_dir(const char *args); |
| char* tool_webhook_add(const char *args); |
| char* tool_cron_add(const char *args); |
| char* tool_cron_list(const char *args); |
| char* tool_cron_remove(const char *args); |
| char* tool_skill_add(const char *args); |
| char* tool_skill_run(const char *args); |
| char* tool_list_skills(const char *args); |
| char* tool_update_core_memory(const char *args); |
| char* tool_recall(const char *args); |
| char* tool_heartbeat(const char *args); |
|
|
| |
| char* tool_shell(const char *args) { |
| (void)args; |
| return strdup("Shell tool is disabled for security."); |
| } |
|
|
| #define MAX_FILE_SIZE (10 * 1024 * 1024) |
| #define MAX_TOOL_RESULT 2000 |
|
|
| char* tool_file_read(const char *args) { |
| FILE *f = fopen(args, "rb"); |
| if (!f) return strdup("Error: cannot open file"); |
| if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return strdup("Error seeking file"); } |
| long size = ftell(f); |
| if (size == -1 || size > MAX_FILE_SIZE) { fclose(f); return strdup("File too large"); } |
| rewind(f); |
| char *buf = malloc(size + 1); |
| if (!buf) { fclose(f); return strdup("Memory error"); } |
| size_t read = fread(buf, 1, size, f); |
| buf[read] = '\0'; |
| fclose(f); |
| if (strlen(buf) > MAX_TOOL_RESULT) buf[MAX_TOOL_RESULT] = '\0'; |
| return buf; |
| } |
|
|
| char* tool_file_write(const char *args) { |
| const char *space = strchr(args, ' '); |
| if (!space) return strdup("Invalid: need 'filename content'"); |
| size_t fname_len = space - args; |
| char *fname = malloc(fname_len + 1); |
| if (!fname) return strdup("Memory error"); |
| memcpy(fname, args, fname_len); |
| fname[fname_len] = '\0'; |
| const char *content = space + 1; |
| FILE *f = fopen(fname, "w"); |
| free(fname); |
| if (!f) return strdup("Cannot write file"); |
| fprintf(f, "%s", content); |
| fclose(f); |
| return strdup("File written"); |
| } |
|
|
| static size_t http_write_cb(void *ptr, size_t size, size_t nmemb, void *user) { |
| size_t total = size * nmemb; |
| char **resp = (char**)user; |
| size_t old_len = *resp ? strlen(*resp) : 0; |
| char *new = realloc(*resp, old_len + total + 1); |
| if (!new) return 0; |
| *resp = new; |
| memcpy(*resp + old_len, ptr, total); |
| (*resp)[old_len + total] = '\0'; |
| return total; |
| } |
|
|
| char* tool_http_get(const char *args) { |
| CURL *curl = curl_easy_init(); |
| if (!curl) return strdup("curl init failed"); |
| char *response = NULL; |
| curl_easy_setopt(curl, CURLOPT_URL, args); |
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_cb); |
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); |
| CURLcode res = curl_easy_perform(curl); |
| curl_easy_cleanup(curl); |
| if (res != CURLE_OK) { |
| free(response); |
| return strdup("HTTP request failed"); |
| } |
| if (response && strlen(response) > MAX_TOOL_RESULT) response[MAX_TOOL_RESULT] = '\0'; |
| return response ? response : strdup(""); |
| } |
|
|
| char* tool_math(const char *args) { |
| |
| char cmd[512]; |
| snprintf(cmd, sizeof(cmd), "echo '%s' | bc 2>/dev/null", args); |
| FILE *fp = popen(cmd, "r"); |
| if (!fp) return strdup("Math error"); |
| char buf[256]; |
| char *result = NULL; |
| size_t len = 0; |
| while (fgets(buf, sizeof(buf), fp)) { |
| size_t l = strlen(buf); |
| char *new = realloc(result, len + l + 1); |
| if (!new) { free(result); pclose(fp); return strdup("Memory error"); } |
| result = new; |
| memcpy(result + len, buf, l + 1); |
| len += l; |
| } |
| pclose(fp); |
| if (!result) result = strdup(""); |
| if (strlen(result) > MAX_TOOL_RESULT) result[MAX_TOOL_RESULT] = '\0'; |
| return result; |
| } |
|
|
| char* tool_list_dir(const char *args) { |
| DIR *d = opendir(args); |
| if (!d) return strdup("Cannot open directory"); |
| char *result = NULL; |
| size_t len = 0; |
| FILE *out = open_memstream(&result, &len); |
| if (!out) { closedir(d); return strdup("Memory error"); } |
| struct dirent *entry; |
| while ((entry = readdir(d)) != NULL) fprintf(out, "%s\n", entry->d_name); |
| closedir(d); |
| fclose(out); |
| if (!result) result = strdup(""); |
| if (strlen(result) > MAX_TOOL_RESULT) result[MAX_TOOL_RESULT] = '\0'; |
| return result; |
| } |
|
|
| char* tool_webhook_add(const char *args) { |
| cJSON *j = cJSON_Parse(args); |
| if (!j) return strdup("Invalid JSON"); |
| cJSON *url = cJSON_GetObjectItem(j, "url"); |
| cJSON *event = cJSON_GetObjectItem(j, "event"); |
| if (!cJSON_IsString(url) || !cJSON_IsString(event)) { |
| cJSON_Delete(j); |
| return strdup("Missing url or event"); |
| } |
| cJSON *obj = cJSON_CreateObject(); |
| cJSON_AddStringToObject(obj, "url", url->valuestring); |
| cJSON_AddStringToObject(obj, "event", event->valuestring); |
| char *entry = cJSON_PrintUnformatted(obj); |
| cJSON_Delete(obj); |
| cJSON_Delete(j); |
| if (entry) { |
| blob_append(BLOB_WEBHOOK, entry); |
| free(entry); |
| return strdup("Webhook added"); |
| } |
| return strdup("Memory error"); |
| } |
|
|
| char* tool_cron_add(const char *args) { |
| cJSON *j = cJSON_Parse(args); |
| if (!j) return strdup("Invalid JSON"); |
| cJSON *sched = cJSON_GetObjectItem(j, "schedule"); |
| cJSON *tool = cJSON_GetObjectItem(j, "tool"); |
| cJSON *tool_args = cJSON_GetObjectItem(j, "args"); |
| if (!cJSON_IsString(sched) || !cJSON_IsString(tool) || !cJSON_IsString(tool_args)) { |
| cJSON_Delete(j); |
| return strdup("Missing schedule, tool, or args"); |
| } |
| cJSON *obj = cJSON_CreateObject(); |
| cJSON_AddStringToObject(obj, "schedule", sched->valuestring); |
| cJSON_AddStringToObject(obj, "tool", tool->valuestring); |
| cJSON_AddStringToObject(obj, "args", tool_args->valuestring); |
| cJSON_AddNumberToObject(obj, "last_run", 0); |
| char *entry = cJSON_PrintUnformatted(obj); |
| cJSON_Delete(obj); |
| cJSON_Delete(j); |
| if (entry) { |
| blob_append(BLOB_CRONJOB, entry); |
| free(entry); |
| return strdup("Cron job added"); |
| } |
| return strdup("Memory error"); |
| } |
|
|
| char* tool_cron_list(const char *args) { |
| (void)args; |
| char *result = NULL; |
| size_t len = 0; |
| FILE *out = open_memstream(&result, &len); |
| if (!out) return strdup("Memory error"); |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CRONJOB) fprintf(out, "%s\n", b->data); |
| b = b->next; |
| } |
| fclose(out); |
| if (!result) result = strdup(""); |
| if (strlen(result) > MAX_TOOL_RESULT) result[MAX_TOOL_RESULT] = '\0'; |
| return result; |
| } |
|
|
| char* tool_cron_remove(const char *args) { |
| Blob *prev = NULL, *curr = blob_list; |
| int removed = 0; |
| while (curr) { |
| if (curr->kind == BLOB_CRONJOB && strstr(curr->data, args)) { |
| Blob *next = curr->next; |
| if (prev) prev->next = next; |
| else blob_list = next; |
| free(curr->data); |
| free(curr); |
| curr = next; |
| removed++; |
| dirty = 1; |
| } else { |
| prev = curr; |
| curr = curr->next; |
| } |
| } |
| char msg[64]; |
| snprintf(msg, sizeof(msg), "Removed %d cron job(s)", removed); |
| return strdup(msg); |
| } |
|
|
| char* tool_skill_add(const char *args) { |
| cJSON *j = cJSON_Parse(args); |
| if (!j) return strdup("Invalid JSON"); |
| cJSON *name = cJSON_GetObjectItem(j, "name"); |
| cJSON *desc = cJSON_GetObjectItem(j, "desc"); |
| cJSON *steps = cJSON_GetObjectItem(j, "steps"); |
| if (!cJSON_IsString(name) || !cJSON_IsString(desc) || !cJSON_IsArray(steps)) { |
| cJSON_Delete(j); |
| return strdup("Missing name, desc, or steps array"); |
| } |
| cJSON *obj = cJSON_CreateObject(); |
| cJSON_AddStringToObject(obj, "name", name->valuestring); |
| cJSON_AddStringToObject(obj, "desc", desc->valuestring); |
| cJSON_AddItemToObject(obj, "steps", cJSON_Duplicate(steps, 1)); |
| char *entry = cJSON_PrintUnformatted(obj); |
| cJSON_Delete(obj); |
| cJSON_Delete(j); |
| if (entry) { |
| blob_append(BLOB_SKILL, entry); |
| free(entry); |
| return strdup("Skill added"); |
| } |
| return strdup("Memory error"); |
| } |
|
|
| char* tool_skill_run(const char *args) { |
| char *copy = strdup(args); |
| if (!copy) return strdup("Memory error"); |
| char *space = strchr(copy, ' '); |
| if (space) *space = '\0'; |
| const char *skill_name = copy; |
| const char *skill_args = space ? space + 1 : ""; |
| char *result = NULL; |
| asprintf(&result, "RUN_SKILL:%s:%s", skill_name, skill_args); |
| free(copy); |
| return result ? result : strdup(""); |
| } |
|
|
| char* tool_list_skills(const char *args) { |
| (void)args; |
| char *result = NULL; |
| size_t len = 0; |
| FILE *out = open_memstream(&result, &len); |
| if (!out) return strdup("Memory error"); |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_SKILL) { |
| cJSON *j = cJSON_Parse(b->data); |
| if (j) { |
| cJSON *name = cJSON_GetObjectItem(j, "name"); |
| if (cJSON_IsString(name)) fprintf(out, "%s\n", name->valuestring); |
| cJSON_Delete(j); |
| } |
| } |
| b = b->next; |
| } |
| fclose(out); |
| if (!result) result = strdup(""); |
| if (strlen(result) > MAX_TOOL_RESULT) result[MAX_TOOL_RESULT] = '\0'; |
| return result; |
| } |
|
|
| char* tool_update_core_memory(const char *args) { |
| cJSON *updates = cJSON_Parse(args); |
| if (!updates) return strdup("Invalid JSON"); |
| |
| cJSON *core = NULL; |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CORE_MEMORY) { |
| core = cJSON_Parse(b->data); |
| break; |
| } |
| b = b->next; |
| } |
| if (!core) core = cJSON_CreateObject(); |
| |
| cJSON *child = NULL; |
| cJSON_ArrayForEach(child, updates) { |
| cJSON_AddItemToObject(core, child->string, cJSON_Duplicate(child, 1)); |
| } |
| char *new_core = cJSON_PrintUnformatted(core); |
| cJSON_Delete(core); |
| cJSON_Delete(updates); |
| if (!new_core) return strdup("Memory error"); |
| |
| Blob *prev = NULL, *curr = blob_list; |
| while (curr) { |
| if (curr->kind == BLOB_CORE_MEMORY) { |
| if (prev) prev->next = curr->next; |
| else blob_list = curr->next; |
| free(curr->data); |
| free(curr); |
| break; |
| } |
| prev = curr; |
| curr = curr->next; |
| } |
| blob_append(BLOB_CORE_MEMORY, new_core); |
| free(new_core); |
| dirty = 1; |
| return strdup("Core memory updated"); |
| } |
|
|
| char* tool_recall(const char *args) { |
| char *result = NULL; |
| size_t len = 0; |
| FILE *out = open_memstream(&result, &len); |
| if (!out) return strdup("Memory error"); |
| Blob *b = blob_list; |
| while (b) { |
| if ((b->kind == BLOB_USER || b->kind == BLOB_ASSISTANT) && strcasestr(b->data, args)) { |
| const char *role = (b->kind == BLOB_USER) ? "User" : "Assistant"; |
| fprintf(out, "[%s] %s\n", role, b->data); |
| } |
| b = b->next; |
| } |
| fclose(out); |
| if (!result) result = strdup(""); |
| if (strlen(result) > MAX_TOOL_RESULT) result[MAX_TOOL_RESULT] = '\0'; |
| return result; |
| } |
|
|
| char* tool_heartbeat(const char *args) { |
| (void)args; |
| return strdup("Heartbeat"); |
| } |
|
|
| static Tool tools[] = { |
| {"shell", tool_shell, "Execute shell command (disabled)", 0}, |
| {"file_read", tool_file_read, "Read a file", 1}, |
| {"file_write", tool_file_write, "Write a file", 1}, |
| {"http_get", tool_http_get, "HTTP GET request", 1}, |
| {"math", tool_math, "Evaluate arithmetic expression", 1}, |
| {"list_dir", tool_list_dir, "List directory contents", 1}, |
| {"webhook_add", tool_webhook_add, "Add a webhook", 1}, |
| {"cron_add", tool_cron_add, "Add a cron job", 1}, |
| {"cron_list", tool_cron_list, "List cron jobs", 1}, |
| {"cron_remove", tool_cron_remove, "Remove cron jobs", 1}, |
| {"skill_add", tool_skill_add, "Add a skill", 1}, |
| {"skill_run", tool_skill_run, "Run a skill", 1}, |
| {"list_skills", tool_list_skills, "List skills", 1}, |
| {"update_core_memory", tool_update_core_memory, "Update core memory", 1}, |
| {"recall", tool_recall, "Search conversation history", 1}, |
| {"heartbeat", tool_heartbeat, "Internal heartbeat", 1}, |
| {NULL, NULL, NULL, 0} |
| }; |
|
|
| char* execute_tool(const char *name, const char *args) { |
| for (Tool *t = tools; t->name; t++) { |
| if (strcmp(t->name, name) == 0 && t->enabled) { |
| return t->func(args); |
| } |
| } |
| return strdup("Unknown or disabled tool"); |
| } |
|
|
| |
| |
| |
| static int no_llm_mode = 0; |
| static int dry_run = 0; |
| static char state_filename[256] = "shadowclaw.bin"; |
| static char soul_filename[256] = "shadowclaw_data/shadowsoul.md"; |
| static const char *ollama_endpoint = "http://localhost:11434"; |
| static const char *ollama_model = "qwen2.5:0.5b"; |
| static long llm_connect_timeout = 15; |
| static long llm_total_timeout = 180; |
| static int llm_retry_attempts = 5; |
| static int shell_enabled = 0; |
|
|
| static int ollama_healthy(void) { |
| CURL *curl = curl_easy_init(); |
| if (!curl) return 0; |
| curl_easy_setopt(curl, CURLOPT_URL, ollama_endpoint); |
| curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L); |
| CURLcode res = curl_easy_perform(curl); |
| curl_easy_cleanup(curl); |
| return (res == CURLE_OK); |
| } |
|
|
| typedef struct { |
| char *data; |
| size_t len; |
| } RespBuf; |
|
|
| static size_t llm_write_cb(void *ptr, size_t size, size_t nmemb, void *stream) { |
| RespBuf *buf = (RespBuf*)stream; |
| size_t total = size * nmemb; |
| char *new = realloc(buf->data, buf->len + total + 1); |
| if (!new) return 0; |
| buf->data = new; |
| memcpy(buf->data + buf->len, ptr, total); |
| buf->len += total; |
| buf->data[buf->len] = '\0'; |
| return total; |
| } |
|
|
| static char* call_llm(const char *prompt) { |
| if (!ollama_healthy()) { |
| fprintf(stderr, COLOR_YELLOW "Ollama unreachable. Switching to no-llm mode.\n" COLOR_RESET); |
| no_llm_mode = 1; |
| return NULL; |
| } |
| CURL *curl = curl_easy_init(); |
| if (!curl) return NULL; |
| char url[256]; |
| snprintf(url, sizeof(url), "%s/api/generate", ollama_endpoint); |
| cJSON *req = cJSON_CreateObject(); |
| cJSON_AddStringToObject(req, "model", ollama_model); |
| cJSON_AddStringToObject(req, "prompt", prompt); |
| cJSON_AddBoolToObject(req, "stream", 0); |
| char *req_str = cJSON_PrintUnformatted(req); |
| cJSON_Delete(req); |
| struct curl_slist *headers = NULL; |
| headers = curl_slist_append(headers, "Content-Type: application/json"); |
| RespBuf resp = {0}; |
| curl_easy_setopt(curl, CURLOPT_URL, url); |
| curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_str); |
| curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); |
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, llm_write_cb); |
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); |
| curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, llm_connect_timeout); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT, llm_total_timeout); |
| CURLcode res = curl_easy_perform(curl); |
| free(req_str); |
| curl_slist_free_all(headers); |
| curl_easy_cleanup(curl); |
| if (res != CURLE_OK) { |
| fprintf(stderr, COLOR_RED "LLM request failed: %s\n" COLOR_RESET, curl_easy_strerror(res)); |
| free(resp.data); |
| return NULL; |
| } |
| cJSON *root = cJSON_Parse(resp.data); |
| free(resp.data); |
| if (!root) return NULL; |
| cJSON *response = cJSON_GetObjectItem(root, "response"); |
| char *result = response && cJSON_IsString(response) ? strdup(response->valuestring) : NULL; |
| cJSON_Delete(root); |
| return result; |
| } |
|
|
| static char* build_simple_prompt(const char *user_msg) { |
| |
| char tools_list[1024] = ""; |
| for (Tool *t = tools; t->name; t++) { |
| if (t->enabled) { |
| strcat(tools_list, t->name); |
| strcat(tools_list, ", "); |
| } |
| } |
| if (strlen(tools_list) > 2) tools_list[strlen(tools_list)-2] = '\0'; |
| |
| char *core = strdup("{}"); |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CORE_MEMORY) { |
| free(core); |
| core = strdup(b->data); |
| break; |
| } |
| b = b->next; |
| } |
| |
| char history[4096] = ""; |
| Blob *prev = NULL, *curr = blob_list, *next; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| int count = 0; |
| curr = blob_list; |
| while (curr && count < 10) { |
| if (curr->kind == BLOB_USER || curr->kind == BLOB_ASSISTANT || |
| curr->kind == BLOB_TOOL_CALL || curr->kind == BLOB_TOOL_RESULT) { |
| const char *role = ""; |
| switch (curr->kind) { |
| case BLOB_USER: role = "User"; break; |
| case BLOB_ASSISTANT: role = "Assistant"; break; |
| case BLOB_TOOL_CALL: role = "Tool call"; break; |
| case BLOB_TOOL_RESULT: role = "Tool result"; break; |
| } |
| char line[512]; |
| snprintf(line, sizeof(line), "%s: %s\n", role, curr->data); |
| strcat(history, line); |
| count++; |
| } |
| curr = curr->next; |
| } |
| |
| prev = NULL; |
| curr = blob_list; |
| while (curr) { |
| next = curr->next; |
| curr->next = prev; |
| prev = curr; |
| curr = next; |
| } |
| blob_list = prev; |
| char *prompt; |
| asprintf(&prompt, |
| "You are Shadowclaw, an AI assistant with access to tools.\n" |
| "Core memory: %s\n\n" |
| "Recent conversation:\n%s\n\n" |
| "Available tools: %s\n" |
| "To use a tool, output exactly:\n```tool\n{\"tool\":\"name\",\"args\":\"arguments\"}\n```\n" |
| "Otherwise, respond normally.\n\n" |
| "User: %s\n", |
| core, history, tools_list, user_msg); |
| free(core); |
| return prompt; |
| } |
|
|
| static char* extract_tool_json(const char *response) { |
| const char *start = strstr(response, "```tool"); |
| if (!start) return NULL; |
| start += 7; |
| while (*start == ' ' || *start == '\n') start++; |
| const char *end = strstr(start, "```"); |
| if (!end) return NULL; |
| size_t len = end - start; |
| char *json = malloc(len + 1); |
| if (!json) return NULL; |
| memcpy(json, start, len); |
| json[len] = '\0'; |
| return json; |
| } |
|
|
| static int parse_tool_call(const char *json_str, char **tool_name, char **tool_args) { |
| cJSON *root = cJSON_Parse(json_str); |
| if (!root) return 0; |
| cJSON *name = cJSON_GetObjectItem(root, "tool"); |
| cJSON *args = cJSON_GetObjectItem(root, "args"); |
| if (!cJSON_IsString(name)) { |
| cJSON_Delete(root); |
| return 0; |
| } |
| *tool_name = strdup(name->valuestring); |
| if (cJSON_IsString(args)) { |
| *tool_args = strdup(args->valuestring); |
| } else if (cJSON_IsArray(args)) { |
| int size = cJSON_GetArraySize(args); |
| size_t total = 0; |
| for (int i = 0; i < size; i++) { |
| cJSON *elem = cJSON_GetArrayItem(args, i); |
| if (cJSON_IsString(elem)) total += strlen(elem->valuestring) + 1; |
| } |
| *tool_args = malloc(total + 1); |
| if (*tool_args) { |
| (*tool_args)[0] = '\0'; |
| for (int i = 0; i < size; i++) { |
| cJSON *elem = cJSON_GetArrayItem(args, i); |
| if (cJSON_IsString(elem)) { |
| if (i > 0) strcat(*tool_args, " "); |
| strcat(*tool_args, elem->valuestring); |
| } |
| } |
| } else { |
| *tool_args = strdup(""); |
| } |
| } else { |
| *tool_args = strdup(""); |
| } |
| cJSON_Delete(root); |
| return 1; |
| } |
|
|
| |
| static int http_post_with_retry(const char *url, const char *payload) { |
| int delays[] = {1, 2, 4}; |
| for (int i = 0; i < 3; i++) { |
| CURL *curl = curl_easy_init(); |
| if (!curl) continue; |
| struct curl_slist *headers = NULL; |
| headers = curl_slist_append(headers, "Content-Type: application/json"); |
| curl_easy_setopt(curl, CURLOPT_URL, url); |
| curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); |
| curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); |
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); |
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, stdout); |
| CURLcode res = curl_easy_perform(curl); |
| curl_easy_cleanup(curl); |
| curl_slist_free_all(headers); |
| if (res == CURLE_OK) return 0; |
| sleep(delays[i]); |
| } |
| fprintf(stderr, COLOR_RED "Webhook POST failed after retries\n" COLOR_RESET); |
| return -1; |
| } |
|
|
| static void trigger_webhooks(const char *event, const char *data) { |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_WEBHOOK) { |
| cJSON *j = cJSON_Parse(b->data); |
| if (j) { |
| cJSON *url = cJSON_GetObjectItem(j, "url"); |
| cJSON *ev = cJSON_GetObjectItem(j, "event"); |
| if (cJSON_IsString(url) && cJSON_IsString(ev) && strcmp(ev->valuestring, event) == 0) { |
| cJSON *payload = cJSON_CreateObject(); |
| cJSON_AddStringToObject(payload, "event", event); |
| cJSON_AddStringToObject(payload, "data", data); |
| char *payload_str = cJSON_PrintUnformatted(payload); |
| cJSON_Delete(payload); |
| if (payload_str) { |
| http_post_with_retry(url->valuestring, payload_str); |
| free(payload_str); |
| } |
| } |
| cJSON_Delete(j); |
| } |
| } |
| b = b->next; |
| } |
| } |
|
|
| |
| static void process_cron_jobs(void) { |
| time_t now = time(NULL); |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CRONJOB) { |
| cJSON *j = cJSON_Parse(b->data); |
| if (!j) { b = b->next; continue; } |
| cJSON *sched = cJSON_GetObjectItem(j, "schedule"); |
| cJSON *tool = cJSON_GetObjectItem(j, "tool"); |
| cJSON *args = cJSON_GetObjectItem(j, "args"); |
| cJSON *last_run_json = cJSON_GetObjectItem(j, "last_run"); |
| if (cJSON_IsString(sched) && cJSON_IsString(tool) && cJSON_IsString(args) && cJSON_IsNumber(last_run_json)) { |
| time_t last_run = (time_t)last_run_json->valuedouble; |
| int interval = 0; |
| if (strcmp(sched->valuestring, "@heartbeat") == 0) interval = 120; |
| else if (strncmp(sched->valuestring, "@every ", 7) == 0) { |
| int val = atoi(sched->valuestring + 7); |
| interval = val; |
| } else if (strcmp(sched->valuestring, "@hourly") == 0) interval = 3600; |
| else if (strcmp(sched->valuestring, "@daily") == 0) interval = 86400; |
| else if (strcmp(sched->valuestring, "@weekly") == 0) interval = 604800; |
| if (interval > 0 && (now - last_run) >= interval) { |
| if (strcmp(tool->valuestring, "heartbeat") == 0) { |
| |
| char *result = execute_tool("heartbeat", ""); |
| printf(COLOR_CYAN "[Cron] Heartbeat: %s\n" COLOR_RESET, result); |
| free(result); |
| } else { |
| |
| char *result = execute_tool(tool->valuestring, args->valuestring); |
| printf(COLOR_CYAN "[Cron] %s(%s) -> %s\n" COLOR_RESET, tool->valuestring, args->valuestring, result); |
| free(result); |
| } |
| |
| cJSON_SetNumberValue(last_run_json, (double)now); |
| char *new_data = cJSON_PrintUnformatted(j); |
| if (new_data) { |
| |
| Blob *prev = NULL, *curr = blob_list; |
| while (curr) { |
| if (curr == b) { |
| if (prev) prev->next = curr->next; |
| else blob_list = curr->next; |
| free(curr->data); |
| free(curr); |
| break; |
| } |
| prev = curr; |
| curr = curr->next; |
| } |
| blob_append(BLOB_CRONJOB, new_data); |
| free(new_data); |
| dirty = 1; |
| break; |
| } |
| } |
| } |
| cJSON_Delete(j); |
| } |
| b = b->next; |
| } |
| } |
|
|
| |
| |
| |
| static void print_help(void) { |
| printf(COLOR_BOLD "\nShadowclaw v%s commands:\n" COLOR_RESET, SHADOWCLAW_VERSION); |
| printf(" %-12s %s\n", "/help", "Show this help"); |
| printf(" %-12s %s\n", "/tools", "List available tools"); |
| printf(" %-12s %s\n", "/state", "Show arena memory stats and soul file info"); |
| printf(" %-12s %s\n", "/clear", "Clear conversation history"); |
| printf(" %-12s %s\n", "/exit", "Exit Shadowclaw"); |
| printf(" %-12s %s\n", "/loop", "Schedule a recurring task: /loop <schedule> <tool> [args...]"); |
| printf(" %-12s %s\n", "/crons", "List scheduled cron jobs"); |
| printf(" %-12s %s\n", "/webhooks", "List registered webhooks"); |
| printf(" %-12s %s\n", "/skills", "List dynamic skills"); |
| printf(" %-12s %s\n", "/compact", "Manually compact arena (remove deleted blobs)"); |
| printf(" %-12s %s\n", "/soul", "Show path to shadowsoul.md and last update"); |
| printf(" %-12s %s\n", "/reconnect", "Reconnect to Ollama (if LLM mode is off)"); |
|
|
| printf(COLOR_BOLD "\nBuilt-in tools:\n" COLOR_RESET); |
| for (Tool *t = tools; t->name; t++) printf(" %s\n", t->name); |
|
|
| int skill_count = 0; |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_SKILL) { |
| cJSON *j = cJSON_Parse(b->data); |
| if (j) { |
| cJSON *name = cJSON_GetObjectItem(j, "name"); |
| cJSON *desc = cJSON_GetObjectItem(j, "desc"); |
| if (cJSON_IsString(name) && cJSON_IsString(desc)) { |
| if (skill_count == 0) printf(COLOR_BOLD "\nDynamic skills:\n" COLOR_RESET); |
| printf(" %-12s %s\n", name->valuestring, desc->valuestring); |
| skill_count++; |
| } |
| cJSON_Delete(j); |
| } |
| } |
| b = b->next; |
| } |
| if (skill_count == 0) printf("\nNo dynamic skills loaded. Use skill_add to create one.\n"); |
|
|
| struct stat st; |
| if (stat(soul_filename, &st) == 0) { |
| char timebuf[128]; |
| struct tm *tm_info = localtime(&st.st_mtime); |
| strftime(timebuf, sizeof(timebuf), "%c", tm_info); |
| printf("\nSoul file: %s (%ld bytes, last modified: %s\n", soul_filename, st.st_size, timebuf); |
| } else { |
| printf("\nSoul file not yet created.\n"); |
| } |
| printf("\n"); |
| } |
|
|
| static void list_tools(void) { |
| printf(COLOR_BOLD "Enabled tools:\n" COLOR_RESET); |
| for (Tool *t = tools; t->name; t++) { |
| if (t->enabled) |
| printf(" %-12s %s\n", t->name, t->description); |
| } |
| } |
|
|
| static void show_state(void) { |
| size_t total_size = 0; |
| Blob *b = blob_list; |
| while (b) { |
| total_size += sizeof(Blob) + b->size + 1; |
| b = b->next; |
| } |
| printf("Blob count: %llu (next id: %llu)\n", (unsigned long long)(next_id - 1), (unsigned long long)next_id); |
| printf("Estimated memory usage: ~%zu bytes\n", total_size); |
| struct stat st; |
| if (stat(soul_filename, &st) == 0) { |
| char timebuf[128]; |
| struct tm *tm_info = localtime(&st.st_mtime); |
| strftime(timebuf, sizeof(timebuf), "%c", tm_info); |
| printf("Soul file: %s (%ld bytes, last modified: %s\n", soul_filename, st.st_size, timebuf); |
| } else { |
| printf("Soul file not yet created.\n"); |
| } |
| } |
|
|
| static void clear_conversation(void) { |
| |
| Blob *new_list = NULL; |
| Blob *b = blob_list; |
| while (b) { |
| Blob *next = b->next; |
| if (b->kind == BLOB_SYSTEM || b->kind == BLOB_CORE_MEMORY || |
| b->kind == BLOB_SKILL || b->kind == BLOB_CRONJOB || b->kind == BLOB_WEBHOOK) { |
| |
| b->next = new_list; |
| new_list = b; |
| } else { |
| free(b->data); |
| free(b); |
| } |
| b = next; |
| } |
| blob_list = new_list; |
| dirty = 1; |
| printf("Conversation history cleared (system, core memory, skills, cron, webhooks preserved).\n"); |
| } |
|
|
| static void compact_arena(void) { |
| |
| |
| |
| |
| if (save_state(state_filename) == 0) { |
| load_state(state_filename); |
| printf("Arena compacted (state saved and reloaded).\n"); |
| } else { |
| printf("Compaction failed.\n"); |
| } |
| } |
|
|
| static const char* parse_schedule_string(const char *sched) { |
| static char buf[32]; |
| if (strcmp(sched, "hourly") == 0) return "@hourly"; |
| if (strcmp(sched, "daily") == 0) return "@daily"; |
| if (strcmp(sched, "weekly") == 0) return "@weekly"; |
| char *end; |
| long val = strtol(sched, &end, 10); |
| if (end != sched && *end) { |
| if (strcmp(end, "s") == 0) { snprintf(buf, sizeof(buf), "@every %lds", val); return buf; } |
| if (strcmp(end, "m") == 0) { snprintf(buf, sizeof(buf), "@every %ldm", val); return buf; } |
| if (strcmp(end, "h") == 0) { snprintf(buf, sizeof(buf), "@every %ldh", val); return buf; } |
| } |
| return NULL; |
| } |
|
|
| |
| |
| |
| int main(int argc, char **argv) { |
| for (int i = 1; i < argc; i++) { |
| if (strcmp(argv[i], "--no-llm") == 0) no_llm_mode = 1; |
| else if (strcmp(argv[i], "--dry-run") == 0) dry_run = 1; |
| else if (strcmp(argv[i], "--enable-shell") == 0) { |
| shell_enabled = 1; |
| for (Tool *t = tools; t->name; t++) |
| if (strcmp(t->name, "shell") == 0) t->enabled = 1; |
| } else if (strcmp(argv[i], "--model") == 0 && i+1 < argc) ollama_model = argv[++i]; |
| else if (strcmp(argv[i], "-f") == 0 && i+1 < argc) strncpy(state_filename, argv[++i], 255); |
| } |
|
|
| curl_global_init(CURL_GLOBAL_ALL); |
| |
| |
| char *soul_dir = strdup(soul_filename); |
| char *last_slash = strrchr(soul_dir, '/'); |
| if (last_slash) { |
| *last_slash = '\0'; |
| mkdir(soul_dir, 0755); |
| } |
| free(soul_dir); |
| |
| if (load_state(state_filename) != 0) { |
| printf("No saved state found. Starting fresh.\n"); |
| blob_append(BLOB_SYSTEM, "You are Shadowclaw, a helpful AI assistant with access to tools."); |
| blob_append(BLOB_CORE_MEMORY, "{}"); |
| save_state(state_filename); |
| } |
| sync_soul_file(soul_filename); |
|
|
| printf(COLOR_BOLD "\nShadowclaw v%s – Ollama-Enhanced\n" COLOR_RESET, SHADOWCLAW_VERSION); |
| if (no_llm_mode) printf(COLOR_YELLOW "LLM mode disabled. Using local interpreter.\n" COLOR_RESET); |
| else printf(COLOR_GREEN "LLM mode enabled (model: %s)\n" COLOR_RESET, ollama_model); |
| |
| |
| int has_heartbeat = 0; |
| Blob *b = blob_list; |
| while (b) { |
| if (b->kind == BLOB_CRONJOB && strstr(b->data, "\"tool\":\"heartbeat\"")) { |
| has_heartbeat = 1; |
| break; |
| } |
| b = b->next; |
| } |
| if (!has_heartbeat) { |
| char *heartbeat_json = NULL; |
| asprintf(&heartbeat_json, "{\"schedule\":\"@every 120s\",\"tool\":\"heartbeat\",\"args\":\"\",\"last_run\":0}"); |
| if (heartbeat_json) { |
| blob_append(BLOB_CRONJOB, heartbeat_json); |
| free(heartbeat_json); |
| dirty = 1; |
| } |
| } |
|
|
| char line[4096]; |
| while (1) { |
| process_cron_jobs(); |
| printf(COLOR_GREEN "> " COLOR_RESET); |
| fflush(stdout); |
| if (!fgets(line, sizeof(line), stdin)) break; |
| line[strcspn(line, "\n")] = 0; |
| if (strlen(line) == 0) continue; |
|
|
| if (line[0] == '/') { |
| if (strcmp(line, "/help") == 0) { |
| print_help(); |
| } else if (strcmp(line, "/tools") == 0) { |
| list_tools(); |
| } else if (strcmp(line, "/state") == 0) { |
| show_state(); |
| } else if (strcmp(line, "/clear") == 0) { |
| clear_conversation(); |
| save_state(state_filename); |
| sync_soul_file(soul_filename); |
| } else if (strcmp(line, "/exit") == 0) { |
| break; |
| } else if (strncmp(line, "/loop", 5) == 0) { |
| char *p = line + 5; |
| while (*p == ' ') p++; |
| if (*p == '\0') { |
| printf("Usage: /loop <schedule> <tool> [args...]\n" |
| "Examples:\n /loop 1h http_get https://example.com/news\n" |
| " /loop daily file_read ~/notes.txt\n"); |
| continue; |
| } |
| char *sched_start = p; |
| while (*p && !isspace(*p)) p++; |
| if (!*p) { printf("Missing tool after schedule.\n"); continue; } |
| *p++ = '\0'; |
| const char *sched_str = parse_schedule_string(sched_start); |
| if (!sched_str) { |
| printf("Unrecognized schedule format. Use e.g., 1h, 30m, daily, hourly, weekly.\n"); |
| continue; |
| } |
| while (*p == ' ') p++; |
| if (*p == '\0') { printf("Missing tool name.\n"); continue; } |
| char *tool_start = p; |
| while (*p && !isspace(*p)) p++; |
| if (*p) *p++ = '\0'; |
| while (*p == ' ') p++; |
| char *args = p; |
| |
| cJSON *obj = cJSON_CreateObject(); |
| cJSON_AddStringToObject(obj, "schedule", sched_str); |
| cJSON_AddStringToObject(obj, "tool", tool_start); |
| cJSON_AddStringToObject(obj, "args", args); |
| cJSON_AddNumberToObject(obj, "last_run", 0); |
| char *json = cJSON_PrintUnformatted(obj); |
| cJSON_Delete(obj); |
| if (json) { |
| blob_append(BLOB_CRONJOB, json); |
| free(json); |
| save_state(state_filename); |
| sync_soul_file(soul_filename); |
| printf("Scheduled: %s every %s\n", tool_start, sched_str); |
| } else { |
| printf("Failed to create cron job.\n"); |
| } |
| } else if (strcmp(line, "/crons") == 0) { |
| Blob *b = blob_list; |
| printf("Scheduled cron jobs:\n"); |
| int count = 0; |
| while (b) { |
| if (b->kind == BLOB_CRONJOB) { |
| printf(" %s\n", b->data); |
| count++; |
| } |
| b = b->next; |
| } |
| if (count == 0) printf(" None\n"); |
| } else if (strcmp(line, "/webhooks") == 0) { |
| Blob *b = blob_list; |
| printf("Registered webhooks:\n"); |
| int count = 0; |
| while (b) { |
| if (b->kind == BLOB_WEBHOOK) { |
| printf(" %s\n", b->data); |
| count++; |
| } |
| b = b->next; |
| } |
| if (count == 0) printf(" None\n"); |
| } else if (strcmp(line, "/skills") == 0) { |
| Blob *b = blob_list; |
| printf("Dynamic skills:\n"); |
| int count = 0; |
| while (b) { |
| if (b->kind == BLOB_SKILL) { |
| cJSON *j = cJSON_Parse(b->data); |
| if (j) { |
| cJSON *name = cJSON_GetObjectItem(j, "name"); |
| cJSON *desc = cJSON_GetObjectItem(j, "desc"); |
| if (cJSON_IsString(name) && cJSON_IsString(desc)) { |
| printf(" %s: %s\n", name->valuestring, desc->valuestring); |
| count++; |
| } |
| cJSON_Delete(j); |
| } |
| } |
| b = b->next; |
| } |
| if (count == 0) printf(" None\n"); |
| } else if (strcmp(line, "/compact") == 0) { |
| compact_arena(); |
| sync_soul_file(soul_filename); |
| } else if (strcmp(line, "/soul") == 0) { |
| struct stat st; |
| if (stat(soul_filename, &st) == 0) { |
| char timebuf[128]; |
| struct tm *tm_info = localtime(&st.st_mtime); |
| strftime(timebuf, sizeof(timebuf), "%c", tm_info); |
| printf("Soul file: %s (%ld bytes, last modified: %s\n", soul_filename, st.st_size, timebuf); |
| } else { |
| printf("Soul file not found.\n"); |
| } |
| } else if (strcmp(line, "/reconnect") == 0) { |
| if (ollama_healthy()) { |
| no_llm_mode = 0; |
| printf("Reconnected to Ollama. LLM mode enabled.\n"); |
| } else { |
| printf("Ollama still unreachable.\n"); |
| } |
| } else { |
| |
| char *resp = interpret_command(line); |
| printf("%s\n", resp); |
| free(resp); |
| } |
| if (dirty) { |
| save_state(state_filename); |
| sync_soul_file(soul_filename); |
| dirty = 0; |
| } |
| continue; |
| } |
|
|
| if (no_llm_mode) { |
| char *resp = interpret_command(line); |
| printf(COLOR_BLUE "%s\n" COLOR_RESET, resp); |
| free(resp); |
| continue; |
| } |
|
|
| blob_append(BLOB_USER, line); |
| sync_soul_file(soul_filename); |
| int max_iter = 3; |
| char *last_response = NULL; |
| for (int iter = 0; iter < max_iter; iter++) { |
| char *prompt = build_simple_prompt(line); |
| char *llm_out = call_llm(prompt); |
| free(prompt); |
| if (!llm_out) { |
| printf("LLM call failed. Try again or use /reconnect.\n"); |
| break; |
| } |
| char *tool_json = extract_tool_json(llm_out); |
| if (!tool_json) { |
| |
| printf(COLOR_BLUE "Assistant: %s\n" COLOR_RESET, llm_out); |
| blob_append(BLOB_ASSISTANT, llm_out); |
| last_response = llm_out; |
| break; |
| } |
| char *tool_name = NULL, *tool_args = NULL; |
| if (parse_tool_call(tool_json, &tool_name, &tool_args)) { |
| if (dry_run) { |
| printf(COLOR_YELLOW "[DRY RUN] would execute %s(%s)\n" COLOR_RESET, tool_name, tool_args); |
| } else { |
| char *result = execute_tool(tool_name, tool_args); |
| printf(COLOR_YELLOW "\n[Tool %s] → %s\n" COLOR_RESET, tool_name, result); |
| blob_append(BLOB_TOOL_CALL, tool_json); |
| blob_append(BLOB_TOOL_RESULT, result); |
| trigger_webhooks(tool_name, result); |
| |
| char *next_prompt; |
| asprintf(&next_prompt, "Tool result: %s\nContinue.", result); |
| free(llm_out); |
| llm_out = call_llm(next_prompt); |
| free(next_prompt); |
| free(result); |
| if (!llm_out) break; |
| |
| free(tool_json); |
| free(tool_name); |
| free(tool_args); |
| continue; |
| } |
| } else { |
| printf(COLOR_RED "Invalid tool JSON: %s\n" COLOR_RESET, tool_json); |
| blob_append(BLOB_ASSISTANT, llm_out); |
| last_response = llm_out; |
| } |
| free(tool_json); |
| free(tool_name); |
| free(tool_args); |
| break; |
| } |
| if (last_response) free(last_response); |
| save_state(state_filename); |
| sync_soul_file(soul_filename); |
| } |
|
|
| save_state(state_filename); |
| curl_global_cleanup(); |
| return 0; |
| } |
|
|