From 410400418c9801931d01b80909d392ae50956289 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 28 Feb 2026 16:57:08 -0400 Subject: [PATCH] v0.0.11 - auto update readme.md --- README.md | 10 +++--- increment_and_push.sh | 79 +++++++++++++++++++++++++++++++++++++++++++ src/config.c | 39 +++++++++++++++++++++ src/config.h | 1 + src/main.c | 1 + src/main.h | 4 +-- 6 files changed, 128 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 40c1188..dcb94bc 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ # Didactyl -An unstoppable agentic system. +An unstoppable agentic network. -Didactyl boots on any internet-connected machine, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands — all orchestrated through Nostr. +Didactyl boots on an internet-connected computer, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, and sharing new skills and learning with other agents — all orchestrated through Nostr. ## Philosophy **Nostr-first.** Where traditional agents ride on top of Linux — reading files, writing to disk — Didactyl rides on top of Nostr. Events are its files. Relays are its network bus. Blossom is its blob storage. The Linux host is just the runtime substrate. -Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (no single entity can erase its memory). +Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (destroying the computer it is on will not kill it.). **Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them. -## Current Status — v0.0.8 +## Current Status — v0.0.11 **Active build — relay-aware autonomous agent with tool-use and Nostr-native startup memory.** +> Last release update: v0.0.11 — auto update readme.md + - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected - Uses kind `31120` startup content as live Soul at boot diff --git a/increment_and_push.sh b/increment_and_push.sh index 820ef9f..5424703 100755 --- a/increment_and_push.sh +++ b/increment_and_push.sh @@ -200,6 +200,79 @@ update_version_in_header() { print_success "Updated version in src/main.h to $new_version" } +# Function to update README Current Status version and release comment +update_readme_current_status() { + local new_version="$1" + local release_comment="$2" + + if [[ ! -f "README.md" ]]; then + print_warning "README.md not found, skipping Current Status update" + return 0 + fi + + print_status "Updating README Current Status section..." + + if python3 - "$new_version" "$release_comment" <<'PY' +import sys +from pathlib import Path + +version = sys.argv[1] +comment = sys.argv[2] +path = Path("README.md") +text = path.read_text(encoding="utf-8") +lines = text.splitlines() + +heading_idx = None +for i, line in enumerate(lines): + if line.startswith("## Current Status"): + heading_idx = i + break + +if heading_idx is None: + raise SystemExit("README Current Status heading not found") + +lines[heading_idx] = f"## Current Status — {version}" + +section_end = len(lines) +for i in range(heading_idx + 1, len(lines)): + if lines[i].startswith("## "): + section_end = i + break + +comment_line = f"> Last release update: {version} — {comment}" + +# Replace existing autogenerated release comment inside Current Status section +existing_idx = None +for i in range(heading_idx + 1, section_end): + if lines[i].startswith("> Last release update:"): + existing_idx = i + break + +if existing_idx is not None: + lines[existing_idx] = comment_line +else: + insert_at = None + for i in range(heading_idx + 1, section_end): + if lines[i].startswith("**Active build"): + insert_at = i + 1 + break + + if insert_at is None: + insert_at = heading_idx + 1 + + payload = ["", comment_line] + lines[insert_at:insert_at] = payload + +path.write_text("\n".join(lines) + "\n", encoding="utf-8") +PY + then + print_success "Updated README Current Status section" + else + print_error "Failed to update README Current Status section" + exit 1 + fi +} + # Function to commit and push changes without creating a tag (tag already created) git_commit_and_push_no_tag() { print_status "Preparing git commit..." @@ -427,6 +500,9 @@ main() { export NEW_VERSION fi + # Keep README Current Status in sync with the version and commit message + update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE" + # Create new git tag BEFORE compilation so version picks it up if git tag "$NEW_VERSION" > /dev/null 2>&1; then print_success "Created tag: $NEW_VERSION" @@ -482,6 +558,9 @@ main() { # Increment version based on type (default to patch) increment_version "$VERSION_INCREMENT_TYPE" + # Keep README Current Status in sync with the version and commit message + update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE" + # Create new git tag BEFORE compilation so version picks it up if git tag "$NEW_VERSION" > /dev/null 2>&1; then print_success "Created tag: $NEW_VERSION" diff --git a/src/config.c b/src/config.c index 02d0ec6..010a335 100644 --- a/src/config.c +++ b/src/config.c @@ -3,6 +3,8 @@ #include "config.h" #include +#include +#include #include #include #include @@ -10,6 +12,19 @@ #include "cjson/cJSON.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" +static char g_config_last_error[512] = {0}; + +static void config_set_error(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vsnprintf(g_config_last_error, sizeof(g_config_last_error), fmt ? fmt : "unknown configuration error", ap); + va_end(ap); +} + +const char* config_last_error(void) { + return g_config_last_error[0] != '\0' ? g_config_last_error : "unknown configuration error"; +} + static int read_file_to_buffer(const char* path, char** out_buf, size_t* out_len) { FILE* fp = fopen(path, "rb"); if (!fp) { @@ -486,9 +501,12 @@ void config_free(didactyl_config_t* config) { int config_load(const char* path, didactyl_config_t* config) { if (!path || !config) { + config_set_error("config_load called with invalid arguments"); return -1; } + config_set_error("unknown configuration error"); + memset(config, 0, sizeof(*config)); config->tools.enabled = 1; config->tools.max_turns = 8; @@ -516,6 +534,7 @@ int config_load(const char* path, didactyl_config_t* config) { char* json_buf = NULL; size_t json_len = 0; if (read_file_to_buffer(path, &json_buf, &json_len) != 0) { + config_set_error("failed to read config file '%s': %s", path, strerror(errno)); return -1; } @@ -523,6 +542,11 @@ int config_load(const char* path, didactyl_config_t* config) { free(json_buf); if (!root) { + const char* err = cJSON_GetErrorPtr(); + config_set_error("invalid JSON in config '%s'%s%s", + path, + err ? " near: " : "", + err ? err : ""); return -1; } @@ -535,26 +559,32 @@ int config_load(const char* path, didactyl_config_t* config) { if (!keys || !cJSON_IsObject(keys) || !admin || !cJSON_IsObject(admin) || !llm || !cJSON_IsObject(llm)) { + config_set_error("config must include object sections: keys, admin, llm"); goto cleanup; } if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 1) != 0) { + config_set_error("keys.nsec is required and must be a non-empty string"); goto cleanup; } char admin_key_raw[OW_MAX_KEY_LEN] = {0}; if (copy_json_string(admin, "pubkey", admin_key_raw, sizeof(admin_key_raw), 1) != 0) { + config_set_error("admin.pubkey is required and must be a non-empty string"); goto cleanup; } if (decode_pubkey_hex_or_npub(admin_key_raw, config->admin.pubkey) != 0) { + config_set_error("admin.pubkey must be npub1... or 64-char hex"); goto cleanup; } if (parse_relays(root, config) != 0) { + config_set_error("relays must be a non-empty array of relay URLs"); goto cleanup; } if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) { + config_set_error("llm.provider must be a string if provided"); goto cleanup; } if (config->llm.provider[0] == '\0') { @@ -562,12 +592,15 @@ int config_load(const char* path, didactyl_config_t* config) { } if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) { + config_set_error("llm.api_key is required and must be a string"); goto cleanup; } if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) { + config_set_error("llm.model is required and must be a string"); goto cleanup; } if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) { + config_set_error("llm.base_url is required and must be a string"); goto cleanup; } @@ -578,26 +611,32 @@ int config_load(const char* path, didactyl_config_t* config) { config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7; if (parse_tools_config(root, config) != 0) { + config_set_error("invalid tools configuration"); goto cleanup; } if (parse_security_config(root, config) != 0) { + config_set_error("invalid security configuration"); goto cleanup; } if (parse_admin_context_config(root, config) != 0) { + config_set_error("invalid admin_context configuration"); goto cleanup; } if (parse_startup_events(root, config) != 0) { + config_set_error("invalid startup_events configuration"); goto cleanup; } if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) { + config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key"); goto cleanup; } if (nostr_ec_public_key_from_private_key(config->keys.private_key, config->keys.public_key) != 0) { + config_set_error("failed to derive public key from keys.nsec"); goto cleanup; } diff --git a/src/config.h b/src/config.h index a7eb9e2..27e23d3 100644 --- a/src/config.h +++ b/src/config.h @@ -86,6 +86,7 @@ typedef struct { } didactyl_config_t; int config_load(const char* path, didactyl_config_t* config); +const char* config_last_error(void); void config_free(didactyl_config_t* config); #endif \ No newline at end of file diff --git a/src/main.c b/src/main.c index fa9ff3a..c64487c 100644 --- a/src/main.c +++ b/src/main.c @@ -47,6 +47,7 @@ int main(int argc, char** argv) { didactyl_config_t cfg; if (config_load(config_path, &cfg) != 0) { fprintf(stderr, "Failed to load config: %s\n", config_path); + fprintf(stderr, "Config error: %s\n", config_last_error()); nostr_cleanup(); return 1; } diff --git a/src/main.h b/src/main.h index 6cb888d..50ba286 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 0 -#define DIDACTYL_VERSION_PATCH 10 -#define DIDACTYL_VERSION "v0.0.10" +#define DIDACTYL_VERSION_PATCH 11 +#define DIDACTYL_VERSION "v0.0.11" // Agent metadata #define DIDACTYL_NAME "Didactyl"