diff --git a/honcho-cli/CHANGELOG.md b/honcho-cli/CHANGELOG.md index 1a1b50e3..866dcc65 100644 --- a/honcho-cli/CHANGELOG.md +++ b/honcho-cli/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Changed + +- `--setup` API key prompts echo `*` per character so a paste is visibly received instead of a blank getpass field + ### Fixed -- `--setup` for openai-compatible writes `EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL` into the profile `.env` alongside `LLM_OPENAI_BASE_URL` +- `--setup` for openai-compatible writes `EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL` into the profile `.env` alongside `LLM_OPENAI_BASE_URL` and visible API key paste. ## [0.1.3] - 2026-08-25 diff --git a/honcho-cli/src/honcho_cli/local/setup.py b/honcho-cli/src/honcho_cli/local/setup.py index 242f47d6..5922dfd4 100644 --- a/honcho-cli/src/honcho_cli/local/setup.py +++ b/honcho-cli/src/honcho_cli/local/setup.py @@ -7,6 +7,7 @@ only — the start command rejects ``--setup`` in JSON / non-TTY mode. from __future__ import annotations +import sys import tomllib from dataclasses import dataclass from pathlib import Path @@ -445,13 +446,7 @@ def _prompt_secret(label: str, current: str | None) -> str: if choice != "2": return current _console.print(f" [dim]{label}[/dim]") - raw = typer.prompt( - f" {label}", - default="", - show_default=False, - hide_input=True, - prompt_suffix=": ", - ).strip() + raw = _prompt_masked(f" {label}: ").strip() if not raw or is_placeholder_key(raw): print_error( "MISSING_LLM_KEY", @@ -461,6 +456,69 @@ def _prompt_secret(label: str, current: str | None) -> str: return raw +def _prompt_masked(prompt: str) -> str: + """Read a secret, echoing ``*`` per character so paste is visibly received.""" + stream = sys.stderr + stream.write(prompt) + stream.flush() + chars: list[str] = [] + + def _write(text: str) -> None: + stream.write(text) + stream.flush() + + def _feed(ch: str) -> bool: + """Return True when input is complete.""" + if not ch or ch in ("\n", "\r", "\x04"): + _write("\n") + return True + if ch in ("\x7f", "\x08"): + if chars: + chars.pop() + _write("\b \b") + return False + if ch == "\x1b": + return False + if ch.isprintable(): + chars.append(ch) + _write("*") + return False + + if sys.platform == "win32": + import msvcrt + + while True: + ch = msvcrt.getwch() + if ch in ("\x00", "\xe0"): + msvcrt.getwch() + continue + if _feed(ch): + return "".join(chars) + + import termios + import tty + + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setcbreak(fd) + while True: + ch = sys.stdin.read(1) + if ch == "\x1b": + nxt = sys.stdin.read(1) + if nxt == "[": + while True: + seq = sys.stdin.read(1) + if not seq or "@" <= seq <= "~": + break + continue + if _feed(ch): + return "".join(chars) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + return "".join(chars) + + def _redact(key: str) -> str: if len(key) <= 4: return "***"