diff --git a/README.md b/README.md index 7710f96..c63905f 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.67 +## Current Status — v0.2.68 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.67 — Add n=none to relay menu to deselect all relays; add blank line before 'Type ok' prompt in admin keypair generation +> Last release update: v0.2.68 — Replace multi-line skill edit with system editor (/nano/vim/vi) in Step 7 skill picker - 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/main.h b/src/main.h index 6416c45..7fbace7 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 67 -#define DIDACTYL_VERSION "v0.2.67" +#define DIDACTYL_VERSION_PATCH 68 +#define DIDACTYL_VERSION "v0.2.68" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index af8226c..298f3bf 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -643,6 +643,103 @@ static const skill_descriptor_t DIDACTYL_DEFAULT_SKILLS[] = { #define DIDACTYL_DEFAULT_SKILL_COUNT \ ((int)(sizeof(DIDACTYL_DEFAULT_SKILLS) / sizeof(DIDACTYL_DEFAULT_SKILLS[0]))) +/* Launch the system editor ($EDITOR, editor, nano, vim, vi) to edit content. + * Writes initial_content to a temp file, opens the editor, reads it back. + * Returns 0 on success with out_content allocated, -1 on failure. */ +static int edit_content_with_editor(const char* initial_content, char** out_content) { + if (!out_content) return -1; + *out_content = NULL; + + char tmp_path[] = "/tmp/didactyl_skill_XXXXXX"; + int fd = mkstemp(tmp_path); + if (fd < 0) { + fprintf(stderr, "%sFailed to create temp file for editing.%s\n", ANSI_RED, ANSI_RESET); + return -1; + } + + if (initial_content && initial_content[0] != '\0') { + size_t len = strlen(initial_content); + ssize_t written = write(fd, initial_content, len); + (void)written; + } + close(fd); + + /* Detect editor: $EDITOR, then editor (Debian policy), nano, vim, vi. */ + const char* editor = getenv("EDITOR"); + if (!editor || editor[0] == '\0') editor = "editor"; + + pid_t pid = fork(); + if (pid == 0) { + /* Child: exec the editor. Try detected editor first, then fallbacks. */ + execlp(editor, editor, tmp_path, (char*)NULL); + execlp("nano", "nano", tmp_path, (char*)NULL); + execlp("vim", "vim", tmp_path, (char*)NULL); + execlp("vi", "vi", tmp_path, (char*)NULL); + _exit(1); + } + + if (pid < 0) { + fprintf(stderr, "%sFailed to fork for editor.%s\n", ANSI_RED, ANSI_RESET); + unlink(tmp_path); + return -1; + } + + int status = 0; + waitpid(pid, &status, 0); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "%sEditor exited with error.%s\n", ANSI_RED, ANSI_RESET); + unlink(tmp_path); + return -1; + } + + /* Read the file back. */ + FILE* f = fopen(tmp_path, "r"); + if (!f) { + fprintf(stderr, "%sFailed to read edited file.%s\n", ANSI_RED, ANSI_RESET); + unlink(tmp_path); + return -1; + } + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + rewind(f); + + if (fsize <= 0) { + fprintf(stderr, "%sEdited file is empty.%s\n", ANSI_RED, ANSI_RESET); + fclose(f); + unlink(tmp_path); + return -1; + } + + char* buf = (char*)malloc((size_t)fsize + 1U); + if (!buf) { + fclose(f); + unlink(tmp_path); + return -1; + } + + size_t nread = fread(buf, 1, (size_t)fsize, f); + fclose(f); + unlink(tmp_path); + + if (nread == 0) { + free(buf); + return -1; + } + + buf[nread] = '\0'; + + /* Strip trailing newlines (editor often adds one). */ + while (nread > 0 && (buf[nread - 1] == '\n' || buf[nread - 1] == '\r')) { + buf[nread - 1] = '\0'; + nread--; + } + + *out_content = buf; + return 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) { @@ -707,34 +804,16 @@ static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_ /* Cycle: 0→1 (enabled), 1→2 (edit), 2→0 (disabled) */ skill_state[idx]++; if (skill_state[idx] > 2) skill_state[idx] = 0; - /* If we entered edit mode, prompt to edit the content. */ + /* If we entered edit mode, open the system editor. */ if (skill_state[idx] == 2) { - fprintf(stderr, "\n Editing skill '%s':\n", skill_d_tags[idx] ? skill_d_tags[idx] : ""); - fprintf(stderr, " Current content:\n%s\n\n", skill_contents[idx] ? skill_contents[idx] : ""); - fprintf(stderr, " Enter new content (markdown, end with '.' on its own line):\n"); - char new_content[4096] = {0}; - 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(new_content)) { - fprintf(stderr, "%sContent too long (max %zu bytes).%s\n", ANSI_RED, sizeof(new_content) - 1, ANSI_RESET); - break; - } - if (content_len > 0) new_content[content_len++] = '\n'; - memcpy(new_content + content_len, line, line_len); - content_len += line_len; - } - if (content_len > 0) { - new_content[content_len] = '\0'; + fprintf(stderr, "\n Opening editor for skill '%s'...\n", skill_d_tags[idx] ? skill_d_tags[idx] : ""); + char* edited = NULL; + if (edit_content_with_editor(skill_contents[idx], &edited) == 0 && edited) { free(skill_contents[idx]); - skill_contents[idx] = strdup(new_content); + skill_contents[idx] = edited; fprintf(stderr, "%sSkill '%s' updated.%s\n", ANSI_YELLOW, skill_d_tags[idx] ? skill_d_tags[idx] : "", ANSI_RESET); + } else { + fprintf(stderr, "%sEdit cancelled or failed; keeping original content.%s\n", ANSI_RED, ANSI_RESET); } /* After edit, set back to enabled. */ skill_state[idx] = 1;