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:
qorx-- 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()fromnip006.h - Call
nostr_key_to_bech32()fromnip019.hwithhrp="nsec"andhrp="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
termiosecho 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
bto 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_tin 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
termiosecho disable - Validate and derive pubkey
- Display npub for confirmation
B2. Recover Config from Nostr
After key derivation:
- Connect to default bootstrap relays
- Query kind 10002 for the pubkey's relay list
- If relay list found: expand relay pool with discovered relays
- Query encrypted kind 30078 events for
d=llm_configandd=agent_config - 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 pairnostr_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()andrecover_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 (notscanf) - Mask nsec input with
termiosecho disable on Linux - Handle Ctrl+C gracefully (restore terminal settings via
atexithandler) - 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
- Piped/non-TTY stdin: Detect with
isatty(STDIN_FILENO). If not a TTY, print usage and exit instead of entering wizard. - Ctrl+C during setup: Signal handler restores terminal settings and exits cleanly.
- Genesis file already exists at write path: Prompt "[O]verwrite or [C]hoose different path?" before overwriting.
- LLM test timeout: Set a reasonable timeout (10s) and allow skipping.
- No relay connectivity during identity check: Warn but allow proceeding -- the check is best-effort.
- Existing agent with no config on Nostr: Seamlessly transition to new-agent steps for the missing pieces.
- Back navigation: Each step supports
bto go back to the previous step.