#define _POSIX_C_SOURCE 200809L #include "tools_internal.h" #include #include #include #include #include #include #include "cjson/cJSON.h" 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 int is_safe_relative_path_local(const char* path) { if (!path || path[0] == '\0') return 0; if (path[0] == '/') return 0; if (strstr(path, "..") != NULL) return 0; if (strchr(path, '\\') != NULL) return 0; return 1; } static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) { if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1; if (!is_safe_relative_path_local(rel_path)) return -1; const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0' ? ctx->cfg->tools.shell.working_directory : "."; int n = 0; if (strcmp(cwd, ".") == 0) { n = snprintf(out, out_size, "%s", rel_path); } else { n = snprintf(out, out_size, "%s/%s", cwd, rel_path); } if (n < 0 || (size_t)n >= out_size) return -1; return 0; } static cJSON* tasks_create_empty_root_local(void) { cJSON* root = cJSON_CreateObject(); cJSON* tasks = cJSON_CreateArray(); if (!root || !tasks) { cJSON_Delete(root); cJSON_Delete(tasks); return NULL; } cJSON_AddItemToObject(root, "tasks", tasks); cJSON_AddNumberToObject(root, "next_id", 1); return root; } static const char* normalize_task_status_local(const char* status) { if (!status) return NULL; if (strcmp(status, "pending") == 0) return "pending"; if (strcmp(status, "active") == 0) return "active"; if (strcmp(status, "done") == 0) return "done"; return NULL; } static cJSON* tasks_load_root_local(const char* path) { if (!path) return NULL; FILE* fp = fopen(path, "rb"); if (!fp) { if (access(path, F_OK) == 0) { return NULL; } return tasks_create_empty_root_local(); } if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return NULL; } long len = ftell(fp); if (len < 0) { fclose(fp); return NULL; } if (fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return NULL; } char* buf = (char*)malloc((size_t)len + 1U); if (!buf) { fclose(fp); return NULL; } size_t n = fread(buf, 1, (size_t)len, fp); fclose(fp); if (n != (size_t)len) { free(buf); return NULL; } buf[len] = '\0'; cJSON* root = cJSON_Parse(buf); free(buf); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return NULL; } cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks"); if (!tasks || !cJSON_IsArray(tasks)) { cJSON_DeleteItemFromObjectCaseSensitive(root, "tasks"); cJSON_AddItemToObject(root, "tasks", cJSON_CreateArray()); } cJSON* next_id = cJSON_GetObjectItemCaseSensitive(root, "next_id"); if (!next_id || !cJSON_IsNumber(next_id) || next_id->valuedouble < 1) { cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id"); cJSON_AddNumberToObject(root, "next_id", 1); } return root; } static int tasks_save_root_local(const char* path, cJSON* root) { if (!path || !root) return -1; char* raw = cJSON_PrintUnformatted(root); if (!raw) return -1; FILE* fp = fopen(path, "wb"); if (!fp) { free(raw); return -1; } size_t len = strlen(raw); size_t n = fwrite(raw, 1, len, fp); fclose(fp); free(raw); return (n == len) ? 0 : -1; } static cJSON* task_find_by_id_local(cJSON* tasks, int id, int* out_index) { if (!tasks || !cJSON_IsArray(tasks) || id <= 0) return NULL; int n = cJSON_GetArraySize(tasks); for (int i = 0; i < n; i++) { cJSON* task = cJSON_GetArrayItem(tasks, i); cJSON* tid = task ? cJSON_GetObjectItemCaseSensitive(task, "id") : NULL; if (tid && cJSON_IsNumber(tid) && (int)tid->valuedouble == id) { if (out_index) *out_index = i; return task; } } return NULL; } char* execute_task_manage(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* action = cJSON_GetObjectItemCaseSensitive(args, "action"); if (!action || !cJSON_IsString(action) || !action->valuestring || action->valuestring[0] == '\0') { cJSON_Delete(args); return json_error_local("task_manage requires string action"); } char tasks_path[PATH_MAX]; if (build_tool_path_local(ctx, "tasks.json", tasks_path, sizeof(tasks_path)) != 0) { cJSON_Delete(args); return json_error_local("failed to resolve tasks file path"); } cJSON* root = tasks_load_root_local(tasks_path); if (!root) { cJSON_Delete(args); return json_error_local("failed to load tasks file"); } cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks"); if (!tasks || !cJSON_IsArray(tasks)) { cJSON_DeleteItemFromObjectCaseSensitive(root, "tasks"); tasks = cJSON_CreateArray(); cJSON_AddItemToObject(root, "tasks", tasks); } cJSON* next_id_item = cJSON_GetObjectItemCaseSensitive(root, "next_id"); int next_id = (next_id_item && cJSON_IsNumber(next_id_item) && next_id_item->valuedouble >= 1) ? (int)next_id_item->valuedouble : 1; const char* action_s = action->valuestring; int mutated = 0; if (strcmp(action_s, "list") == 0) { } else if (strcmp(action_s, "add") == 0) { cJSON* text = cJSON_GetObjectItemCaseSensitive(args, "text"); cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status"); if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage add requires non-empty string text"); } const char* normalized_status = "pending"; if (status && !cJSON_IsNull(status)) { if (!cJSON_IsString(status) || !status->valuestring) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage add status must be string when provided"); } normalized_status = normalize_task_status_local(status->valuestring); if (!normalized_status) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage add status must be pending, active, or done"); } } cJSON* task = cJSON_CreateObject(); if (!task) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("allocation failure"); } time_t now = time(NULL); cJSON_AddNumberToObject(task, "id", next_id++); cJSON_AddStringToObject(task, "text", text->valuestring); cJSON_AddStringToObject(task, "status", normalized_status); cJSON_AddNumberToObject(task, "created_at", (double)now); cJSON_AddNumberToObject(task, "updated_at", (double)now); cJSON_AddItemToArray(tasks, task); mutated = 1; } else if (strcmp(action_s, "update") == 0) { cJSON* id = cJSON_GetObjectItemCaseSensitive(args, "id"); cJSON* text = cJSON_GetObjectItemCaseSensitive(args, "text"); cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status"); if (!id || !cJSON_IsNumber(id) || id->valuedouble < 1) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage update requires integer id"); } if ((!text || cJSON_IsNull(text)) && (!status || cJSON_IsNull(status))) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage update requires text and/or status"); } cJSON* task = task_find_by_id_local(tasks, (int)id->valuedouble, NULL); if (!task) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task not found"); } if (text && !cJSON_IsNull(text)) { if (!cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage update text must be non-empty string when provided"); } cJSON_DeleteItemFromObjectCaseSensitive(task, "text"); cJSON_AddStringToObject(task, "text", text->valuestring); mutated = 1; } if (status && !cJSON_IsNull(status)) { if (!cJSON_IsString(status) || !status->valuestring) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage update status must be string when provided"); } const char* normalized_status = normalize_task_status_local(status->valuestring); if (!normalized_status) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage update status must be pending, active, or done"); } cJSON_DeleteItemFromObjectCaseSensitive(task, "status"); cJSON_AddStringToObject(task, "status", normalized_status); mutated = 1; } if (mutated) { time_t now = time(NULL); cJSON_DeleteItemFromObjectCaseSensitive(task, "updated_at"); cJSON_AddNumberToObject(task, "updated_at", (double)now); } } else if (strcmp(action_s, "remove") == 0) { cJSON* id = cJSON_GetObjectItemCaseSensitive(args, "id"); if (!id || !cJSON_IsNumber(id) || id->valuedouble < 1) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage remove requires integer id"); } int idx = -1; cJSON* task = task_find_by_id_local(tasks, (int)id->valuedouble, &idx); if (!task || idx < 0) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task not found"); } cJSON_DeleteItemFromArray(tasks, idx); mutated = 1; } else if (strcmp(action_s, "clear") == 0) { cJSON* status = cJSON_GetObjectItemCaseSensitive(args, "status"); if (!status || cJSON_IsNull(status)) { while (cJSON_GetArraySize(tasks) > 0) { cJSON_DeleteItemFromArray(tasks, 0); } mutated = 1; } else { if (!cJSON_IsString(status) || !status->valuestring) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage clear status must be string when provided"); } const char* normalized_status = normalize_task_status_local(status->valuestring); if (!normalized_status) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage clear status must be pending, active, or done"); } int i = 0; while (i < cJSON_GetArraySize(tasks)) { cJSON* task = cJSON_GetArrayItem(tasks, i); cJSON* task_status = task ? cJSON_GetObjectItemCaseSensitive(task, "status") : NULL; const char* task_status_s = (task_status && cJSON_IsString(task_status) && task_status->valuestring) ? task_status->valuestring : "pending"; if (strcmp(task_status_s, normalized_status) == 0) { cJSON_DeleteItemFromArray(tasks, i); mutated = 1; } else { i++; } } } } else if (strcmp(action_s, "replace") == 0) { cJSON* tasks_in = cJSON_GetObjectItemCaseSensitive(args, "tasks"); if (!tasks_in || !cJSON_IsArray(tasks_in)) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage replace requires array tasks"); } while (cJSON_GetArraySize(tasks) > 0) { cJSON_DeleteItemFromArray(tasks, 0); } int n = cJSON_GetArraySize(tasks_in); time_t now = time(NULL); for (int i = 0; i < n; i++) { cJSON* text = cJSON_GetArrayItem(tasks_in, i); if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') { continue; } cJSON* task = cJSON_CreateObject(); if (!task) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("allocation failure"); } cJSON_AddNumberToObject(task, "id", next_id++); cJSON_AddStringToObject(task, "text", text->valuestring); cJSON_AddStringToObject(task, "status", "pending"); cJSON_AddNumberToObject(task, "created_at", (double)now); cJSON_AddNumberToObject(task, "updated_at", (double)now); cJSON_AddItemToArray(tasks, task); } mutated = 1; } else { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("task_manage action must be one of: list, add, update, remove, clear, replace"); } if (mutated) { cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id"); cJSON_AddNumberToObject(root, "next_id", next_id); if (tasks_save_root_local(tasks_path, root) != 0) { cJSON_Delete(args); cJSON_Delete(root); return json_error_local("failed to save tasks file"); } } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(args); cJSON_Delete(root); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "action", action_s); cJSON_AddStringToObject(out, "path", tasks_path); cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0); cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks)); cJSON* tasks_dup = cJSON_Duplicate(tasks, 1); if (!tasks_dup) { cJSON_Delete(args); cJSON_Delete(root); cJSON_Delete(out); return NULL; } cJSON_AddItemToObject(out, "tasks", tasks_dup); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(args); cJSON_Delete(root); return json; } char* execute_task_list(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); char* listed = execute_task_manage(ctx, "{\"action\":\"list\"}"); if (!listed) return json_error_local("task_list failed to list tasks"); cJSON* listed_root = cJSON_Parse(listed); if (!listed_root || !cJSON_IsObject(listed_root)) { cJSON_Delete(listed_root); free(listed); return json_error_local("task_list received invalid task_manage response"); } cJSON* tasks = cJSON_GetObjectItemCaseSensitive(listed_root, "tasks"); int tcount = (tasks && cJSON_IsArray(tasks)) ? cJSON_GetArraySize(tasks) : 0; size_t cap = 512U; char* content = (char*)malloc(cap); if (!content) { cJSON_Delete(listed_root); free(listed); return NULL; } size_t used = 0U; int n = snprintf(content + used, cap - used, "## Current Task List\n\n### Current Tasks\n\nYour active task list - short-term working memory for tracking plan steps.\n\n"); if (n < 0) { free(content); cJSON_Delete(listed_root); free(listed); return NULL; } used += (size_t)n; int written = 0; for (int i = 0; i < tcount; i++) { cJSON* task = cJSON_GetArrayItem(tasks, i); cJSON* id = task ? cJSON_GetObjectItemCaseSensitive(task, "id") : NULL; cJSON* text = task ? cJSON_GetObjectItemCaseSensitive(task, "text") : NULL; cJSON* status = task ? cJSON_GetObjectItemCaseSensitive(task, "status") : NULL; if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') { continue; } const char* status_s = (status && cJSON_IsString(status) && status->valuestring) ? status->valuestring : "pending"; const char* mark = " "; if (strcmp(status_s, "done") == 0) mark = "x"; else if (strcmp(status_s, "active") == 0) mark = "-"; int id_value = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : (i + 1); int needed = snprintf(NULL, 0, "- [%s] %d. %s\n", mark, id_value, text->valuestring); if (needed < 0) continue; if (used + (size_t)needed + 1U > cap) { size_t next = cap; while (used + (size_t)needed + 1U > next) next *= 2U; char* grown = (char*)realloc(content, next); if (!grown) { free(content); cJSON_Delete(listed_root); free(listed); return NULL; } content = grown; cap = next; } snprintf(content + used, cap - used, "- [%s] %d. %s\n", mark, id_value, text->valuestring); used += (size_t)needed; written++; } if (written == 0) { const char* fallback = "- [ ] No active tasks yet.\n"; size_t need = strlen(fallback); if (used + need + 1U > cap) { size_t next = cap; while (used + need + 1U > next) next *= 2U; char* grown = (char*)realloc(content, next); if (!grown) { free(content); cJSON_Delete(listed_root); free(listed); return NULL; } content = grown; cap = next; } memcpy(content + used, fallback, need + 1U); used += need; } cJSON* args = parse_args_local(args_json); if (!args) { free(content); cJSON_Delete(listed_root); free(listed); return json_error_local("invalid arguments JSON"); } cJSON_Delete(args); cJSON* out = cJSON_CreateObject(); if (!out) { free(content); cJSON_Delete(listed_root); free(listed); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "content", content); cJSON* tasks_dup = tasks ? cJSON_Duplicate(tasks, 1) : cJSON_CreateArray(); if (!tasks_dup) tasks_dup = cJSON_CreateArray(); cJSON_AddItemToObject(out, "tasks", tasks_dup); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); free(content); cJSON_Delete(listed_root); free(listed); return json; }