# Didactyl — Interactive Guided Setup Mode ## Overview When `./didactyl` is run with **no command-line arguments**, the agent enters an interactive guided setup wizard on the terminal. This replaces the current behavior of silently trying to load `./genesis.jsonc` and failing. The wizard first asks whether the user is creating a new agent or starting an existing one, then branches accordingly. --- ## Trigger Condition ``` if (argc == 1) -> enter interactive setup mode ``` Any argument at all (`--config`, `--nsec`, `--help`, etc.) bypasses the wizard and uses the existing startup path. The zero-argument case is the only entry point. --- ## TUI Menu Convention All menus use **single-letter hotkeys** (case-insensitive). The hotkey letter is rendered **underlined** in the terminal using ANSI escape `\e[4m` (underline on) and `\e[0m` (reset). For example, `[N]ew` displays with the N underlined. Global shortcuts: - `q` or `x` -- quit/back out of any menu - Ctrl+C -- clean exit (restore terminal settings) Input is read with `fgets()` and matched on the first non-whitespace character. --- ## Wizard Flow ```mermaid flowchart TD A[./didactyl with no args] --> B[Welcome screen] B --> C{New or Existing?} C -->|New agent| D[New Agent Flow] C -->|Existing agent| E[Existing Agent Flow] C -->|Load genesis| F[Load genesis.jsonc and boot] C -->|Quit| Z[Exit] D --> D1[Step: Generate or provide nsec] D1 --> D2[Check if pubkey exists on Nostr] D2 -->|Exists - warn| D3{Continue or abort?} D2 -->|Fresh| D4[Step: Admin npub] D3 -->|Continue| D4 D3 -->|Abort| Z D4 --> D5[Step: LLM provider + test] D5 --> D6[Step: Relay config] D6 --> D7[Step: Review and boot/export] D7 --> BOOT[Normal startup] E --> E1[Step: Provide nsec] E1 --> E2[Connect to bootstrap relays] E2 --> E3[Recover config from Nostr] E3 -->|Config found| E4{Review recovered config} E3 -->|Config missing| E5[Prompt for missing config] E4 --> BOOT E5 --> D4 ``` --- ## Step Details ### Welcome Screen **Prompt:** ``` ============================================= Didactyl v0.0.71 -- Interactive Setup ============================================= [N]ew agent -- create a fresh Nostr identity [E]xisting -- start an agent you have already set up [L]oad -- boot from a genesis.jsonc file [Q]uit > ``` --- ### Path A: New Agent #### A1. Identity **Prompt:** ``` New Agent Setup -- Identity [G]enerate a new Nostr keypair [P]rovide an existing nsec [B]ack > ``` **Option G -- Generate new keypair:** - Call `nostr_generate_keypair()` from `nip006.h` - Call `nostr_key_to_bech32()` from `nip019.h` with `hrp="nsec"` and `hrp="npub"` to display bech32 keys - Display the generated nsec and npub - **Critical warning**: "Save your nsec securely. It will NOT be stored unless you choose to write a genesis file." - Prompt user to confirm they have saved the nsec before proceeding **Option P -- Provide existing nsec:** - Accept nsec1... bech32 or 64-char hex - Mask input with `termios` echo disable - Validate using existing `derive_keys_from_nsec()` logic - Display derived npub for confirmation **After key derivation -- Existing identity check:** - Connect to default bootstrap relays (damus, primal, nos.lol) - Query kind 10002 for the derived pubkey - If found: warn the user that this identity already exists on Nostr - "This pubkey already has a kind 10002 relay list on Nostr." - "Running first-run genesis will overwrite existing profile/relay/skill events." - "[C]ontinue anyway or [A]bort?" - If not found: inform user this is a fresh identity, proceed #### A2. Administrator **Prompt:** ``` New Agent Setup -- Administrator Enter the admin's Nostr public key (npub1... or hex): > ``` - Validate using `decode_pubkey_hex_or_npub()` - Display the decoded hex for confirmation - This is required -- loop until valid input - `b` to go back #### A3. LLM Provider **Prompt:** ``` New Agent Setup -- LLM Provider Didactyl needs an OpenAI-compatible LLM API. Base URL [https://api.openai.com/v1]: API Key: Model [gpt-4o-mini]: Max Tokens [512]: Temperature [0.7]: ``` - Show defaults in brackets, accept Enter for default - After collecting all fields, make a **test API call**: - Send a minimal chat completion request - Display result: "LLM test: OK (model responded)" or "LLM test: FAILED (HTTP 401 -- check API key)" - On failure: ``` LLM test: FAILED (HTTP 401 -- check API key) [R]e-enter LLM settings [S]kip test and continue anyway [Q]uit > ``` #### A4. Relay Configuration **Prompt:** ``` New Agent Setup -- Relay Configuration Current relays: 1. wss://relay.damus.io 2. wss://nos.lol 3. wss://relay.primal.net [A]dd a relay [R]emove a relay (by number) [D]one -- use this list [B]ack > ``` - Start with the 3 default bootstrap relays - Allow adding custom relay URLs (validate wss:// or ws:// prefix) - Allow removing by number (prompt: "Remove which number?") - Minimum 1 relay required - Display updated list after each change #### A5. Review and Confirm **Prompt:** ``` New Agent Setup -- Review Identity: npub1... Admin: npub1... LLM: claude-haiku-4.5 @ https://api.ppq.ai Relays: 3 configured DM Protocol: nip04 [B]oot the agent now [W]rite genesis.jsonc (without nsec) and boot [I]nclude nsec in genesis.jsonc and boot (security risk!) [E]xport genesis.jsonc (without nsec) and exit [S]tart over [Q]uit > ``` - **B** -- populate `didactyl_config_t` in memory and proceed to normal startup - **W** -- write genesis.jsonc without the nsec field, then boot - **I** -- write genesis.jsonc WITH nsec (with explicit warning), then boot - **E** -- write file and exit so user can review - **S** -- restart wizard from welcome screen - **Q** -- exit --- ### Path B: Existing Agent #### B1. Provide nsec **Prompt:** ``` Existing Agent -- Identity Enter your agent's nsec (nsec1... or hex): > ``` - Mask input with `termios` echo disable - Validate and derive pubkey - Display npub for confirmation #### B2. Recover Config from Nostr After key derivation: 1. Connect to default bootstrap relays 2. Query kind 10002 for the pubkey's relay list 3. If relay list found: expand relay pool with discovered relays 4. Query encrypted kind 30078 events for `d=llm_config` and `d=agent_config` 5. Decrypt and apply recovered config (reuses existing `recover_missing_runtime_config_from_nostr()` logic) **Display recovery status:** ``` Existing Agent -- Config Recovery Relay list (kind 10002): FOUND (6 relays) LLM config: FOUND (claude-haiku-4.5 @ ppq.ai) Admin config: FOUND (npub1...) [B]oot with recovered config [E]dit settings before booting [Q]uit > ``` If any required config is missing: ``` Existing Agent -- Config Recovery Relay list (kind 10002): FOUND (6 relays) LLM config: NOT FOUND Admin config: NOT FOUND Some required config was not found on Nostr. Entering guided setup for missing fields... ``` Then jump to the appropriate new-agent steps (A2/A3) for the missing pieces only. --- ### Path C: Load Genesis File **Prompt:** ``` Load Genesis File Path [./genesis.jsonc]: > ``` - Accept file path, default to `./genesis.jsonc` - Load with `config_load()` - If nsec is missing from the file, prompt for it - Proceed to normal startup --- ## Implementation Architecture ### New Source File: `src/setup_wizard.c` / `src/setup_wizard.h` Keep the wizard logic isolated from `main.c`. The interface: ```c // Returns 0 on success (config populated, ready to boot) // Returns -1 on abort/error // Returns 1 on "wrote genesis and exit" (no boot) int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t path_size); ``` ### Changes to `src/main.c` In `main()`, before the existing argument parsing: ```c if (argc == 1) { if (!isatty(STDIN_FILENO)) { print_usage(argv[0]); return 1; } debug_init(DEBUG_LEVEL_INFO); if (nostr_init() != NOSTR_SUCCESS) { fprintf(stderr, "Failed to initialize nostr core\n"); return 1; } didactyl_config_t cfg; memset(&cfg, 0, sizeof(cfg)); char genesis_path[256] = {0}; int wizard_rc = setup_wizard_run(&cfg, genesis_path, sizeof(genesis_path)); if (wizard_rc < 0) { nostr_cleanup(); return 1; // user aborted } if (wizard_rc == 1) { nostr_cleanup(); return 0; // wrote genesis and exit } // Continue with normal startup using populated cfg // Jump past config_load() into the startup checklist goto startup_with_config; } ``` ### Changes to `nostr_core_lib` **No new functions needed.** The existing API surface is sufficient: - `nostr_generate_keypair()` -- generate private/public key pair - `nostr_key_to_bech32(key, "nsec", output)` -- encode private key as nsec1... - `nostr_key_to_bech32(key, "npub", output)` -- encode public key as npub1... - `nostr_decode_nsec()` / `nostr_decode_npub()` -- decode bech32 inputs ### LLM Test Call Reuse the existing `llm.c` infrastructure: - Temporarily initialize `llm_init()` with the test config - Make a single completion call with a trivial prompt - Check for HTTP success and valid response - Clean up with `llm_cleanup()` ### Existing Identity Check / Config Recovery For both new and existing agent paths: - Initialize relay pool with bootstrap relays - Query kind 10002 for the derived pubkey - For existing agents: also query kind 30078 encrypted config events - Reuse `is_first_run_from_kind10002()` and `recover_missing_runtime_config_from_nostr()` logic ### Genesis File Writer A helper function that serializes the collected config to JSONC format: ```c int setup_wizard_write_genesis(const didactyl_config_t* config, const char* path, int include_nsec); ``` This writes a human-readable JSONC file with comments, matching the existing `genesis.jsonc` format. --- ## Terminal I/O Considerations - Use `fgets()` for line input (not `scanf`) - Mask nsec input with `termios` echo disable on Linux - Handle Ctrl+C gracefully (restore terminal settings via `atexit` handler) - Use ANSI underline (`\e[4m`) for hotkey letters in menu options - Use ANSI colors sparingly for emphasis (warnings in yellow/red) - All prompts go to stderr, so stdout can be piped if needed --- ## Security Considerations - Generated nsec is held in memory only unless user explicitly chooses to write genesis with nsec - nsec input is masked (echo disabled) during entry - If writing genesis with nsec, display a clear warning about the security implications - The nsec-less genesis file is the recommended default - After writing, suggest: "For subsequent runs, use: `./didactyl --config genesis.jsonc --nsec `" - Or: "Set DIDACTYL_NSEC environment variable for convenience" --- ## Files to Create/Modify | File | Action | Description | |------|--------|-------------| | `src/setup_wizard.h` | Create | Wizard public interface | | `src/setup_wizard.c` | Create | Interactive wizard implementation | | `src/main.c` | Modify | Add `argc == 1` check to enter wizard before arg parsing | | `Makefile` | Modify | Add `setup_wizard.o` to build | | `docs/GENESIS.md` | Modify | Document the interactive setup mode | | `README.md` | Modify | Add guided setup to usage section | --- ## Edge Cases 1. **Piped/non-TTY stdin**: Detect with `isatty(STDIN_FILENO)`. If not a TTY, print usage and exit instead of entering wizard. 2. **Ctrl+C during setup**: Signal handler restores terminal settings and exits cleanly. 3. **Genesis file already exists at write path**: Prompt "[O]verwrite or [C]hoose different path?" before overwriting. 4. **LLM test timeout**: Set a reasonable timeout (10s) and allow skipping. 5. **No relay connectivity during identity check**: Warn but allow proceeding -- the check is best-effort. 6. **Existing agent with no config on Nostr**: Seamlessly transition to new-agent steps for the missing pieces. 7. **Back navigation**: Each step supports `b` to go back to the previous step.