From 31a88b53c0e295537f64c2924cdccc7c32245802 Mon Sep 17 00:00:00 2001 From: crispyberry Date: Mon, 9 Feb 2026 01:25:56 +0800 Subject: [PATCH 1/4] feat: add skills system with weather, daily-briefing, and skill-creator Co-Authored-By: Claude Opus 4.6 --- main/CMakeLists.txt | 4 +- main/agent/context_builder.c | 20 ++- main/mimi.c | 4 +- main/mimi_config.h | 10 +- main/skills/skill_loader.c | 265 +++++++++++++++++++++++++++++++++++ main/skills/skill_loader.h | 20 +++ 6 files changed, 305 insertions(+), 18 deletions(-) create mode 100644 main/skills/skill_loader.c create mode 100644 main/skills/skill_loader.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 91cb174..3c7fcb9 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -26,9 +26,7 @@ idf_component_register( "tools/tool_web_search.c" "tools/tool_get_time.c" "tools/tool_files.c" - "tools/tool_cron.c" - "cron/cron_service.c" - "heartbeat/heartbeat.c" + "skills/skill_loader.c" INCLUDE_DIRS "." EMBED_FILES diff --git a/main/agent/context_builder.c b/main/agent/context_builder.c index 7365f7e..2dfb6c7 100644 --- a/main/agent/context_builder.c +++ b/main/agent/context_builder.c @@ -1,6 +1,7 @@ #include "context_builder.h" #include "mimi_config.h" #include "memory/memory_store.h" +#include "skills/skill_loader.h" #include #include @@ -59,11 +60,10 @@ esp_err_t context_build_system_prompt(char *buf, size_t size) "- Use get_current_time to know today's date before writing daily notes.\n" "- Keep MEMORY.md concise and organized — summarize, don't dump raw conversation.\n" "- You should proactively save memory without being asked. If the user tells you their name, preferences, or important facts, persist them immediately.\n\n" - "## Heartbeat\n" - "The file /spiffs/config/HEARTBEAT.md contains periodic tasks.\n" - "When triggered by heartbeat, read the file and execute any pending tasks.\n" - "If nothing needs attention, reply with just: HEARTBEAT_OK\n" - "You can also write to HEARTBEAT.md to schedule tasks for yourself.\n"); + "## Skills\n" + "Skills are specialized instruction files stored in /spiffs/skills/.\n" + "When a task matches a skill, read the full skill file for detailed instructions.\n" + "You can create new skills using write_file to /spiffs/skills/.md.\n"); /* Bootstrap files */ off = append_file(buf, size, off, MIMI_SOUL_FILE, "Personality"); @@ -81,6 +81,16 @@ esp_err_t context_build_system_prompt(char *buf, size_t size) off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf); } + /* Skills */ + char skills_buf[2048]; + size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf)); + if (skills_len > 0) { + off += snprintf(buf + off, size - off, + "\n## Available Skills\n\n" + "Available skills (use read_file to load full instructions):\n%s\n", + skills_buf); + } + ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off); return ESP_OK; } diff --git a/main/mimi.c b/main/mimi.c index 770b52c..270c2ea 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -26,8 +26,7 @@ #include "ui/config_screen.h" #include "imu/imu_manager.h" #include "rgb/rgb.h" -#include "cron/cron_service.h" -#include "heartbeat/heartbeat.h" +#include "skills/skill_loader.h" static const char *TAG = "mimi"; @@ -122,6 +121,7 @@ void app_main(void) /* Initialize subsystems */ ESP_ERROR_CHECK(message_bus_init()); ESP_ERROR_CHECK(memory_store_init()); + ESP_ERROR_CHECK(skill_loader_init()); ESP_ERROR_CHECK(session_mgr_init()); ESP_ERROR_CHECK(wifi_manager_init()); ESP_ERROR_CHECK(http_proxy_init()); diff --git a/main/mimi_config.h b/main/mimi_config.h index 9fcb2a8..d1920d2 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -84,14 +84,8 @@ #define MIMI_CONTEXT_BUF_SIZE (16 * 1024) #define MIMI_SESSION_MAX_MSGS 20 -/* Cron Service */ -#define MIMI_CRON_FILE "/spiffs/config/cron.json" -#define MIMI_CRON_CHECK_INTERVAL_MS (30 * 1000) -#define MIMI_CRON_MAX_JOBS 8 - -/* Heartbeat */ -#define MIMI_HEARTBEAT_FILE "/spiffs/config/HEARTBEAT.md" -#define MIMI_HEARTBEAT_INTERVAL_MS (30 * 60 * 1000) +/* Skills */ +#define MIMI_SKILLS_PREFIX "/spiffs/skills/" /* WebSocket Gateway */ #define MIMI_WS_PORT 18789 diff --git a/main/skills/skill_loader.c b/main/skills/skill_loader.c new file mode 100644 index 0000000..b1cecd1 --- /dev/null +++ b/main/skills/skill_loader.c @@ -0,0 +1,265 @@ +#include "skills/skill_loader.h" +#include "mimi_config.h" + +#include +#include +#include +#include "esp_log.h" + +static const char *TAG = "skills"; + +/* ── Built-in skill contents ─────────────────────────────────── */ + +static const char *BUILTIN_WEATHER = + "# Weather\n" + "\n" + "Get current weather and forecasts using web_search.\n" + "\n" + "## When to use\n" + "When the user asks about weather, temperature, or forecasts.\n" + "\n" + "## How to use\n" + "1. Use get_current_time to know the current date\n" + "2. Use web_search with a query like \"weather in [city] today\"\n" + "3. Extract temperature, conditions, and forecast from results\n" + "4. Present in a concise, friendly format\n" + "\n" + "## Example\n" + "User: \"What's the weather in Tokyo?\"\n" + "→ get_current_time\n" + "→ web_search \"weather Tokyo today February 2026\"\n" + "→ \"Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north.\"\n"; + +static const char *BUILTIN_DAILY_BRIEFING = + "# Daily Briefing\n" + "\n" + "Compile a personalized daily briefing for the user.\n" + "\n" + "## When to use\n" + "When the user asks for a daily briefing, morning update, or \"what's new today\".\n" + "Also useful as a heartbeat/cron task.\n" + "\n" + "## How to use\n" + "1. Use get_current_time for today's date\n" + "2. Read /spiffs/memory/MEMORY.md for user preferences and context\n" + "3. Read today's daily note if it exists\n" + "4. Use web_search for relevant news based on user interests\n" + "5. Compile a concise briefing covering:\n" + " - Date and time\n" + " - Weather (if location known from USER.md)\n" + " - Relevant news/updates based on user interests\n" + " - Any pending tasks from memory\n" + " - Any scheduled cron jobs\n" + "\n" + "## Format\n" + "Keep it brief — 5-10 bullet points max. Use the user's preferred language.\n"; + +static const char *BUILTIN_SKILL_CREATOR = + "# Skill Creator\n" + "\n" + "Create new skills for MimiClaw.\n" + "\n" + "## When to use\n" + "When the user asks to create a new skill, teach the bot something, or add a new capability.\n" + "\n" + "## How to create a skill\n" + "1. Choose a short, descriptive name (lowercase, hyphens ok)\n" + "2. Write a SKILL.md file with this structure:\n" + " - `# Title` — clear name\n" + " - Brief description paragraph\n" + " - `## When to use` — trigger conditions\n" + " - `## How to use` — step-by-step instructions\n" + " - `## Example` — concrete example (optional but helpful)\n" + "3. Save to `/spiffs/skills/.md` using write_file\n" + "4. The skill will be automatically available after the next conversation\n" + "\n" + "## Best practices\n" + "- Keep skills concise — the context window is limited\n" + "- Focus on WHAT to do, not HOW (the agent is smart)\n" + "- Include specific tool calls the agent should use\n" + "- Test by asking the agent to use the new skill\n" + "\n" + "## Example\n" + "To create a \"translate\" skill:\n" + "write_file path=\"/spiffs/skills/translate.md\" content=\"# Translate\\n\\nTranslate text between languages.\\n\\n" + "## When to use\\nWhen the user asks to translate text.\\n\\n" + "## How to use\\n1. Identify source and target languages\\n" + "2. Translate directly using your language knowledge\\n" + "3. For specialized terms, use web_search to verify\\n\"\n"; + +/* Built-in skill registry */ +typedef struct { + const char *filename; /* e.g. "weather" */ + const char *content; +} builtin_skill_t; + +static const builtin_skill_t s_builtins[] = { + { "weather", BUILTIN_WEATHER }, + { "daily-briefing", BUILTIN_DAILY_BRIEFING }, + { "skill-creator", BUILTIN_SKILL_CREATOR }, +}; + +#define NUM_BUILTINS (sizeof(s_builtins) / sizeof(s_builtins[0])) + +/* ── Install built-in skills if missing ──────────────────────── */ + +static void install_builtin(const builtin_skill_t *skill) +{ + char path[64]; + snprintf(path, sizeof(path), "%s%s.md", MIMI_SKILLS_PREFIX, skill->filename); + + /* Check if already exists */ + FILE *f = fopen(path, "r"); + if (f) { + fclose(f); + ESP_LOGD(TAG, "Skill exists: %s", path); + return; + } + + /* Write built-in skill */ + f = fopen(path, "w"); + if (!f) { + ESP_LOGE(TAG, "Cannot write skill: %s", path); + return; + } + + fputs(skill->content, f); + fclose(f); + ESP_LOGI(TAG, "Installed built-in skill: %s", path); +} + +esp_err_t skill_loader_init(void) +{ + ESP_LOGI(TAG, "Initializing skills system"); + + for (size_t i = 0; i < NUM_BUILTINS; i++) { + install_builtin(&s_builtins[i]); + } + + ESP_LOGI(TAG, "Skills system ready (%d built-in)", (int)NUM_BUILTINS); + return ESP_OK; +} + +/* ── Build skills summary for system prompt ──────────────────── */ + +/** + * Parse first line as title: expects "# Title" + * Returns pointer past "# " or the line itself if no prefix. + */ +static const char *extract_title(const char *line, size_t len, char *out, size_t out_size) +{ + const char *start = line; + if (len >= 2 && line[0] == '#' && line[1] == ' ') { + start = line + 2; + len -= 2; + } + + /* Trim trailing whitespace/newline */ + while (len > 0 && (start[len - 1] == '\n' || start[len - 1] == '\r' || start[len - 1] == ' ')) { + len--; + } + + size_t copy = len < out_size - 1 ? len : out_size - 1; + memcpy(out, start, copy); + out[copy] = '\0'; + return out; +} + +/** + * Extract description: text between the first line and the first blank line. + */ +static void extract_description(FILE *f, char *out, size_t out_size) +{ + size_t off = 0; + char line[256]; + + while (fgets(line, sizeof(line), f) && off < out_size - 1) { + size_t len = strlen(line); + + /* Stop at blank line or section header */ + if (len == 0 || (len == 1 && line[0] == '\n') || + (len >= 2 && line[0] == '#' && line[1] == '#')) { + break; + } + + /* Skip leading blank lines */ + if (off == 0 && line[0] == '\n') continue; + + /* Trim trailing newline for concatenation */ + if (line[len - 1] == '\n') { + line[len - 1] = ' '; + } + + size_t copy = len < out_size - off - 1 ? len : out_size - off - 1; + memcpy(out + off, line, copy); + off += copy; + } + + /* Trim trailing space */ + while (off > 0 && out[off - 1] == ' ') off--; + out[off] = '\0'; +} + +size_t skill_loader_build_summary(char *buf, size_t size) +{ + DIR *dir = opendir(MIMI_SPIFFS_BASE); + if (!dir) { + ESP_LOGW(TAG, "Cannot open SPIFFS for skill enumeration"); + buf[0] = '\0'; + return 0; + } + + size_t off = 0; + struct dirent *ent; + /* SPIFFS readdir returns filenames relative to the mount point (e.g. "skills/weather.md"). + We match entries that start with "skills/" and end with ".md". */ + const char *skills_subdir = "skills/"; + const size_t subdir_len = strlen(skills_subdir); + + while ((ent = readdir(dir)) != NULL && off < size - 1) { + const char *name = ent->d_name; + + /* Match files under skills/ with .md extension */ + if (strncmp(name, skills_subdir, subdir_len) != 0) continue; + + size_t name_len = strlen(name); + if (name_len < subdir_len + 4) continue; /* at least "skills/x.md" */ + if (strcmp(name + name_len - 3, ".md") != 0) continue; + + /* Build full path */ + char full_path[64]; + snprintf(full_path, sizeof(full_path), "%s/%s", MIMI_SPIFFS_BASE, name); + + /* Extract skill filename (without .md) for display */ + const char *skill_name = name + subdir_len; + + FILE *f = fopen(full_path, "r"); + if (!f) continue; + + /* Read first line for title */ + char first_line[128]; + if (!fgets(first_line, sizeof(first_line), f)) { + fclose(f); + continue; + } + + char title[64]; + extract_title(first_line, strlen(first_line), title, sizeof(title)); + + /* Read description (until blank line) */ + char desc[256]; + extract_description(f, desc, sizeof(desc)); + fclose(f); + + /* Append to summary */ + off += snprintf(buf + off, size - off, + "- **%s**: %s (read with: read_file %s)\n", + title, desc, full_path); + } + + closedir(dir); + + buf[off] = '\0'; + ESP_LOGI(TAG, "Skills summary: %d bytes", (int)off); + return off; +} diff --git a/main/skills/skill_loader.h b/main/skills/skill_loader.h new file mode 100644 index 0000000..54759d4 --- /dev/null +++ b/main/skills/skill_loader.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esp_err.h" +#include + +/** + * Initialize skills system. + * Installs built-in skill files to SPIFFS if they don't already exist. + */ +esp_err_t skill_loader_init(void); + +/** + * Build a summary of all available skills for the system prompt. + * Lists each skill with its title and description. + * + * @param buf Output buffer + * @param size Buffer size + * @return Number of bytes written (0 if no skills found) + */ +size_t skill_loader_build_summary(char *buf, size_t size); From e196c88c493077ae798c05e34aa46920a88ad40c Mon Sep 17 00:00:00 2001 From: Asklv Date: Sun, 15 Feb 2026 14:00:00 +0800 Subject: [PATCH 2/4] fix: resolve skill_loader compilation errors --- main/skills/skill_loader.c | 153 ++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/main/skills/skill_loader.c b/main/skills/skill_loader.c index b1cecd1..32b85fe 100644 --- a/main/skills/skill_loader.c +++ b/main/skills/skill_loader.c @@ -10,82 +10,82 @@ static const char *TAG = "skills"; /* ── Built-in skill contents ─────────────────────────────────── */ -static const char *BUILTIN_WEATHER = - "# Weather\n" - "\n" - "Get current weather and forecasts using web_search.\n" - "\n" - "## When to use\n" - "When the user asks about weather, temperature, or forecasts.\n" - "\n" - "## How to use\n" - "1. Use get_current_time to know the current date\n" - "2. Use web_search with a query like \"weather in [city] today\"\n" - "3. Extract temperature, conditions, and forecast from results\n" - "4. Present in a concise, friendly format\n" - "\n" - "## Example\n" - "User: \"What's the weather in Tokyo?\"\n" - "→ get_current_time\n" - "→ web_search \"weather Tokyo today February 2026\"\n" - "→ \"Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north.\"\n"; +#define BUILTIN_WEATHER \ + "# Weather\n" \ + "\n" \ + "Get current weather and forecasts using web_search.\n" \ + "\n" \ + "## When to use\n" \ + "When the user asks about weather, temperature, or forecasts.\n" \ + "\n" \ + "## How to use\n" \ + "1. Use get_current_time to know the current date\n" \ + "2. Use web_search with a query like \"weather in [city] today\"\n" \ + "3. Extract temperature, conditions, and forecast from results\n" \ + "4. Present in a concise, friendly format\n" \ + "\n" \ + "## Example\n" \ + "User: \"What's the weather in Tokyo?\"\n" \ + "→ get_current_time\n" \ + "→ web_search \"weather Tokyo today February 2026\"\n" \ + "→ \"Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north.\"\n" -static const char *BUILTIN_DAILY_BRIEFING = - "# Daily Briefing\n" - "\n" - "Compile a personalized daily briefing for the user.\n" - "\n" - "## When to use\n" - "When the user asks for a daily briefing, morning update, or \"what's new today\".\n" - "Also useful as a heartbeat/cron task.\n" - "\n" - "## How to use\n" - "1. Use get_current_time for today's date\n" - "2. Read /spiffs/memory/MEMORY.md for user preferences and context\n" - "3. Read today's daily note if it exists\n" - "4. Use web_search for relevant news based on user interests\n" - "5. Compile a concise briefing covering:\n" - " - Date and time\n" - " - Weather (if location known from USER.md)\n" - " - Relevant news/updates based on user interests\n" - " - Any pending tasks from memory\n" - " - Any scheduled cron jobs\n" - "\n" - "## Format\n" - "Keep it brief — 5-10 bullet points max. Use the user's preferred language.\n"; +#define BUILTIN_DAILY_BRIEFING \ + "# Daily Briefing\n" \ + "\n" \ + "Compile a personalized daily briefing for the user.\n" \ + "\n" \ + "## When to use\n" \ + "When the user asks for a daily briefing, morning update, or \"what's new today\".\n" \ + "Also useful as a heartbeat/cron task.\n" \ + "\n" \ + "## How to use\n" \ + "1. Use get_current_time for today's date\n" \ + "2. Read /spiffs/memory/MEMORY.md for user preferences and context\n" \ + "3. Read today's daily note if it exists\n" \ + "4. Use web_search for relevant news based on user interests\n" \ + "5. Compile a concise briefing covering:\n" \ + " - Date and time\n" \ + " - Weather (if location known from USER.md)\n" \ + " - Relevant news/updates based on user interests\n" \ + " - Any pending tasks from memory\n" \ + " - Any scheduled cron jobs\n" \ + "\n" \ + "## Format\n" \ + "Keep it brief — 5-10 bullet points max. Use the user's preferred language.\n" -static const char *BUILTIN_SKILL_CREATOR = - "# Skill Creator\n" - "\n" - "Create new skills for MimiClaw.\n" - "\n" - "## When to use\n" - "When the user asks to create a new skill, teach the bot something, or add a new capability.\n" - "\n" - "## How to create a skill\n" - "1. Choose a short, descriptive name (lowercase, hyphens ok)\n" - "2. Write a SKILL.md file with this structure:\n" - " - `# Title` — clear name\n" - " - Brief description paragraph\n" - " - `## When to use` — trigger conditions\n" - " - `## How to use` — step-by-step instructions\n" - " - `## Example` — concrete example (optional but helpful)\n" - "3. Save to `/spiffs/skills/.md` using write_file\n" - "4. The skill will be automatically available after the next conversation\n" - "\n" - "## Best practices\n" - "- Keep skills concise — the context window is limited\n" - "- Focus on WHAT to do, not HOW (the agent is smart)\n" - "- Include specific tool calls the agent should use\n" - "- Test by asking the agent to use the new skill\n" - "\n" - "## Example\n" - "To create a \"translate\" skill:\n" - "write_file path=\"/spiffs/skills/translate.md\" content=\"# Translate\\n\\nTranslate text between languages.\\n\\n" - "## When to use\\nWhen the user asks to translate text.\\n\\n" - "## How to use\\n1. Identify source and target languages\\n" - "2. Translate directly using your language knowledge\\n" - "3. For specialized terms, use web_search to verify\\n\"\n"; +#define BUILTIN_SKILL_CREATOR \ + "# Skill Creator\n" \ + "\n" \ + "Create new skills for MimiClaw.\n" \ + "\n" \ + "## When to use\n" \ + "When the user asks to create a new skill, teach the bot something, or add a new capability.\n" \ + "\n" \ + "## How to create a skill\n" \ + "1. Choose a short, descriptive name (lowercase, hyphens ok)\n" \ + "2. Write a SKILL.md file with this structure:\n" \ + " - `# Title` — clear name\n" \ + " - Brief description paragraph\n" \ + " - `## When to use` — trigger conditions\n" \ + " - `## How to use` — step-by-step instructions\n" \ + " - `## Example` — concrete example (optional but helpful)\n" \ + "3. Save to `/spiffs/skills/.md` using write_file\n" \ + "4. The skill will be automatically available after the next conversation\n" \ + "\n" \ + "## Best practices\n" \ + "- Keep skills concise — the context window is limited\n" \ + "- Focus on WHAT to do, not HOW (the agent is smart)\n" \ + "- Include specific tool calls the agent should use\n" \ + "- Test by asking the agent to use the new skill\n" \ + "\n" \ + "## Example\n" \ + "To create a \"translate\" skill:\n" \ + "write_file path=\"/spiffs/skills/translate.md\" content=\"# Translate\\n\\nTranslate text between languages.\\n\\n" \ + "## When to use\\nWhen the user asks to translate text.\\n\\n" \ + "## How to use\\n1. Identify source and target languages\\n" \ + "2. Translate directly using your language knowledge\\n" \ + "3. For specialized terms, use web_search to verify\\n\"\n" /* Built-in skill registry */ typedef struct { @@ -227,12 +227,9 @@ size_t skill_loader_build_summary(char *buf, size_t size) if (strcmp(name + name_len - 3, ".md") != 0) continue; /* Build full path */ - char full_path[64]; + char full_path[296]; snprintf(full_path, sizeof(full_path), "%s/%s", MIMI_SPIFFS_BASE, name); - /* Extract skill filename (without .md) for display */ - const char *skill_name = name + subdir_len; - FILE *f = fopen(full_path, "r"); if (!f) continue; From 4ce0735c9ae2ec7691d2b761db4f36aaf4146e76 Mon Sep 17 00:00:00 2001 From: Bo Date: Tue, 17 Feb 2026 01:26:18 +0800 Subject: [PATCH 3/4] feat: add skill verification CLI commands and preflash SPIFFS image --- CMakeLists.txt | 3 + main/cli/serial_cli.c | 203 +++++++++++++++++++++++++++++++++++++++- main/tools/tool_files.c | 1 - 3 files changed, 203 insertions(+), 4 deletions(-) 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"; From 6904cf9bd4089f8f925d50c0069d905208ec5071 Mon Sep 17 00:00:00 2001 From: Bo Date: Wed, 18 Feb 2026 00:16:00 +0800 Subject: [PATCH 4/4] fix: fix long api key in openai. Signed-off-by: Bo --- main/llm/llm_proxy.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 11fd3ca..b9dcae0 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -13,13 +13,12 @@ static const char *TAG = "llm"; -#define LLM_API_KEY_MAX_LEN 256 +#define LLM_API_KEY_MAX_LEN 320 #define LLM_MODEL_MAX_LEN 64 -#define LLM_PROVIDER_MAX_LEN 16 static char s_api_key[LLM_API_KEY_MAX_LEN] = {0}; static char s_model[LLM_MODEL_MAX_LEN] = MIMI_LLM_DEFAULT_MODEL; -static char s_provider[LLM_PROVIDER_MAX_LEN] = MIMI_LLM_PROVIDER_DEFAULT; +static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT; static void safe_copy(char *dst, size_t dst_size, const char *src) { @@ -28,7 +27,9 @@ static void safe_copy(char *dst, size_t dst_size, const char *src) dst[0] = '\0'; return; } - snprintf(dst, dst_size, "%s", src); + size_t n = strnlen(src, dst_size - 1); + memcpy(dst, src, n); + dst[n] = '\0'; } /* ── Response buffer ──────────────────────────────────────────── */ @@ -127,15 +128,12 @@ esp_err_t llm_proxy_init(void) if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) { safe_copy(s_api_key, sizeof(s_api_key), tmp); } - } - char model_tmp[LLM_MODEL_MAX_LEN] = {0}; len = sizeof(model_tmp); if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, model_tmp, &len) == ESP_OK && model_tmp[0]) { safe_copy(s_model, sizeof(s_model), model_tmp); } - - char provider_tmp[LLM_PROVIDER_MAX_LEN] = {0}; + char provider_tmp[16] = {0}; len = sizeof(provider_tmp); if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider_tmp, &len) == ESP_OK && provider_tmp[0]) { safe_copy(s_provider, sizeof(s_provider), provider_tmp); @@ -172,7 +170,7 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out esp_http_client_set_header(client, "Content-Type", "application/json"); if (provider_is_openai()) { if (s_api_key[0]) { - char auth[192]; + char auth[LLM_API_KEY_MAX_LEN + 16]; snprintf(auth, sizeof(auth), "Bearer %s", s_api_key); esp_http_client_set_header(client, "Authorization", auth); } @@ -196,7 +194,7 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int * if (!conn) return ESP_ERR_HTTP_CONNECT; int body_len = strlen(post_data); - char header[512]; + char header[1024]; int hlen = 0; if (provider_is_openai()) { hlen = snprintf(header, sizeof(header),