Files
didactyl/plans/interactive_guided_setup.md

12 KiB

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

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:

// 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:

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:

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 <your_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.