430 lines
12 KiB
Markdown
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.
|