Files
didactyl/plans/interactive_guided_setup.md
T

430 lines
12 KiB
Markdown

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