From 0b352d1bfa34eafa901efffa1e3a31c99e51690b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Tue, 23 Dec 2025 10:36:36 +0100 Subject: [PATCH] fix: embed get-skill installer (#69) --- hindsight-docs/static/get-skill | 42 +++-- hindsight-embed/hindsight_embed/cli.py | 210 +++++++++++++++++-------- hindsight-embed/pyproject.toml | 1 - uv.lock | 47 +----- 4 files changed, 177 insertions(+), 123 deletions(-) diff --git a/hindsight-docs/static/get-skill b/hindsight-docs/static/get-skill index 3417ac54..51d03a45 100755 --- a/hindsight-docs/static/get-skill +++ b/hindsight-docs/static/get-skill @@ -193,22 +193,34 @@ print_banner # Validate app parameter if [ -z "$APP" ]; then - echo -e "${DIM}Select your AI coding assistant:${NC}" - echo "" - echo " ${BOLD}1)${NC} Claude Code" - echo " ${BOLD}2)${NC} OpenCode" - echo " ${BOLD}3)${NC} Codex CLI" - echo "" - read -p "Enter choice [1]: " app_choice - app_choice=${app_choice:-1} + # Check if we have a terminal for interactive prompts + if [ -t 0 ] || [ -e /dev/tty ]; then + echo -e "${DIM}Select your AI coding assistant:${NC}" + echo "" + echo " ${BOLD}1)${NC} Claude Code" + echo " ${BOLD}2)${NC} OpenCode" + echo " ${BOLD}3)${NC} Codex CLI" + echo "" + # Use /dev/tty for input if stdin is piped + if [ -t 0 ]; then + read -p "Enter choice [1]: " app_choice + else + read -p "Enter choice [1]: " app_choice ", file=sys.stderr) + print(f" HINDSIGHT_EMBED_LLM_PROVIDER={provider} # optional, default: openai", file=sys.stderr) + print(f" HINDSIGHT_EMBED_LLM_MODEL= # optional, default: {default_model}", file=sys.stderr) + return 1 + + model = os.environ.get("HINDSIGHT_EMBED_LLM_MODEL", default_model) + bank_id = os.environ.get("HINDSIGHT_EMBED_BANK_ID", "default") + + print() + print("\033[1m\033[36m Hindsight Embed - Non-interactive Configuration\033[0m") + print() + print(f" \033[2mProvider:\033[0m {provider}") + print(f" \033[2mModel:\033[0m {model}") + print(f" \033[2mBank ID:\033[0m {bank_id}") + + # Save configuration + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + with open(CONFIG_FILE, "w") as f: + f.write("# Hindsight Embed Configuration\n") + f.write(f"# Generated by hindsight-embed configure (non-interactive)\n\n") + f.write(f"HINDSIGHT_EMBED_LLM_PROVIDER={provider}\n") + f.write(f"HINDSIGHT_EMBED_LLM_MODEL={model}\n") + f.write(f"HINDSIGHT_EMBED_BANK_ID={bank_id}\n") + if api_key: + f.write(f"HINDSIGHT_EMBED_LLM_API_KEY={api_key}\n") + + CONFIG_FILE.chmod(0o600) + + print() + print("\033[32m ✓ Configuration saved!\033[0m") + print() + + return 0 + + +def _prompt_choice(prompt: str, choices: list[tuple[str, str]], default: int = 1) -> str | None: + """Simple choice prompt that works with /dev/tty.""" + print(f"\033[1m{prompt}\033[0m") + print() + for i, (label, _) in enumerate(choices, 1): + print(f" \033[36m{i})\033[0m {label}") + print() + try: + response = input(f"Enter choice [{default}]: ").strip() + if not response: + return choices[default - 1][1] + idx = int(response) + if 1 <= idx <= len(choices): + return choices[idx - 1][1] + return choices[default - 1][1] + except (ValueError, EOFError, KeyboardInterrupt): + return None + + +def _prompt_text(prompt: str, default: str = "") -> str | None: + """Simple text prompt.""" + try: + suffix = f" [{default}]" if default else "" + response = input(f"\033[1m{prompt}\033[0m{suffix}: ").strip() + return response if response else default + except (EOFError, KeyboardInterrupt): + return None + + +def _prompt_password(prompt: str) -> str | None: + """Simple password prompt.""" + import getpass + try: + return getpass.getpass(f"\033[1m{prompt}\033[0m: ") + except (EOFError, KeyboardInterrupt): + return None + + +def _prompt_confirm(prompt: str, default: bool = True) -> bool | None: + """Simple yes/no prompt.""" + suffix = "[Y/n]" if default else "[y/N]" + try: + response = input(f"\033[1m{prompt}\033[0m {suffix}: ").strip().lower() + if not response: + return default + return response in ('y', 'yes') + except (EOFError, KeyboardInterrupt): + return None + + +def _do_configure_interactive(): + """Internal interactive configuration.""" print() print("\033[1m\033[36m ╭─────────────────────────────────────╮\033[0m") print("\033[1m\033[36m │ Hindsight Embed Configuration │\033[0m") @@ -117,82 +226,53 @@ def do_configure(args): # Check existing config if CONFIG_FILE.exists(): - if not questionary.confirm( - "Existing configuration found. Reconfigure?", - default=False, - style=custom_style, - ).ask(): + if not _prompt_confirm("Existing configuration found. Reconfigure?", default=False): print("\n\033[32m✓\033[0m Keeping existing configuration.") return 0 print() - # Provider selection with descriptions + # Provider selection providers = [ - questionary.Choice("OpenAI (recommended)", value=("openai", "o3-mini", "OpenAI")), - questionary.Choice("Groq (fast & free tier)", value=("groq", "openai/gpt-oss-20b", "Groq")), - questionary.Choice("Google Gemini", value=("google", "gemini-2.0-flash", "Google")), - questionary.Choice("Ollama (local, no API key)", value=("ollama", "llama3.2", None)), + ("OpenAI (recommended)", "openai"), + ("Groq (fast & free tier)", "groq"), + ("Google Gemini", "google"), + ("Ollama (local, no API key)", "ollama"), ] - result = questionary.select( - "Select your LLM provider:", - choices=providers, - style=custom_style, - ).ask() - - if result is None: # User cancelled + provider = _prompt_choice("Select your LLM provider:", providers, default=1) + if provider is None: print("\n\033[33m⚠\033[0m Configuration cancelled.") return 1 - provider, default_model, key_name = result + _, default_model, env_key = PROVIDER_DEFAULTS[provider] + print() # API key api_key = "" - if key_name: - env_keys = { - "OpenAI": "OPENAI_API_KEY", - "Groq": "GROQ_API_KEY", - "Google": "GOOGLE_API_KEY", - } - env_key = env_keys.get(key_name, "") + if env_key: existing = os.environ.get(env_key, "") if existing: masked = existing[:8] + "..." + existing[-4:] if len(existing) > 12 else "***" - if questionary.confirm( - f"Found {key_name} key in ${env_key} ({masked}). Use it?", - default=True, - style=custom_style, - ).ask(): + if _prompt_confirm(f"Found API key in ${env_key} ({masked}). Use it?", default=True): api_key = existing + print() if not api_key: - api_key = questionary.password( - f"Enter your {key_name} API key:", - style=custom_style, - ).ask() - + api_key = _prompt_password("Enter your API key") if not api_key: print("\n\033[31m✗\033[0m API key is required.", file=sys.stderr) return 1 + print() # Model selection - model = questionary.text( - "Model name:", - default=default_model, - style=custom_style, - ).ask() - + model = _prompt_text("Model name", default=default_model) if model is None: return 1 + print() # Bank ID - bank_id = questionary.text( - "Memory bank ID:", - default="default", - style=custom_style, - ).ask() - + bank_id = _prompt_text("Memory bank ID", default="default") if bank_id is None: return 1 diff --git a/hindsight-embed/pyproject.toml b/hindsight-embed/pyproject.toml index f60da315..e07b3b35 100644 --- a/hindsight-embed/pyproject.toml +++ b/hindsight-embed/pyproject.toml @@ -9,7 +9,6 @@ description = "Hindsight embedded CLI - local memory operations without a server readme = "README.md" requires-python = ">=3.11" dependencies = [ - "questionary>=2.0.0", "httpx>=0.27.0", ] diff --git a/uv.lock b/uv.lock index c91d974a..3fc3c065 100644 --- a/uv.lock +++ b/uv.lock @@ -1292,7 +1292,7 @@ wheels = [ [[package]] name = "hindsight-all" -version = "0.1.12" +version = "0.1.13" source = { editable = "hindsight" } dependencies = [ { name = "hindsight-api" }, @@ -1316,7 +1316,7 @@ provides-extras = ["test"] [[package]] name = "hindsight-api" -version = "0.1.12" +version = "0.1.13" source = { editable = "hindsight-api" } dependencies = [ { name = "alembic" }, @@ -1422,7 +1422,7 @@ dev = [ [[package]] name = "hindsight-client" -version = "0.1.12" +version = "0.1.13" source = { editable = "hindsight-clients/python" } dependencies = [ { name = "aiohttp" }, @@ -1456,7 +1456,7 @@ provides-extras = ["test"] [[package]] name = "hindsight-dev" -version = "0.1.12" +version = "0.1.13" source = { editable = "hindsight-dev" } dependencies = [ { name = "hindsight-api" }, @@ -1495,14 +1495,10 @@ version = "0.1.0" source = { editable = "hindsight-embed" } dependencies = [ { name = "httpx" }, - { name = "questionary" }, ] [package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.27.0" }, - { name = "questionary", specifier = ">=2.0.0" }, -] +requires-dist = [{ name = "httpx", specifier = ">=0.27.0" }] [[package]] name = "httpcore" @@ -2915,18 +2911,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, ] -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 }, -] - [[package]] name = "propcache" version = "0.4.1" @@ -3632,18 +3616,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, ] -[[package]] -name = "questionary" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "prompt-toolkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753 }, -] - [[package]] name = "redis" version = "7.1.0" @@ -4965,15 +4937,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 }, ] -[[package]] -name = "wcwidth" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 }, -] - [[package]] name = "websockets" version = "15.0.1"