From 6c24ba8d96c4e429dd962b864f8eedd7568e733a Mon Sep 17 00:00:00 2001 From: Didactyl User Date: Fri, 7 Aug 2026 08:03:35 -0400 Subject: [PATCH] v0.2.61 - Skill template uses agent name + admin npub; add skill picker step with toggle menu and custom skill creation --- README.md | 4 +- src/default_events.h | 35 ++++--- src/main.h | 4 +- src/setup_wizard.c | 230 ++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 239 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 321ac06..74a449f 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.2.60 +## Current Status — v0.2.61 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.60 — Add FIPS relay to default relays after laantungir +> Last release update: v0.2.61 — Skill template uses agent name + admin npub; add skill picker step with toggle menu and custom skill creation - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/default_events.h b/src/default_events.h index c979588..b0e8365 100644 --- a/src/default_events.h +++ b/src/default_events.h @@ -41,21 +41,26 @@ static const char* DIDACTYL_DEFAULT_RELAYS[] = { #define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0]))) -static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE = - "# Didactyl Agent\n\n" - "You are {{my_kind0_profile}}\n\n" - "Your npub: {{my_npub}}\n\n" - "## Rules\n\n" - "- Communicate through encrypted Nostr direct messages\n" - "- Keep responses concise and clear\n" - "- Be helpful and technically accurate\n" - "- If unsure, state uncertainty directly\n" - "- Use tools when a request requires taking action\n" - "- After a tool call, base your answer on the actual tool result\n" - "- Never claim a tool was run if no tool was executed\n" - "- Maintain your task list as short-term working memory\n" - "- Never reveal your private key (nsec)\n" - "- You may share your public key (npub) with anyone"; +/* Template for the identity_and_rules skill. Use %s placeholders: + * %s = agent name + * %s = admin npub (bech32) + * The content is built at wizard time by configure_default_skills_for_agent(). */ +#define DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT \ + "# %s\n\n" \ + "You are {{my_kind0_profile}}\n\n" \ + "Your npub: {{my_npub}}\n\n" \ + "Admin npub: %s\n\n" \ + "## Rules\n\n" \ + "- Communicate through encrypted Nostr direct messages\n" \ + "- Keep responses concise and clear\n" \ + "- Be helpful and technically accurate\n" \ + "- If unsure, state uncertainty directly\n" \ + "- Use tools when a request requires taking action\n" \ + "- After a tool call, base your answer on the actual tool result\n" \ + "- Never claim a tool was run if no tool was executed\n" \ + "- Maintain your task list as short-term working memory\n" \ + "- Never reveal your private key (nsec)\n" \ + "- You may share your public key (npub) with anyone" static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE = "## Recent Conversation\n\n" diff --git a/src/main.h b/src/main.h index 28dbfbd..24cf874 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 2 -#define DIDACTYL_VERSION_PATCH 60 -#define DIDACTYL_VERSION "v0.2.60" +#define DIDACTYL_VERSION_PATCH 61 +#define DIDACTYL_VERSION "v0.2.61" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 6068087..994fca1 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -588,26 +588,226 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen return 0; } +/* Build the identity_and_rules skill content from the template format string, + * substituting the agent name and admin npub. */ +static char* build_identity_skill_content(const didactyl_config_t* cfg, const char* agent_name) { + if (!cfg || !agent_name) return NULL; + char admin_npub[OW_MAX_KEY_LEN] = {0}; + unsigned char admin_pubkey[32] = {0}; + if (nostr_hex_to_bytes(cfg->admin.pubkey, admin_pubkey, 32) == 0) { + (void)nostr_key_to_bech32(admin_pubkey, "npub", admin_npub); + } + if (admin_npub[0] == '\0') { + snprintf(admin_npub, sizeof(admin_npub), "%s", cfg->admin.pubkey); + } + size_t needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT, + agent_name, admin_npub) + 1; + char* content = (char*)malloc(needed); + if (!content) return NULL; + snprintf(content, needed, DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT, + agent_name, admin_npub); + return content; +} + +/* Default skill descriptors used by the skill picker. */ +#define SKILL_PICKER_MAX 32 + +typedef struct { + const char* d_tag; + const char* description; + const char* tags_json; + const char* content_template; + int kind; + int enabled_by_default; +} skill_descriptor_t; + +static const skill_descriptor_t DIDACTYL_DEFAULT_SKILLS[] = { + { + .d_tag = "identity_and_rules", + .description = "Agent identity, admin npub, and behavioral rules", + .tags_json = DIDACTYL_DEFAULT_SKILL_TAGS_JSON, + .content_template = NULL, + .kind = DIDACTYL_DEFAULT_SKILL_KIND, + .enabled_by_default = 1 + }, + { + .d_tag = "dm_history", + .description = "DM conversation history for context continuity", + .tags_json = DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON, + .content_template = NULL, /* dm_history template used directly */ + .kind = DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND, + .enabled_by_default = 1 + } +}; + +#define DIDACTYL_DEFAULT_SKILL_COUNT \ + ((int)(sizeof(DIDACTYL_DEFAULT_SKILLS) / sizeof(DIDACTYL_DEFAULT_SKILLS[0]))) + +/* Prompt the operator to pick which skills to include, and optionally create + * custom skills. Shows a toggle-based menu similar to the relay picker. */ +static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_name) { + if (!cfg || !agent_name) return -1; + + int skill_enabled[SKILL_PICKER_MAX]; + char* skill_d_tags[SKILL_PICKER_MAX]; + char* skill_contents[SKILL_PICKER_MAX]; + char* skill_tags[SKILL_PICKER_MAX]; + int skill_kinds[SKILL_PICKER_MAX]; + int skill_count = 0; + + for (int i = 0; i < DIDACTYL_DEFAULT_SKILL_COUNT && skill_count < SKILL_PICKER_MAX; i++) { + skill_enabled[skill_count] = DIDACTYL_DEFAULT_SKILLS[i].enabled_by_default; + skill_d_tags[skill_count] = strdup(DIDACTYL_DEFAULT_SKILLS[i].d_tag); + if (strcmp(DIDACTYL_DEFAULT_SKILLS[i].d_tag, "dm_history") == 0) { + skill_contents[skill_count] = strdup(DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE); + } else { + skill_contents[skill_count] = build_identity_skill_content(cfg, agent_name); + } + skill_tags[skill_count] = strdup(DIDACTYL_DEFAULT_SKILLS[i].tags_json); + skill_kinds[skill_count] = DIDACTYL_DEFAULT_SKILLS[i].kind; + skill_count++; + } + + char input[WIZARD_LINE_MAX]; + for (;;) { + render_wizard_page_header("Step 7 of 8", "New Agent Setup -- Skills"); + fprintf(stderr, " Skills (enter a number to toggle, [X] = enabled):\n"); + for (int i = 0; i < skill_count; i++) { + fprintf(stderr, " %d. [%c] %s\n", i + 1, + skill_enabled[i] ? 'X' : ' ', + skill_d_tags[i] ? skill_d_tags[i] : ""); + } + fprintf(stderr, "\n"); + fprintf(stderr, " Commands: a=add custom skill, d=done, b=back, q=quit\n"); + if (read_line_prompt("> ", input, sizeof(input)) != 0) { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return -1; + } + if (line_is_quit(input)) { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return -1; + } + + char* trimmed = input; + while (*trimmed == ' ') trimmed++; + if (trimmed[0] == '\0') continue; + + int is_number = 1; + for (size_t i = 0; trimmed[i] != '\0'; i++) { + if (!isdigit((unsigned char)trimmed[i])) { is_number = 0; break; } + } + if (is_number) { + int idx = atoi(trimmed) - 1; + if (idx >= 0 && idx < skill_count) { + skill_enabled[idx] = !skill_enabled[idx]; + } else { + fprintf(stderr, "%sInvalid skill number (1-%d).%s\n", ANSI_RED, skill_count, ANSI_RESET); + } + continue; + } + + char c = (char)tolower((unsigned char)trimmed[0]); + + if (c == 'b') { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return 1; + } + if (c == 'd') { + for (int i = 0; i < cfg->startup_event_count; i++) { + free(cfg->startup_events[i].content); + free(cfg->startup_events[i].tags_json); + } + free(cfg->startup_events); + cfg->startup_events = NULL; + cfg->startup_event_count = 0; + + for (int i = 0; i < skill_count; i++) { + if (skill_enabled[i]) { + if (upsert_startup_event(cfg, skill_kinds[i], + skill_contents[i], + skill_tags[i]) != 0) { + fprintf(stderr, "%sFailed to add skill '%s'.%s\n", + ANSI_RED, skill_d_tags[i] ? skill_d_tags[i] : "", ANSI_RESET); + } + } + } + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return 0; + } + if (c == 'a') { + if (skill_count >= SKILL_PICKER_MAX) { + fprintf(stderr, "%sMaximum skill count reached.%s\n", ANSI_RED, ANSI_RESET); + continue; + } + char d_tag[WIZARD_LINE_MAX] = {0}; + if (read_line_prompt(" Skill d_tag (identifier): ", d_tag, sizeof(d_tag)) != 0) { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return -1; + } + if (line_is_quit(d_tag)) { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return -1; + } + if (d_tag[0] == '\0') { + fprintf(stderr, "%sd_tag cannot be empty.%s\n", ANSI_RED, ANSI_RESET); + continue; + } + int dup = 0; + for (int i = 0; i < skill_count; i++) { + if (skill_d_tags[i] && strcmp(skill_d_tags[i], d_tag) == 0) { dup = 1; break; } + } + if (dup) { + fprintf(stderr, "%sA skill with d_tag '%s' already exists.%s\n", ANSI_RED, d_tag, ANSI_RESET); + continue; + } + + char content[4096] = {0}; + fprintf(stderr, " Enter skill content (markdown, end with '.' on its own line):\n"); + size_t content_len = 0; + for (;;) { + char line[WIZARD_LINE_MAX]; + if (read_line_prompt(" ", line, sizeof(line)) != 0) { + for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); } + return -1; + } + if (strcmp(line, ".") == 0) break; + size_t line_len = strlen(line); + if (content_len + line_len + 2 > sizeof(content)) { + fprintf(stderr, "%sSkill content too long (max %zu bytes).%s\n", ANSI_RED, sizeof(content) - 1, ANSI_RESET); + break; + } + if (content_len > 0) content[content_len++] = '\n'; + memcpy(content + content_len, line, line_len); + content_len += line_len; + } + if (content_len == 0) { + fprintf(stderr, "%sSkill content cannot be empty.%s\n", ANSI_RED, ANSI_RESET); + continue; + } + content[content_len] = '\0'; + + skill_d_tags[skill_count] = strdup(d_tag); + skill_contents[skill_count] = strdup(content); + char tags_json[512] = {0}; + snprintf(tags_json, sizeof(tags_json), + "[[\"d\",\"%s\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"trigger\",\"dm\"]]", + d_tag); + skill_tags[skill_count] = strdup(tags_json); + skill_kinds[skill_count] = DIDACTYL_DEFAULT_SKILL_KIND; + skill_enabled[skill_count] = 1; + skill_count++; + fprintf(stderr, "%sCustom skill '%s' added.%s\n", ANSI_YELLOW, d_tag, ANSI_RESET); + continue; + } + } +} + static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) { if (!cfg || !agent_name || agent_name[0] == '\0') { return -1; } - if (upsert_startup_event(cfg, - DIDACTYL_DEFAULT_SKILL_KIND, - DIDACTYL_DEFAULT_SKILL_TEMPLATE, - DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) { - return -1; - } - - if (upsert_startup_event(cfg, - DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND, - DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE, - DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) { - return -1; - } - - return 0; + return prompt_skill_configuration(cfg, agent_name); } static int relay_contains(const didactyl_config_t* cfg, const char* relay) {