566 lines
19 KiB
C
566 lines
19 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#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;
|
|
}
|