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);