| | #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 "cJSON.h" |
| |
|
| | |
| | |
| | |
| | typedef struct { |
| | size_t capacity; |
| | size_t length; |
| | uint64_t tag; |
| | uint32_t version; |
| | uint32_t flags; |
| | } ShadowHeader; |
| |
|
| | #define SHADOW_MAGIC 0x534841444F57434CULL |
| | #define SHADOW_SIZE (sizeof(ShadowHeader)) |
| | #define ALIGN_UP(x, a) (((x) + (a)-1) & ~((a)-1)) |
| |
|
| | static inline ShadowHeader* shadow_header(void *data) { |
| | return (ShadowHeader*)((char*)data - SHADOW_SIZE); |
| | } |
| |
|
| | typedef struct { |
| | void *data; |
| | size_t reserved; |
| | } ShadowArena; |
| |
|
| | ShadowArena shadow_arena_create(size_t initial_capacity) { |
| | ShadowArena a = {0}; |
| | size_t total = SHADOW_SIZE + initial_capacity; |
| | total = ALIGN_UP(total, 64); |
| |
|
| | ShadowHeader *h = malloc(total); |
| | if (!h) abort(); |
| |
|
| | *h = (ShadowHeader){ |
| | .capacity = initial_capacity, |
| | .length = 0, |
| | .tag = SHADOW_MAGIC, |
| | .version = 1, |
| | .flags = 0 |
| | }; |
| | a.data = (char*)h + SHADOW_SIZE; |
| | a.reserved = total; |
| | return a; |
| | } |
| |
|
| | void shadow_arena_destroy(ShadowArena *a) { |
| | if (a->data) { |
| | free(shadow_header(a->data)); |
| | a->data = NULL; |
| | } |
| | } |
| |
|
| | void* shadow_arena_push(ShadowArena *a, const void *src, size_t bytes) { |
| | ShadowHeader *h = shadow_header(a->data); |
| |
|
| | if (h->length + bytes > h->capacity) { |
| | size_t new_cap = h->capacity ? h->capacity * 2 : 4096; |
| | while (new_cap < h->length + bytes) new_cap *= 2; |
| | size_t new_total = SHADOW_SIZE + new_cap; |
| | new_total = ALIGN_UP(new_total, 64); |
| |
|
| | ShadowHeader *new_h = realloc(h, new_total); |
| | if (!new_h) abort(); |
| |
|
| | new_h->capacity = new_cap; |
| | a->data = (char*)new_h + SHADOW_SIZE; |
| | a->reserved = new_total; |
| | h = new_h; |
| | } |
| |
|
| | char *dst = (char*)a->data + h->length; |
| | if (src) memcpy(dst, src, bytes); |
| | else memset(dst, 0, bytes); |
| |
|
| | h->length += bytes; |
| | h->flags |= 1; |
| | return dst; |
| | } |
| |
|
| | size_t shadow_arena_len(const ShadowArena *a) { |
| | return shadow_header(a->data)->length; |
| | } |
| |
|
| | void shadow_arena_clear(ShadowArena *a) { |
| | ShadowHeader *h = shadow_header(a->data); |
| | h->length = 0; |
| | h->flags &= ~1; |
| | } |
| |
|
| | |
| | |
| | |
| | typedef struct { |
| | uint32_t size; |
| | uint32_t kind; |
| | uint64_t id; |
| | } BlobHeader; |
| |
|
| | |
| | ptrdiff_t blob_append(ShadowArena *a, uint32_t kind, uint64_t id, |
| | const void *payload, size_t payload_bytes) |
| | { |
| | size_t total = sizeof(BlobHeader) + payload_bytes; |
| | char *p = shadow_arena_push(a, NULL, total); |
| |
|
| | BlobHeader bh = { |
| | .size = (uint32_t)payload_bytes, |
| | .kind = kind, |
| | .id = id |
| | }; |
| | memcpy(p, &bh, sizeof(bh)); |
| | if (payload_bytes) memcpy(p + sizeof(bh), payload, payload_bytes); |
| | return p - (char*)a->data; |
| | } |
| |
|
| | |
| | void blob_foreach(ShadowArena *a, |
| | void (*f)(const BlobHeader*, const char*, void*), |
| | void *userdata) |
| | { |
| | char *start = a->data; |
| | size_t len = shadow_header(a->data)->length; |
| | char *end = start + len; |
| | char *p = start; |
| | while (p < end) { |
| | BlobHeader *bh = (BlobHeader*)p; |
| | char *payload = p + sizeof(BlobHeader); |
| | f(bh, payload, userdata); |
| | p += sizeof(BlobHeader) + bh->size; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | bool arena_save(const ShadowArena *a, const char *filename) { |
| | FILE *f = fopen(filename, "wb"); |
| | if (!f) return false; |
| | size_t n = fwrite(shadow_header(a->data), 1, a->reserved, f); |
| | fclose(f); |
| | return n == a->reserved; |
| | } |
| |
|
| | bool arena_load(ShadowArena *a, const char *filename) { |
| | FILE *f = fopen(filename, "rb"); |
| | if (!f) return false; |
| |
|
| | fseek(f, 0, SEEK_END); |
| | long size = ftell(f); |
| | fseek(f, 0, SEEK_SET); |
| | if (size < (long)SHADOW_SIZE) { |
| | fclose(f); |
| | return false; |
| | } |
| |
|
| | void *block = malloc(size); |
| | if (!block) { fclose(f); return false; } |
| | if (fread(block, 1, size, f) != (size_t)size) { |
| | free(block); |
| | fclose(f); |
| | return false; |
| | } |
| | fclose(f); |
| |
|
| | ShadowHeader *h = (ShadowHeader*)block; |
| | if (h->tag != SHADOW_MAGIC || h->version != 1) { |
| | free(block); |
| | return false; |
| | } |
| |
|
| | shadow_arena_destroy(a); |
| | a->data = (char*)block + SHADOW_SIZE; |
| | a->reserved = size; |
| | return true; |
| | } |
| |
|
| | |
| | |
| | |
| | typedef struct { |
| | char *name; |
| | char *(*func)(const char *args); |
| | } Tool; |
| |
|
| | |
| | char* tool_shell(const char *args) { |
| | FILE *fp = popen(args, "r"); |
| | if (!fp) return strdup("error: popen failed"); |
| | char *result = NULL; |
| | size_t len = 0; |
| | FILE *out = open_memstream(&result, &len); |
| | char buf[256]; |
| | while (fgets(buf, sizeof(buf), fp)) fputs(buf, out); |
| | pclose(fp); |
| | fclose(out); |
| | return result ? result : strdup(""); |
| | } |
| |
|
| | |
| | char* tool_read_file(const char *args) { |
| | FILE *fp = fopen(args, "rb"); |
| | if (!fp) return strdup("error: cannot open file"); |
| | char *content = NULL; |
| | size_t len = 0; |
| | FILE *out = open_memstream(&content, &len); |
| | char buf[4096]; |
| | size_t n; |
| | while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) |
| | fwrite(buf, 1, n, out); |
| | fclose(fp); |
| | fclose(out); |
| | return content ? content : strdup(""); |
| | } |
| |
|
| | |
| | char* tool_write_file(const char *args) { |
| | char *filename = strdup(args); |
| | char *newline = strchr(filename, '\n'); |
| | if (!newline) { |
| | free(filename); |
| | return strdup("error: missing newline separator"); |
| | } |
| | *newline = '\0'; |
| | char *content = newline + 1; |
| | FILE *fp = fopen(filename, "wb"); |
| | if (!fp) { |
| | free(filename); |
| | return strdup("error: cannot write file"); |
| | } |
| | fwrite(content, 1, strlen(content), fp); |
| | fclose(fp); |
| | free(filename); |
| | return strdup("ok"); |
| | } |
| |
|
| | |
| | size_t write_cb(void *ptr, size_t size, size_t nmemb, void *stream) { |
| | size_t total = size * nmemb; |
| | fwrite(ptr, 1, total, (FILE*)stream); |
| | return total; |
| | } |
| | char* tool_http_get(const char *args) { |
| | CURL *curl = curl_easy_init(); |
| | if (!curl) return strdup("error: curl init failed"); |
| |
|
| | char *response = NULL; |
| | size_t len = 0; |
| | FILE *out = open_memstream(&response, &len); |
| |
|
| | curl_easy_setopt(curl, CURLOPT_URL, args); |
| | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); |
| | curl_easy_setopt(curl, CURLOPT_WRITEDATA, out); |
| | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); |
| | CURLcode res = curl_easy_perform(curl); |
| | if (res != CURLE_OK) { |
| | fclose(out); |
| | curl_easy_cleanup(curl); |
| | return strdup("error: curl failed"); |
| | } |
| | fclose(out); |
| | curl_easy_cleanup(curl); |
| | return response ? response : strdup(""); |
| | } |
| |
|
| | |
| | char* tool_math(const char *args) { |
| | char cmd[4096]; |
| | snprintf(cmd, sizeof(cmd), "echo '%s' | bc 2>/dev/null", args); |
| | FILE *fp = popen(cmd, "r"); |
| | if (!fp) return strdup("error: bc failed"); |
| | char *result = NULL; |
| | size_t len = 0; |
| | FILE *out = open_memstream(&result, &len); |
| | char buf[256]; |
| | while (fgets(buf, sizeof(buf), fp)) fputs(buf, out); |
| | pclose(fp); |
| | fclose(out); |
| | return result ? result : strdup(""); |
| | } |
| |
|
| | |
| | char* tool_list_dir(const char *args) { |
| | DIR *d = opendir(args); |
| | if (!d) return strdup("error: cannot open directory"); |
| | char *result = NULL; |
| | size_t len = 0; |
| | FILE *out = open_memstream(&result, &len); |
| | struct dirent *entry; |
| | while ((entry = readdir(d)) != NULL) { |
| | fprintf(out, "%s\n", entry->d_name); |
| | } |
| | closedir(d); |
| | fclose(out); |
| | return result ? result : strdup(""); |
| | } |
| |
|
| | |
| | Tool tools[] = { |
| | {"shell", tool_shell}, |
| | {"read_file", tool_read_file}, |
| | {"write_file", tool_write_file}, |
| | {"http_get", tool_http_get}, |
| | {"math", tool_math}, |
| | {"list_dir", tool_list_dir}, |
| | {NULL, NULL} |
| | }; |
| |
|
| | char* execute_tool(const char *name, const char *args) { |
| | for (Tool *t = tools; t->name; t++) { |
| | if (strcmp(t->name, name) == 0) { |
| | return t->func(args); |
| | } |
| | } |
| | return strdup("error: unknown tool"); |
| | } |
| |
|
| | |
| | |
| | |
| | typedef struct { |
| | char *data; |
| | size_t len; |
| | } ResponseBuffer; |
| |
|
| | size_t write_response(void *ptr, size_t size, size_t nmemb, void *stream) { |
| | ResponseBuffer *buf = (ResponseBuffer*)stream; |
| | size_t total = size * nmemb; |
| | buf->data = realloc(buf->data, buf->len + total + 1); |
| | if (!buf->data) return 0; |
| | memcpy(buf->data + buf->len, ptr, total); |
| | buf->len += total; |
| | buf->data[buf->len] = '\0'; |
| | return total; |
| | } |
| |
|
| | |
| | char* ollama_generate(const char *prompt, const char *model, const char *endpoint) { |
| | CURL *curl = curl_easy_init(); |
| | if (!curl) return NULL; |
| |
|
| | char url[256]; |
| | snprintf(url, sizeof(url), "%s/api/generate", endpoint); |
| |
|
| | cJSON *req_json = cJSON_CreateObject(); |
| | cJSON_AddStringToObject(req_json, "model", model); |
| | cJSON_AddStringToObject(req_json, "prompt", prompt); |
| | cJSON_AddBoolToObject(req_json, "stream", false); |
| | char *req_str = cJSON_PrintUnformatted(req_json); |
| | cJSON_Delete(req_json); |
| |
|
| | struct curl_slist *headers = NULL; |
| | headers = curl_slist_append(headers, "Content-Type: application/json"); |
| |
|
| | ResponseBuffer resp = {0}; |
| | curl_easy_setopt(curl, CURLOPT_URL, url); |
| | curl_easy_setopt(curl, CURLOPT_POST, 1L); |
| | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); |
| | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_str); |
| | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_response); |
| | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); |
| |
|
| | CURLcode res = curl_easy_perform(curl); |
| | free(req_str); |
| | curl_slist_free_all(headers); |
| | curl_easy_cleanup(curl); |
| |
|
| | if (res != CURLE_OK) { |
| | free(resp.data); |
| | return NULL; |
| | } |
| | return resp.data; |
| | } |
| |
|
| | |
| | |
| | |
| | typedef struct { |
| | char *text; |
| | size_t cap; |
| | size_t len; |
| | } StringBuilder; |
| |
|
| | void sb_append(StringBuilder *sb, const char *s) { |
| | size_t add = strlen(s); |
| | if (sb->len + add + 1 > sb->cap) { |
| | sb->cap = sb->cap ? sb->cap * 2 : 1024; |
| | while (sb->len + add + 1 > sb->cap) sb->cap *= 2; |
| | sb->text = realloc(sb->text, sb->cap); |
| | } |
| | memcpy(sb->text + sb->len, s, add); |
| | sb->len += add; |
| | sb->text[sb->len] = '\0'; |
| | } |
| |
|
| | void collect_blob(const BlobHeader *bh, const char *payload, void *user) { |
| | StringBuilder *sb = (StringBuilder*)user; |
| | static int count = 0; |
| | |
| | if (bh->kind == 1) { |
| | sb_append(sb, "[System]\n"); |
| | sb_append(sb, payload); |
| | sb_append(sb, "\n\n"); |
| | } else if (bh->kind == 2 || bh->kind == 3 || bh->kind == 5) { |
| | if (count < 10) { |
| | const char *role = bh->kind==2 ? "User" : (bh->kind==3 ? "Assistant" : "Tool"); |
| | sb_append(sb, "["); |
| | sb_append(sb, role); |
| | sb_append(sb, "]\n"); |
| | sb_append(sb, payload); |
| | sb_append(sb, "\n\n"); |
| | count++; |
| | } |
| | } |
| | } |
| |
|
| | char* build_prompt(ShadowArena *arena) { |
| | StringBuilder sb = {0}; |
| | |
| | blob_foreach(arena, collect_blob, &sb); |
| | |
| | sb_append(&sb, |
| | "[Instruction]\n" |
| | "You are ShadowClaw, a tiny AI agent. You can use tools by outputting a JSON block like:\n" |
| | "```tool\n{\"tool\":\"name\",\"args\":\"arguments\"}\n```\n" |
| | "Available tools: shell, read_file, write_file, http_get, math, list_dir.\n" |
| | "After using a tool, you'll see its result. Continue the conversation.\n\n" |
| | "[User]\n"); |
| | return sb.text; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | char* parse_tool_call(const char *text, char **tool_name, char **tool_args) { |
| | const char *start = strstr(text, "```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_str = malloc(len + 1); |
| | memcpy(json_str, start, len); |
| | json_str[len] = '\0'; |
| |
|
| | cJSON *root = cJSON_Parse(json_str); |
| | free(json_str); |
| | if (!root) return NULL; |
| |
|
| | cJSON *name = cJSON_GetObjectItem(root, "tool"); |
| | if (!cJSON_IsString(name)) { |
| | cJSON_Delete(root); |
| | return NULL; |
| | } |
| |
|
| | cJSON *args = cJSON_GetObjectItem(root, "args"); |
| | char *args_str = NULL; |
| | if (cJSON_IsString(args)) { |
| | args_str = strdup(args->valuestring); |
| | } else if (cJSON_IsArray(args)) { |
| | int count = cJSON_GetArraySize(args); |
| | size_t total_len = 0; |
| | for (int i = 0; i < count; i++) { |
| | cJSON *elem = cJSON_GetArrayItem(args, i); |
| | if (cJSON_IsString(elem)) { |
| | if (i > 0) total_len++; |
| | total_len += strlen(elem->valuestring); |
| | } |
| | } |
| | args_str = malloc(total_len + 1); |
| | if (args_str) { |
| | args_str[0] = '\0'; |
| | for (int i = 0; i < count; i++) { |
| | cJSON *elem = cJSON_GetArrayItem(args, i); |
| | if (cJSON_IsString(elem)) { |
| | if (i > 0) strcat(args_str, " "); |
| | strcat(args_str, elem->valuestring); |
| | } |
| | } |
| | } else { |
| | args_str = strdup(""); |
| | } |
| | } else { |
| | args_str = strdup(""); |
| | } |
| |
|
| | *tool_name = strdup(name->valuestring); |
| | *tool_args = args_str; |
| | cJSON_Delete(root); |
| | return (char*)end + 3; |
| | } |
| |
|
| | |
| | |
| | |
| | int main(int argc, char **argv) { |
| | const char *state_file = "shadowclaw.bin"; |
| | const char *ollama_endpoint = "http://localhost:11434"; |
| | const char *ollama_model = "qwen2.5:0.5b"; |
| |
|
| | ShadowArena arena = shadow_arena_create(128 * 1024); |
| |
|
| | |
| | if (access(state_file, F_OK) == 0) { |
| | if (arena_load(&arena, state_file)) { |
| | printf("[ShadowClaw] loaded state from %s\n", state_file); |
| | } else { |
| | printf("[ShadowClaw] failed to load %s, starting fresh\n", state_file); |
| | } |
| | } else { |
| | |
| | const char *sys = "You are ShadowClaw – tiny, shadowy, Unix‑punk AI agent. Use tools when helpful. Stay minimal.\n" |
| | "Available tools: shell, read_file, write_file, http_get, math, list_dir."; |
| | blob_append(&arena, 1, 1, sys, strlen(sys)+1); |
| | } |
| |
|
| | uint64_t msg_id = time(NULL); |
| |
|
| | printf("ShadowClaw ready. Type your message (Ctrl-D to exit)\n"); |
| | char line[4096]; |
| | while (fgets(line, sizeof(line), stdin)) { |
| | |
| | line[strcspn(line, "\n")] = 0; |
| | if (strlen(line) == 0) continue; |
| |
|
| | |
| | if (line[0] == '/') { |
| | if (strcmp(line, "/help") == 0) { |
| | printf("Shadowclaw commands:\n" |
| | " /help Show this help\n" |
| | " /tools List available tools\n" |
| | " /state Show arena memory stats\n" |
| | " /clear Clear conversation history (keeps system prompt)\n" |
| | " /chat Remind you that chat mode is active\n" |
| | " /exit Exit Shadowclaw\n"); |
| | } else if (strcmp(line, "/tools") == 0) { |
| | printf("Available tools:\n"); |
| | for (Tool *t = tools; t->name; t++) { |
| | printf(" %s\n", t->name); |
| | } |
| | } else if (strcmp(line, "/state") == 0) { |
| | ShadowHeader *h = shadow_header(arena.data); |
| | printf("Arena capacity: %zu bytes\n", h->capacity); |
| | printf("Arena used: %zu bytes\n", h->length); |
| | printf("Reserved total: %zu bytes\n", arena.reserved); |
| | printf("Dirty flag: %d\n", h->flags & 1); |
| | } else if (strcmp(line, "/clear") == 0) { |
| | shadow_arena_clear(&arena); |
| | |
| | const char *sys = "You are ShadowClaw – tiny, shadowy, Unix‑punk AI agent. Use tools when helpful. Stay minimal.\n" |
| | "Available tools: shell, read_file, write_file, http_get, math, list_dir."; |
| | blob_append(&arena, 1, 1, sys, strlen(sys)+1); |
| | printf("Conversation cleared.\n"); |
| | } else if (strcmp(line, "/chat") == 0) { |
| | printf("You are already in chat mode. Type your message.\n"); |
| | } else if (strcmp(line, "/exit") == 0) { |
| | break; |
| | } else { |
| | printf("Unknown command. Try /help\n"); |
| | } |
| | continue; |
| | } |
| |
|
| | |
| | |
| | blob_append(&arena, 2, msg_id++, line, strlen(line)+1); |
| |
|
| | |
| | char *prompt = build_prompt(&arena); |
| | if (!prompt) { |
| | fprintf(stderr, "error building prompt\n"); |
| | break; |
| | } |
| |
|
| | |
| | char *response_json = ollama_generate(prompt, ollama_model, ollama_endpoint); |
| | free(prompt); |
| | if (!response_json) { |
| | fprintf(stderr, "LLM call failed\n"); |
| | continue; |
| | } |
| |
|
| | |
| | cJSON *root = cJSON_Parse(response_json); |
| | if (!root) { |
| | fprintf(stderr, "JSON parse error. Raw response: %s\n", response_json); |
| | free(response_json); |
| | continue; |
| | } |
| |
|
| | |
| | cJSON *err = cJSON_GetObjectItem(root, "error"); |
| | if (err && cJSON_IsString(err)) { |
| | fprintf(stderr, "Ollama error: %s\n", err->valuestring); |
| | cJSON_Delete(root); |
| | free(response_json); |
| | continue; |
| | } |
| |
|
| | cJSON *resp_text = cJSON_GetObjectItem(root, "response"); |
| | if (!cJSON_IsString(resp_text)) { |
| | fprintf(stderr, "no 'response' field in LLM output. Full JSON: %s\n", response_json); |
| | cJSON_Delete(root); |
| | free(response_json); |
| | continue; |
| | } |
| | const char *assistant_msg = resp_text->valuestring; |
| |
|
| | |
| | char *tool_name = NULL, *tool_args = NULL; |
| | char *after_tool = parse_tool_call(assistant_msg, &tool_name, &tool_args); |
| | if (tool_name && tool_args) { |
| | |
| | char *tool_result = execute_tool(tool_name, tool_args); |
| | |
| | blob_append(&arena, 4, msg_id++, assistant_msg, after_tool - assistant_msg); |
| | blob_append(&arena, 5, msg_id++, tool_result, strlen(tool_result)+1); |
| | |
| | printf("\n[Tool %s] → %s\n", tool_name, tool_result); |
| | free(tool_result); |
| | free(tool_name); |
| | free(tool_args); |
| | } else { |
| | |
| | printf("\n[ShadowClaw] %s\n", assistant_msg); |
| | blob_append(&arena, 3, msg_id++, assistant_msg, strlen(assistant_msg)+1); |
| | } |
| |
|
| | cJSON_Delete(root); |
| | free(response_json); |
| |
|
| | |
| | if (!arena_save(&arena, state_file)) { |
| | fprintf(stderr, "warning: could not save state\n"); |
| | } |
| | } |
| |
|
| | shadow_arena_destroy(&arena); |
| | return 0; |
| | } |
| |
|