From 7675109afc53f8462e0f868373360566e00bd04a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Mar 2026 10:14:10 -0400 Subject: [PATCH] v0.0.79 - Enhance existing-agent wizard with full config recovery review edit flow and new-agent fallback --- .gitignore | 2 +- README.md | 4 +- deploy_lt.sh | 8 + plans/enhanced_existing_agent_wizard.md | 149 ++++++++ src/main.h | 4 +- src/setup_wizard.c | 441 +++++++++++++++++++++++- 6 files changed, 592 insertions(+), 16 deletions(-) create mode 100644 plans/enhanced_existing_agent_wizard.md diff --git a/.gitignore b/.gitignore index ce30921..9efed84 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ test_keys.txt /mongoose/ /Trash/ - +deploy_lt.sh # Build artifacts /build/ diff --git a/README.md b/README.md index 2149505..b5d6767 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,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.0.78 +## Current Status — v0.0.79 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.78 — Seed default skill into self-skill cache during startup reconcile +> Last release update: v0.0.79 — Enhance existing-agent wizard with full config recovery review edit flow and new-agent fallback - 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/deploy_lt.sh b/deploy_lt.sh index 495dbe3..106976d 100755 --- a/deploy_lt.sh +++ b/deploy_lt.sh @@ -17,6 +17,14 @@ if [ ! -f "$SOURCE_BINARY" ]; then exit 1 fi +echo "Building static binary first..." +"$SCRIPT_DIR/build_static.sh" + +if [ ! -f "$SOURCE_BINARY" ]; then + echo "ERROR: Build completed but source binary not found: $SOURCE_BINARY" + exit 1 +fi + echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET" rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET" echo "Deployment complete." \ No newline at end of file diff --git a/plans/enhanced_existing_agent_wizard.md b/plans/enhanced_existing_agent_wizard.md new file mode 100644 index 0000000..85c2682 --- /dev/null +++ b/plans/enhanced_existing_agent_wizard.md @@ -0,0 +1,149 @@ +# Enhanced Existing Agent Wizard Flow + +## Problem + +When choosing "existing agent" in the wizard, the current flow: +1. Asks for nsec +2. Recovers kind 10002 relay list from Nostr +3. Immediately boots the agent + +This is insufficient when installing an existing agent on a **new server** because: +- You cannot review or change the admin pubkey +- You cannot review or change the LLM provider/model/API key +- You cannot install a systemd service with a dedicated user +- You cannot see what config was recovered from Nostr +- The agent name is not recovered from the kind 0 profile + +## Current Code + +- [`existing_agent_flow()`](src/setup_wizard.c:1795) — 22 lines, minimal recovery +- [`recover_existing_config_from_nostr()`](src/setup_wizard.c:792) — only recovers kind 10002 relays +- Returns `SETUP_WIZARD_RC_EXISTING` (2) which does NOT set `bootstrap_mode` +- In `main.c`, existing agents skip `reconcile_startup_events()` unless `first_run` is detected + +## Data Available on Nostr for an Existing Agent + +| Data | Kind | Storage | Recovery Method | +|------|------|---------|-----------------| +| Relay list | 10002 | Public tags | `query_and_extract_kind10002_relays()` | +| Agent profile/name | 0 | Public JSON content | Query kind 0 by agent pubkey | +| LLM config | 30078 d=llm_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` | +| Agent config - admin pubkey, DM protocol | 30078 d=agent_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` | +| Default skill | 31124 d=didactyl-default | Public content | Query kind 31124 by agent pubkey | +| Adoption list | 10123 | Public tags | Query kind 10123 by agent pubkey | + +## Proposed Enhanced Flow + +```mermaid +flowchart TD + A[Enter nsec] --> B[Connect to default relays] + B --> C[Recover kind 10002 relay list] + C --> D{Relay list found?} + D -->|No| E{Offer to create new agent with this nsec} + E -->|Yes| E2[Jump to new_agent_flow with nsec pre-loaded] + E -->|No| E3[Return to main menu] + D -->|Yes| F[Reconnect with recovered relays] + F --> G[Recover all config from Nostr] + G --> H[Display recovered config summary] + H --> I{Review each setting} + I -->|Keep all| J[Review summary + launch options] + I -->|Change admin| K[Prompt new admin pubkey] + I -->|Change LLM| L[Prompt LLM config] + I -->|Change relays| M[Prompt relay config] + K --> I + L --> I + M --> I + J --> N{Launch option} + N -->|Boot now| O[Return EXISTING] + N -->|Install systemd| P[Install dedicated-user service] + N -->|Quit| Q[Exit] +``` + +### Step-by-step + +#### Step 1: Identity — Enter nsec +Same as current. Derive keys from nsec. + +#### Step 2: Recovery — Connect and fetch config from Nostr +1. Init nostr handler with default relays +2. Wait for relay connections +3. Query kind 10002 for relay list — if not found, offer to create a new agent with this nsec (jump to `new_agent_flow` with keys pre-loaded, skipping identity step) +4. Cleanup and re-init with recovered relays +5. Wait for relay connections on recovered relays +6. Query kind 0 for agent profile — extract display_name/name +7. Query kind 30078 d=llm_config — decrypt and parse LLM settings +8. Query kind 30078 d=agent_config — decrypt and parse admin pubkey + DM protocol +9. Cleanup nostr handler + +#### Step 3: Review — Present recovered config +Display all recovered values: +``` + ┌─────────────────────────────────────────────┐ + │ Existing Agent -- Recovered Configuration │ + ├─────────────────────────────────────────────┤ + │ Agent name: Simon │ + │ Identity: b27072b7fc2edf45... │ + │ Admin: a1b2c3d4e5f6... │ + │ LLM Provider: ppq │ + │ LLM Model: claude-haiku-4.5 │ + │ LLM Base URL: https://api.ppq.ai │ + │ LLM API Key: sk-...**** │ + │ DM Protocol: nip04 │ + │ Relays: 5 configured │ + └─────────────────────────────────────────────┘ +``` + +Then offer a menu: +``` + [a] change Admin pubkey + [l] change LLM provider/model/key + [r] change Relay configuration + [c] continue with these settings + [q] quit +``` + +Each change option reuses the existing prompt functions (`prompt_admin_pubkey`, `prompt_llm_config`, `prompt_relay_configuration`) but with context-appropriate headers. + +After any change, redisplay the summary and menu. + +#### Step 4: Launch — Boot or install systemd +Same as the new-agent flow's final step: +``` + [b] boot the agent now + [i] install dedicated-user systemd service and boot + [q] quit +``` + +The systemd install reuses `install_system_service_with_dedicated_user()`. + +### Return Codes + +- If user chooses "boot now": return `SETUP_WIZARD_RC_EXISTING` (2) — same as current +- If user chooses "install systemd": return `SETUP_WIZARD_RC_EXIT` (1) — agent runs as systemd service +- If user changed config values: the `main()` flow should still work because `recover_missing_runtime_config_from_nostr()` at line 1108 will fill in any gaps, and the existing agent path at line 1163 loads system context from adopted skills + +### Key Consideration: bootstrap_mode for changed configs + +If the user changes LLM or admin config in the wizard, those changes need to be persisted back to Nostr. Currently, `persist_runtime_config_to_nostr()` is only called when `bootstrap_mode || first_run`. + +**Solution**: When the existing-agent wizard detects that config was changed, return `SETUP_WIZARD_RC_BOOTSTRAP` (0) instead of `SETUP_WIZARD_RC_EXISTING` (2). This triggers the full reconcile path which persists the updated config. + +## Files to Modify + +1. **`src/setup_wizard.c`**: + - Add `recover_full_config_from_nostr()` — fetches kind 0, kind 30078 llm_config, kind 30078 agent_config + - Add `query_self_kind0_name()` — queries agent's own kind 0 profile for name + - Add `fetch_and_decrypt_self_config()` — replicates `fetch_self_config_plaintext()` logic from main.c for use in wizard context + - Rewrite `existing_agent_flow()` with the enhanced multi-step flow + - Make `prompt_admin_pubkey()`, `prompt_llm_config()`, `prompt_relay_configuration()` accept a context string parameter for the page header, or add wrapper versions for the existing-agent context + +2. **`src/main.c`**: + - Potentially expose `fetch_self_config_plaintext()`, `apply_recalled_llm_config()`, `apply_recalled_agent_config()` as non-static, OR duplicate the logic in setup_wizard.c + - Better approach: move these to a shared location or make them accessible via a header + +## Implementation Notes + +- The wizard already calls `nostr_handler_init()` / `nostr_handler_cleanup()` for validation queries. The enhanced flow will do the same but with two init/cleanup cycles: first with default relays to get kind 10002, then with recovered relays to get everything else. +- The `prompt_admin_pubkey()` and `prompt_llm_config()` functions currently have hardcoded step headers like "Step 3 of 7". These should be parameterized or have existing-agent variants. +- The `fetch_self_config_plaintext()` function in main.c requires an active nostr handler. The wizard will need to have the handler initialized when calling it. +- API key display should be masked — show only first 4 and last 4 characters. diff --git a/src/main.h b/src/main.h index 5de2e41..6df2185 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 78 -#define DIDACTYL_VERSION "v0.0.78" +#define DIDACTYL_VERSION_PATCH 79 +#define DIDACTYL_VERSION "v0.0.79" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 8aa7fef..343ee00 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -801,6 +801,261 @@ static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_r return -1; } +static int query_self_kind0_name(const didactyl_config_t* cfg, + char* out_name, + size_t out_name_size, + int* out_found) { + if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->keys.public_key_hex[0] == '\0') { + return -1; + } + + out_name[0] = '\0'; + *out_found = 0; + + cJSON* filter = cJSON_CreateObject(); + cJSON* kinds = cJSON_CreateArray(); + cJSON* authors = cJSON_CreateArray(); + if (!filter || !kinds || !authors) { + cJSON_Delete(filter); + cJSON_Delete(kinds); + cJSON_Delete(authors); + return -1; + } + + cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); + cJSON_AddItemToObject(filter, "authors", authors); + cJSON_AddNumberToObject(filter, "limit", 1); + + char* events_json = nostr_handler_query_json(filter, 3000); + cJSON_Delete(filter); + if (!events_json) { + return -1; + } + + cJSON* arr = cJSON_Parse(events_json); + free(events_json); + if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { + cJSON_Delete(arr); + return 0; + } + + cJSON* ev = cJSON_GetArrayItem(arr, 0); + cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; + if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { + cJSON_Delete(arr); + return 0; + } + + cJSON* profile = cJSON_Parse(content->valuestring); + if (!profile || !cJSON_IsObject(profile)) { + cJSON_Delete(profile); + cJSON_Delete(arr); + return 0; + } + + cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name"); + cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name"); + cJSON* chosen = NULL; + + if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') { + chosen = display_name; + } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { + chosen = name; + } + + if (chosen && chosen->valuestring) { + snprintf(out_name, out_name_size, "%s", chosen->valuestring); + *out_found = 1; + } + + cJSON_Delete(profile); + cJSON_Delete(arr); + return 0; +} + +static int fetch_and_decrypt_self_config_wizard(const didactyl_config_t* cfg, const char* d_tag, char** out_plaintext) { + if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') { + return -1; + } + + *out_plaintext = NULL; + + cJSON* filter = cJSON_CreateObject(); + cJSON* kinds = cJSON_CreateArray(); + cJSON* authors = cJSON_CreateArray(); + cJSON* d_vals = cJSON_CreateArray(); + if (!filter || !kinds || !authors || !d_vals) { + cJSON_Delete(filter); + cJSON_Delete(kinds); + cJSON_Delete(authors); + cJSON_Delete(d_vals); + return -1; + } + + cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078)); + cJSON_AddItemToObject(filter, "kinds", kinds); + cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); + cJSON_AddItemToObject(filter, "authors", authors); + cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag)); + cJSON_AddItemToObject(filter, "#d", d_vals); + cJSON_AddNumberToObject(filter, "limit", 1); + + char* events_json = nostr_handler_query_json(filter, 4000); + cJSON_Delete(filter); + if (!events_json) { + return -1; + } + + cJSON* arr = cJSON_Parse(events_json); + free(events_json); + if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { + cJSON_Delete(arr); + return -1; + } + + cJSON* ev = cJSON_GetArrayItem(arr, 0); + cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; + if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { + cJSON_Delete(arr); + return -1; + } + + size_t out_cap = strlen(content->valuestring) + 1024U; + char* plaintext = (char*)malloc(out_cap); + if (!plaintext) { + cJSON_Delete(arr); + return -1; + } + + int dec_rc = nostr_nip44_decrypt(cfg->keys.private_key, + cfg->keys.public_key, + content->valuestring, + plaintext, + out_cap); + cJSON_Delete(arr); + if (dec_rc != NOSTR_SUCCESS) { + free(plaintext); + return -1; + } + + *out_plaintext = plaintext; + return 0; +} + +static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char* plaintext) { + if (!cfg || !plaintext) return -1; + + cJSON* root = cJSON_Parse(plaintext); + if (!root || !cJSON_IsObject(root)) { + cJSON_Delete(root); + return -1; + } + + cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider"); + cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key"); + cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model"); + cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url"); + cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens"); + cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature"); + + if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || + !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || + !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') { + cJSON_Delete(root); + return -1; + } + + if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') { + snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring); + } else if (cfg->llm.provider[0] == '\0') { + snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai"); + } + + snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring); + snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring); + snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring); + if (max_tokens && cJSON_IsNumber(max_tokens)) { + cfg->llm.max_tokens = (int)max_tokens->valuedouble; + } + if (temperature && cJSON_IsNumber(temperature)) { + cfg->llm.temperature = temperature->valuedouble; + } + + cJSON_Delete(root); + return 0; +} + +static int apply_recalled_agent_config_wizard(didactyl_config_t* cfg, const char* plaintext) { + if (!cfg || !plaintext) return -1; + + cJSON* root = cJSON_Parse(plaintext); + if (!root || !cJSON_IsObject(root)) { + cJSON_Delete(root); + return -1; + } + + cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey"); + cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol"); + + if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') { + char decoded[65] = {0}; + if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) { + snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded); + } + } + + if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') { + if (strcmp(dm_protocol->valuestring, "nip17") == 0) { + cfg->dm_protocol = DM_PROTOCOL_NIP17; + } else if (strcmp(dm_protocol->valuestring, "both") == 0) { + cfg->dm_protocol = DM_PROTOCOL_BOTH; + } else { + cfg->dm_protocol = DM_PROTOCOL_NIP04; + } + } + + cJSON_Delete(root); + return 0; +} + +static int recover_full_config_from_nostr(didactyl_config_t* cfg, + char* out_agent_name, + size_t out_agent_name_size, + int* out_agent_name_found) { + if (!cfg || !out_agent_name || out_agent_name_size == 0 || !out_agent_name_found) { + return -1; + } + + out_agent_name[0] = '\0'; + *out_agent_name_found = 0; + + didactyl_config_t tmp = *cfg; + if (nostr_handler_init(&tmp) != 0) { + return -1; + } + + (void)wait_connected_relays_ms(5000); + + (void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found); + + char* llm_plaintext = NULL; + if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) { + (void)apply_recalled_llm_config_wizard(cfg, llm_plaintext); + } + free(llm_plaintext); + + char* agent_plaintext = NULL; + if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) { + (void)apply_recalled_agent_config_wizard(cfg, agent_plaintext); + } + free(agent_plaintext); + + nostr_handler_cleanup(); + return 0; +} + static int wizard_llm_test(const llm_config_t* llm_cfg) { if (!llm_cfg) return -1; if (llm_init(llm_cfg) != 0) { @@ -1037,10 +1292,12 @@ static int prompt_model_selection(const cJSON* models, } } -static int prompt_admin_pubkey(didactyl_config_t* cfg) { +static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg, + const char* step_title, + const char* section_title) { char input[WIZARD_LINE_MAX]; for (;;) { - render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Administrator"); + render_wizard_page_header(step_title, section_title); if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) { return -1; } @@ -1056,9 +1313,15 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) { } } -static int prompt_llm_config(didactyl_config_t* cfg) { +static int prompt_admin_pubkey(didactyl_config_t* cfg) { + return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator"); +} + +static int prompt_llm_config_with_header(didactyl_config_t* cfg, + const char* step_title, + const char* section_title) { for (;;) { - render_wizard_page_header("Step 5 of 7", "New Agent Setup -- LLM Provider"); + render_wizard_page_header(step_title, section_title); print_option('1', " ppq.ai"); print_option('2', " Routstr"); print_option('3', " OpenAI-compatible"); @@ -1179,11 +1442,17 @@ static int prompt_llm_config(didactyl_config_t* cfg) { } } -static int prompt_relay_configuration(didactyl_config_t* cfg) { +static int prompt_llm_config(didactyl_config_t* cfg) { + return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider"); +} + +static int prompt_relay_configuration_with_header(didactyl_config_t* cfg, + const char* step_title, + const char* section_title) { char input[WIZARD_LINE_MAX]; for (;;) { - render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Relay Configuration"); + render_wizard_page_header(step_title, section_title); fprintf(stderr, " Current relays:\n"); for (int i = 0; i < cfg->relay_count; i++) { fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : ""); @@ -1235,6 +1504,10 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) { } } +static int prompt_relay_configuration(didactyl_config_t* cfg) { + return prompt_relay_configuration_with_header(cfg, "Step 6 of 7", "New Agent Setup -- Relay Configuration"); +} + static int run_command_local(char* const argv[]) { pid_t pid = fork(); if (pid < 0) return -1; @@ -1794,6 +2067,10 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t static int existing_agent_flow(didactyl_config_t* cfg) { char nsec_in[OW_MAX_KEY_LEN] = {0}; + char agent_name[OW_MAX_NAME_LEN] = {0}; + int agent_name_found = 0; + int config_changed = 0; + render_wizard_page_header("Existing Agent", "Identity"); if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1; if (line_is_quit(nsec_in)) return -1; @@ -1807,13 +2084,153 @@ static int existing_agent_flow(didactyl_config_t* cfg) { render_wizard_page_header("Existing Agent", "Config Recovery"); fprintf(stderr, " Relay list (kind 10002): NOT FOUND\n\n"); fprintf(stderr, "%sUnable to find a kind 10002 relay list for this identity.%s\n", ANSI_RED, ANSI_RESET); + fprintf(stderr, "\n"); + print_option('n', "ew agent setup with this identity"); + print_option('q', "uit to main menu"); + wizard_option_t opts[] = {{'n', ""}, {'q', ""}}; + char c = read_menu_choice(opts, 2); + if (c == 'n') { + int rc = new_agent_flow(cfg, NULL, 0); + if (rc == 0) return SETUP_WIZARD_RC_BOOTSTRAP; + if (rc == 1) return SETUP_WIZARD_RC_EXIT; + return -1; + } return -1; } - render_wizard_page_header("Existing Agent", "Config Recovery"); - fprintf(stderr, " Relay list (kind 10002): FOUND (%d relays)\n\n", cfg->relay_count); + if (recover_full_config_from_nostr(cfg, agent_name, sizeof(agent_name), &agent_name_found) != 0) { + fprintf(stderr, "%sWarning: unable to recover full config from Nostr, continuing with partial recovery.%s\n", + ANSI_YELLOW, + ANSI_RESET); + } - return 0; + if (!agent_name_found || agent_name[0] == '\0') { + snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl"); + } + + for (;;) { + char masked_key[64] = {0}; + size_t api_len = strlen(cfg->llm.api_key); + if (api_len >= 8U) { + snprintf(masked_key, + sizeof(masked_key), + "%.4s...%s", + cfg->llm.api_key, + cfg->llm.api_key + api_len - 4U); + } else if (api_len > 0U) { + snprintf(masked_key, sizeof(masked_key), "****"); + } else { + snprintf(masked_key, sizeof(masked_key), "(not set)"); + } + + render_wizard_page_header("Existing Agent", "Recovered Configuration"); + fprintf(stderr, " Name: %s\n", agent_name); + fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); + fprintf(stderr, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)"); + fprintf(stderr, " LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)"); + fprintf(stderr, " LLM Model: %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)"); + fprintf(stderr, " LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)"); + fprintf(stderr, " LLM API Key: %s\n", masked_key); + fprintf(stderr, " Relays: %d configured\n", cfg->relay_count); + fprintf(stderr, "\n"); + + print_option('a', "dmin pubkey"); + print_option('l', "lm config"); + print_option('r', "elay configuration"); + print_option('c', "ontinue to launch options"); + print_option('q', "uit"); + + wizard_option_t opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'c', ""}, {'q', ""}}; + char c = read_menu_choice(opts, 5); + if (c == 'q') { + return -1; + } + if (c == 'c') { + break; + } + if (c == 'a') { + if (prompt_admin_pubkey_with_header(cfg, + "Existing Agent -- Review", + "Existing Agent -- Administrator") != 0) { + return -1; + } + config_changed = 1; + continue; + } + if (c == 'l') { + if (prompt_llm_config_with_header(cfg, + "Existing Agent -- Review", + "Existing Agent -- LLM Provider") != 0) { + return -1; + } + config_changed = 1; + continue; + } + if (c == 'r') { + for (;;) { + int rc = prompt_relay_configuration_with_header(cfg, + "Existing Agent -- Review", + "Existing Agent -- Relay Configuration"); + if (rc < 0) return -1; + if (rc == 1) break; + config_changed = 1; + break; + } + continue; + } + } + + render_wizard_page_header("Existing Agent", "Launch Options"); + fprintf(stderr, " Name: %s\n", agent_name); + fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); + fprintf(stderr, " Admin: %.16s...\n", cfg->admin.pubkey); + fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url); + fprintf(stderr, " Relays: %d configured\n\n", cfg->relay_count); + + print_option('b', "oot the agent now"); + print_option('i', "nstall dedicated-user systemd service and boot"); + print_option('q', "uit"); + + wizard_option_t launch_opts[] = {{'b', ""}, {'i', ""}, {'q', ""}}; + char lc = read_menu_choice(launch_opts, 3); + if (lc == 'q') return -1; + + if (lc == 'b') { + return config_changed ? SETUP_WIZARD_RC_BOOTSTRAP : SETUP_WIZARD_RC_EXISTING; + } + + if (lc == 'i') { + if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) { + fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET); + return -1; + } + + char service_user[64] = {0}; + sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user)); + + char service_name[96] = {0}; + snprintf(service_name, sizeof(service_name), "%s.service", service_user); + + sleep(2); + char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL}; + if (run_privileged_command_local(is_active_argv) == 0) { + fprintf(stderr, + "%sService %s is active. Setup is complete; this wizard will now exit.%s\n", + ANSI_YELLOW, + service_name, + ANSI_RESET); + } else { + fprintf(stderr, + "%sService installed but not active yet. Check status with: sudo systemctl status %s%s\n", + ANSI_RED, + service_name, + ANSI_RESET); + } + + return SETUP_WIZARD_RC_EXIT; + } + + return -1; } static int load_genesis_path_flow(didactyl_config_t* cfg) { @@ -1876,8 +2293,10 @@ int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t g if (c == 'e') { int rc = existing_agent_flow(config); - if (rc == 0) { - return SETUP_WIZARD_RC_EXISTING; + if (rc == SETUP_WIZARD_RC_BOOTSTRAP || + rc == SETUP_WIZARD_RC_EXISTING || + rc == SETUP_WIZARD_RC_EXIT) { + return rc; } return rc; }