Skip to content

Commit bb4b172

Browse files
authored
Overhaul bootstrap UX: CWD-independent config discovery, wizard resume + preflight, Telegram pairing, Bedrock region prefixes (#112)
1 parent a61f436 commit bb4b172

12 files changed

Lines changed: 1727 additions & 81 deletions

config.example.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ gateway:
3636
# Channels
3737
telegram:
3838
enabled: true
39-
dm_policy: pairing # "open" or "pairing" (require pairing code)
39+
# DM authorization:
40+
# pairing — only users in allowed_users may talk to the bot. New users
41+
# authorize with a one-time code: run `nerve pair` on the
42+
# server, then send the bot `/pair <code>`. Paired IDs are
43+
# persisted to config.local.yaml automatically.
44+
# open — anyone can talk to the bot (dangerous: full agent access).
45+
dm_policy: pairing
46+
# allowed_users: [123456789] # numeric Telegram user IDs (or pair instead)
4047
stream_mode: partial # "partial" (edit messages) or "full" (wait for complete)
4148

4249
# Quiet hours (local timezone)

docs/config.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ Nerve uses two YAML config files:
55
- `config.local.yaml` — Secrets and personal overrides (gitignored)
66

77
Values in `config.local.yaml` are deep-merged on top of `config.yaml`.
8+
Unknown keys are ignored but logged as warnings at startup (and shown by
9+
`nerve doctor`) so typos don't fail silently.
10+
11+
## Config Directory Resolution
12+
13+
`nerve` commands locate the config directory via a waterfall, so they work
14+
from any working directory:
15+
16+
1. `--config-dir` / `-c` flag
17+
2. `NERVE_CONFIG_DIR` environment variable
18+
3. The current directory, if it contains `config.yaml` or `config.local.yaml`
19+
4. The pointer file `~/.nerve/config_dir` (written by `nerve init` and on
20+
daemon start)
21+
5. The current directory (fresh-install fallback)
22+
23+
`nerve doctor` reports which directory was used and how it was found.
824

925
## Core
1026

@@ -42,9 +58,27 @@ Values in `config.local.yaml` are deep-merged on top of `config.yaml`.
4258
|-----|------|---------|-------------|
4359
| `telegram.enabled` | bool | `true` | Enable Telegram bot |
4460
| `telegram.bot_token` | string | - | Bot token from @BotFather |
45-
| `telegram.dm_policy` | string | `pairing` | `open` or `pairing` |
61+
| `telegram.dm_policy` | string | `pairing` | `pairing` (allowlist + one-time pairing codes) or `open` (anyone — dangerous) |
62+
| `telegram.allowed_users` | list[int] | `[]` | Telegram user IDs allowed to DM the bot |
4663
| `telegram.stream_mode` | string | `partial` | `partial` (edit msgs) or `full` |
4764

65+
### Pairing
66+
67+
With `dm_policy: pairing` (the default), the bot only talks to users in
68+
`allowed_users` and rejects everyone else. To authorize a user without
69+
editing config files:
70+
71+
1. Run `nerve pair` on the server — it prints a one-time 6-digit code
72+
(valid 1 hour). On a fresh install with no `allowed_users`, a code is
73+
also generated automatically at startup and printed to the log.
74+
2. Send the bot `/pair <code>` from the Telegram account to authorize.
75+
3. The user ID is appended to `telegram.allowed_users` in
76+
`config.local.yaml` and takes effect immediately.
77+
78+
An unauthorized `/start` gets a reply with the sender's numeric ID and
79+
pairing instructions (rate-limited); all other messages from unauthorized
80+
users are ignored.
81+
4882
## Quiet Hours
4983

5084
| Key | Type | Default | Description |

install.sh

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ get_python_version() {
6565

6666
trap 'error "Installation failed at line $LINENO. See above for details."' ERR
6767

68+
# A clean message on Ctrl+C / SIGTERM beats silent death: tell the user
69+
# what state they're in and how to continue.
70+
on_interrupt() {
71+
trap - INT TERM ERR
72+
printf "\n"
73+
warn "Interrupted."
74+
if [ "${INSTALL_DONE:-0}" = "1" ]; then
75+
info "Installation itself is complete — finish setup anytime with: nerve init"
76+
info "(setup answers are saved; it resumes where you left off)"
77+
else
78+
info "Re-run the installer to continue — completed steps are skipped."
79+
fi
80+
exit 130
81+
}
82+
trap on_interrupt INT TERM
83+
INSTALL_DONE=0
84+
6885
# --- OS Detection ---
6986

7087
detect_os() {
@@ -450,20 +467,66 @@ setup_path() {
450467

451468
# --- Run nerve init ---
452469

453-
run_init() {
454-
step "Running Nerve setup"
470+
INIT_COMPLETED=0
471+
INIT_SKIPPED=0
455472

473+
run_init() {
456474
local nerve_bin="$INSTALL_DIR/.venv/bin/nerve"
457475
local config_local="$INSTALL_DIR/config.local.yaml"
458476

459477
if [ "$IS_UPGRADE" = "1" ] && [ -f "$config_local" ]; then
460478
info "Existing configuration found — skipping setup wizard"
461479
info "Run 'nerve init' to reconfigure"
480+
INIT_COMPLETED=1
481+
return
482+
fi
483+
484+
# Clear boundary between installation and configuration: everything is
485+
# installed at this point — the wizard is optional and resumable.
486+
printf "\n"
487+
printf "${BOLD}${GREEN} Installation complete.${NC}\n"
488+
printf "\n"
489+
printf " Next: an interactive setup wizard configures your API keys,\n"
490+
printf " workspace, and channels. It takes a few minutes; Ctrl+C is\n"
491+
printf " safe — answers are saved and ${BOLD}nerve init${NC} resumes later.\n"
492+
printf "\n"
493+
494+
if ! confirm "Run the setup wizard now?"; then
495+
info "Skipping setup. Run it later with: nerve init"
496+
INIT_SKIPPED=1
462497
return
463498
fi
464499

500+
step "Running Nerve setup"
465501
cd "$INSTALL_DIR" || exit 1
466-
"$nerve_bin" init
502+
# Don't let a wizard abort look like a failed installation (set -e).
503+
if "$nerve_bin" init; then
504+
INIT_COMPLETED=1
505+
else
506+
printf "\n"
507+
warn "Setup did not finish. Installation itself is complete."
508+
info "Resume setup anytime with: nerve init"
509+
INIT_SKIPPED=1
510+
fi
511+
}
512+
513+
# --- Offer to start the daemon ---
514+
515+
offer_start() {
516+
# Only when freshly configured and interactive
517+
if [ "$INIT_COMPLETED" != "1" ] || [ "$IS_UPGRADE" = "1" ]; then
518+
return
519+
fi
520+
printf "\n"
521+
if ! confirm "Start Nerve now?"; then
522+
return
523+
fi
524+
local nerve_bin="$INSTALL_DIR/.venv/bin/nerve"
525+
if "$nerve_bin" start; then
526+
NERVE_STARTED=1
527+
else
528+
warn "Start failed — check logs with: nerve logs"
529+
fi
467530
}
468531

469532
# --- Summary ---
@@ -478,6 +541,14 @@ print_summary() {
478541
if [ "$IS_UPGRADE" = "1" ]; then
479542
printf " ${BOLD}Upgrade complete.${NC} Restart to apply changes:\n"
480543
printf " ${CYAN}nerve restart${NC}\n"
544+
elif [ "${NERVE_STARTED:-0}" = "1" ]; then
545+
printf " ${BOLD}Nerve is running.${NC}\n"
546+
printf " ${CYAN}http://localhost:8900${NC} Open the web UI\n"
547+
printf " ${CYAN}nerve logs${NC} Follow daemon logs\n"
548+
elif [ "$INIT_SKIPPED" = "1" ]; then
549+
printf " ${BOLD}Finish setup, then start:${NC}\n"
550+
printf " ${CYAN}nerve init${NC} Resume the setup wizard\n"
551+
printf " ${CYAN}nerve start${NC} Start as daemon\n"
481552
else
482553
printf " ${BOLD}Get started:${NC}\n"
483554
printf " ${CYAN}nerve start${NC} Start as daemon\n"
@@ -502,6 +573,7 @@ print_summary() {
502573
warn "nerve is not yet on PATH in this shell session"
503574
printf " Run: ${BOLD}source ~/.bashrc${NC} (or restart your terminal)\n\n"
504575
fi
576+
printf " ${DIM}Installer finished — you can close this terminal.${NC}\n\n"
505577
}
506578

507579
# --- Usage ---
@@ -564,8 +636,11 @@ main() {
564636
setup_python_env
565637
build_web_ui
566638
setup_path
639+
INSTALL_DONE=1
567640
run_init
641+
offer_start
568642
print_summary
643+
exit 0
569644
}
570645

571646
main "$@"

0 commit comments

Comments
 (0)