Compare commits

...
Author SHA1 Message Date
Nicolò Boschi d25222ba9c fix: embed get-skill installer 2025-12-23 10:33:23 +01:00
4 changed files with 177 additions and 123 deletions
+27 -15
View File
@@ -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 </dev/tty
fi
app_choice=${app_choice:-1}
case $app_choice in
1) APP="claude" ;;
2) APP="opencode" ;;
3) APP="codex" ;;
*) APP="claude" ;;
esac
echo ""
case $app_choice in
1) APP="claude" ;;
2) APP="opencode" ;;
3) APP="codex" ;;
*) APP="claude" ;;
esac
echo ""
else
# Non-interactive (CI) - default to claude
APP="claude"
print_info "Non-interactive mode detected, defaulting to Claude Code"
fi
fi
# Get skills directory for selected app
+145 -65
View File
@@ -83,32 +83,141 @@ def get_config():
}
def do_configure(args):
"""Interactive configuration setup with beautiful TUI."""
import questionary
from questionary import Style
# Provider defaults: (provider_id, default_model, env_key_name)
PROVIDER_DEFAULTS = {
"openai": ("openai", "o3-mini", "OPENAI_API_KEY"),
"groq": ("groq", "openai/gpt-oss-20b", "GROQ_API_KEY"),
"google": ("google", "gemini-2.0-flash", "GOOGLE_API_KEY"),
"ollama": ("ollama", "llama3.2", None),
}
def do_configure(args):
"""Interactive configuration setup."""
# If stdin is not a terminal (e.g., running via curl | bash),
# reopen stdin from /dev/tty for interactive prompts
# redirect stdin from /dev/tty for interactive prompts
original_stdin = None
if not sys.stdin.isatty():
try:
original_stdin = sys.stdin
sys.stdin = open('/dev/tty', 'r')
except OSError:
print("Error: Cannot run interactive configuration without a terminal.", file=sys.stderr)
print("Run directly: uvx hindsight-embed configure", file=sys.stderr)
return 1
# No terminal available - try non-interactive mode with env vars
return _do_configure_from_env()
# Custom style for the prompts
custom_style = Style([
('qmark', 'fg:cyan bold'),
('question', 'fg:white bold'),
('answer', 'fg:cyan'),
('pointer', 'fg:cyan bold'),
('highlighted', 'fg:cyan bold'),
('selected', 'fg:green'),
('text', 'fg:white'),
])
try:
return _do_configure_interactive()
finally:
if original_stdin is not None:
sys.stdin.close()
sys.stdin = original_stdin
def _do_configure_from_env():
"""Non-interactive configuration from environment variables (for CI)."""
# Check for required environment variables
api_key = os.environ.get("HINDSIGHT_EMBED_LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
provider = os.environ.get("HINDSIGHT_EMBED_LLM_PROVIDER", "openai")
if provider not in PROVIDER_DEFAULTS:
print(f"Error: Unknown provider '{provider}'. Supported: {', '.join(PROVIDER_DEFAULTS.keys())}", file=sys.stderr)
return 1
_, default_model, env_key = PROVIDER_DEFAULTS[provider]
# Check for API key (required for non-ollama providers)
if not api_key and provider != "ollama":
print("Error: Cannot run interactive configuration without a terminal.", file=sys.stderr)
print("", file=sys.stderr)
print("For non-interactive (CI) mode, set environment variables:", file=sys.stderr)
print(f" HINDSIGHT_EMBED_LLM_API_KEY=<your-api-key>", file=sys.stderr)
print(f" HINDSIGHT_EMBED_LLM_PROVIDER={provider} # optional, default: openai", file=sys.stderr)
print(f" HINDSIGHT_EMBED_LLM_MODEL=<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
-1
View File
@@ -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",
]
Generated
+5 -42
View File
@@ -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"