diff --git a/CMakeLists.txt b/CMakeLists.txt index b6ab68a..fcfc15b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,3 +4,6 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(mimiclaw) + +# Pre-flash a valid SPIFFS image so first boot does not need runtime formatting. +spiffs_create_partition_image(spiffs spiffs_data FLASH_IN_PROJECT) diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 88e0eab..225e87e 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -7,12 +7,12 @@ #include "memory/session_mgr.h" #include "proxy/http_proxy.h" #include "tools/tool_web_search.h" -#include "tools/tool_registry.h" -#include "cron/cron_service.h" -#include "heartbeat/heartbeat.h" +#include "skills/skill_loader.h" #include #include +#include +#include #include "esp_log.h" #include "esp_console.h" #include "esp_system.h" @@ -253,6 +253,173 @@ static int cmd_wifi_scan(int argc, char **argv) return 0; } +/* --- skill_list command --- */ +static int cmd_skill_list(int argc, char **argv) +{ + (void)argc; + (void)argv; + + char *buf = malloc(4096); + if (!buf) { + printf("Out of memory.\n"); + return 1; + } + + size_t n = skill_loader_build_summary(buf, 4096); + if (n == 0) { + printf("No skills found under /spiffs/skills/.\n"); + } else { + printf("=== Skills ===\n%s", buf); + } + free(buf); + return 0; +} + +/* --- skill_show command --- */ +static struct { + struct arg_str *name; + struct arg_end *end; +} skill_show_args; + +static bool has_md_suffix(const char *name) +{ + size_t len = strlen(name); + return (len >= 3) && strcmp(name + len - 3, ".md") == 0; +} + +static bool build_skill_path(const char *name, char *out, size_t out_size) +{ + if (!name || !name[0]) return false; + if (strstr(name, "..") != NULL) return false; + if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) return false; + + if (has_md_suffix(name)) { + snprintf(out, out_size, "/spiffs/skills/%s", name); + } else { + snprintf(out, out_size, "/spiffs/skills/%s.md", name); + } + return true; +} + +static int cmd_skill_show(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&skill_show_args); + if (nerrors != 0) { + arg_print_errors(stderr, skill_show_args.end, argv[0]); + return 1; + } + + char path[128]; + if (!build_skill_path(skill_show_args.name->sval[0], path, sizeof(path))) { + printf("Invalid skill name.\n"); + return 1; + } + + FILE *f = fopen(path, "r"); + if (!f) { + printf("Skill not found: %s\n", path); + return 1; + } + + printf("=== %s ===\n", path); + char line[256]; + while (fgets(line, sizeof(line), f)) { + fputs(line, stdout); + } + fclose(f); + printf("\n============\n"); + return 0; +} + +/* --- skill_search command --- */ +static struct { + struct arg_str *keyword; + struct arg_end *end; +} skill_search_args; + +static bool contains_nocase(const char *text, const char *keyword) +{ + if (!text || !keyword || !keyword[0]) return false; + + size_t key_len = strlen(keyword); + for (const char *p = text; *p; p++) { + size_t i = 0; + while (i < key_len && p[i] && + tolower((unsigned char)p[i]) == tolower((unsigned char)keyword[i])) { + i++; + } + if (i == key_len) return true; + } + return false; +} + +static int cmd_skill_search(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&skill_search_args); + if (nerrors != 0) { + arg_print_errors(stderr, skill_search_args.end, argv[0]); + return 1; + } + + const char *keyword = skill_search_args.keyword->sval[0]; + DIR *dir = opendir("/spiffs"); + if (!dir) { + printf("Cannot open /spiffs.\n"); + return 1; + } + + const char *prefix = "skills/"; + const size_t prefix_len = strlen(prefix); + int matches = 0; + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + const char *name = ent->d_name; + size_t name_len = strlen(name); + + if (strncmp(name, prefix, prefix_len) != 0) continue; + if (name_len < prefix_len + 4) continue; + if (strcmp(name + name_len - 3, ".md") != 0) continue; + + char full_path[296]; + snprintf(full_path, sizeof(full_path), "/spiffs/%s", name); + + bool file_matched = contains_nocase(name, keyword); + int matched_line = 0; + + FILE *f = fopen(full_path, "r"); + if (!f) continue; + + char line[256]; + int line_no = 0; + while (!file_matched && fgets(line, sizeof(line), f)) { + line_no++; + if (contains_nocase(line, keyword)) { + file_matched = true; + matched_line = line_no; + } + } + fclose(f); + + if (file_matched) { + matches++; + if (matched_line > 0) { + printf("- %s (matched at line %d)\n", full_path, matched_line); + } else { + printf("- %s (matched in filename)\n", full_path); + } + } + } + + closedir(dir); + if (matches == 0) { + printf("No skills matched keyword: %s\n", keyword); + } else { + printf("Total matches: %d\n", matches); + } + return 0; +} + /* --- config_show command --- */ static void print_config(const char *label, const char *ns, const char *key, const char *build_val, bool mask) @@ -463,6 +630,36 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&provider_cmd); + /* skill_list */ + esp_console_cmd_t skill_list_cmd = { + .command = "skill_list", + .help = "List installed skills from /spiffs/skills/", + .func = &cmd_skill_list, + }; + esp_console_cmd_register(&skill_list_cmd); + + /* skill_show */ + skill_show_args.name = arg_str1(NULL, NULL, "", "Skill name (e.g. weather or weather.md)"); + skill_show_args.end = arg_end(1); + esp_console_cmd_t skill_show_cmd = { + .command = "skill_show", + .help = "Print full content of one skill file", + .func = &cmd_skill_show, + .argtable = &skill_show_args, + }; + esp_console_cmd_register(&skill_show_cmd); + + /* skill_search */ + skill_search_args.keyword = arg_str1(NULL, NULL, "", "Keyword to search in skills"); + skill_search_args.end = arg_end(1); + esp_console_cmd_t skill_search_cmd = { + .command = "skill_search", + .help = "Search skill files by keyword (filename + content)", + .func = &cmd_skill_search, + .argtable = &skill_search_args, + }; + esp_console_cmd_register(&skill_search_cmd); + /* memory_read */ esp_console_cmd_t mem_read_cmd = { .command = "memory_read", diff --git a/main/tools/tool_files.c b/main/tools/tool_files.c index 358f060..4d70d50 100644 --- a/main/tools/tool_files.c +++ b/main/tools/tool_files.c @@ -9,7 +9,6 @@ #include #include "esp_log.h" #include "cJSON.h" -#include static const char *TAG = "tool_files";