diff --git a/README.md b/README.md index 9b6373c..bc53c22 100644 --- a/README.md +++ b/README.md @@ -24,26 +24,36 @@ flowchart LR ## Install ```sh -curl -sL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh | sh ``` -The installer requires Python 3.11+, prefers `pipx`, and falls back to user-site `pip`. From source: +This creates or updates a user-level `nomnom` command. The installer tries `uv tool install` first, +then pipx, then a non-virtualenv Python 3.11+ user-site install. It verifies the actual executable, +`nomnom --version`, and `nomnom doctor --json` in a sanitized user/system-only environment. For +bootstrap verification, it deliberately uses `$HOME/.config`: inherited agent XDG roots and every +`NOMNOM_*` override are ignored. It never opens the meal database, cache, or aliases. + +For machine-readable agent output: ```sh -git clone https://github.com/maxjustships/nomnomcli -cd nomnomcli -python3 -m pip install -e . -nomnom --version -nomnom setup -nomnom doctor --json +curl -fsSL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh \ + | sh -s -- --status-json +``` + +The structured `status` is `installed_and_ready`, `installed_needs_provider_setup`, +`installed_path_repair_needed`, or `error`, with the executable, version, and a concrete repair or +error action when needed. No credential value is emitted. `--dry-run` remains available. + +Check the optional generic-food connection without a prompt: + +```sh +nomnom setup --status --json ``` -Run `nomnom setup` in an interactive terminal before the first food log. It independently probes -Open Food Facts product/barcode lookup and full-text resolution, explains that OFF needs no key, -shows the official USDA signup URL, validates the USDA key with a minimal FoodData Central request, -and only then stores it locally. Follow with `nomnom doctor --json` to verify current provider -readiness instead of assuming setup worked. OFF product reachability does not imply that full-text -resolution is ready. +If it reports `setup_required`, run `nomnom setup` once in your own interactive terminal. Base +product/barcode and existing local-cache capture still work when this is deferred. Setup explains +why no-label generic-food lookup needs USDA, links to the official free signup page, validates the +key before saving, and prints a final `Connected` receipt. It never opens a browser by itself. USDA credentials are local user configuration, never repository or database content. The default path is `$XDG_CONFIG_HOME/nomnomcli/config.toml` or `~/.config/nomnomcli/config.toml`, written with @@ -93,9 +103,10 @@ apply to every v1 candidate. ### Enable USDA fallback -Run the guided flow (recommended): +Check first, then run the one-time guided connection only if needed: ```sh +nomnom setup --status --json nomnom setup nomnom doctor --json ``` diff --git a/install.sh b/install.sh old mode 100755 new mode 100644 index 4e87108..1ba3ba4 --- a/install.sh +++ b/install.sh @@ -1,70 +1,456 @@ #!/bin/sh -set -eu +set -u REPO_URL="git+https://github.com/maxjustships/nomnomcli" SKILL_URL="https://raw.githubusercontent.com/maxjustships/nomnomcli/main/skill/SKILL.md" DRY_RUN=0 +JSON_OUTPUT=0 +STATUS="" +EXECUTABLE="" +VERSION="" +ERROR_CODE="" +ERROR_MESSAGE="" +ERROR_ACTION="" +PATH_REPAIR="" usage() { - printf '%s\n' "Usage: sh install.sh [--dry-run]" + printf '%s\n' "Usage: sh install.sh [--dry-run] [--json|--status-json]" } -if [ "${1:-}" = "--dry-run" ]; then - DRY_RUN=1 +while [ "$#" -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + --json|--status-json) JSON_OUTPUT=1 ;; + -h|--help) + usage + exit 0 + ;; + *) + usage >&2 + exit 2 + ;; + esac shift -fi -if [ "$#" -ne 0 ]; then - usage >&2 - exit 2 -fi +done say() { - printf '%s\n' "$*" + [ "$JSON_OUTPUT" -eq 0 ] && printf '%s\n' "$*" +} + +note() { + if [ "$JSON_OUTPUT" -eq 1 ]; then + printf '%s\n' "$*" >&2 + else + printf '%s\n' "$*" + fi +} + +json_quote() { + printf '%s' "$1" | awk ' + BEGIN { printf "\"" } + { + if (NR > 1) printf "\\n" + gsub(/\\/, "\\\\") + gsub(/"/, "\\\"") + gsub(/\r/, "\\r") + gsub(/\t/, "\\t") + printf "%s", $0 + } + END { printf "\"" } + ' +} + +json_value_or_null() { + if [ -n "$1" ]; then + json_quote "$1" + else + printf 'null' + fi +} + +emit_status() { + if [ "$JSON_OUTPUT" -eq 1 ]; then + printf '{"status":' + json_quote "$STATUS" + printf ',"executable":' + json_value_or_null "$EXECUTABLE" + printf ',"version":' + json_value_or_null "$VERSION" + printf ',"error":' + if [ -n "$ERROR_CODE" ]; then + printf '{"code":' + json_quote "$ERROR_CODE" + printf ',"message":' + json_quote "$ERROR_MESSAGE" + printf ',"action":' + json_quote "$ERROR_ACTION" + printf '}' + else + printf 'null' + fi + printf ',"path_repair":' + json_value_or_null "$PATH_REPAIR" + printf '}\n' + fi +} + +fail() { + STATUS="error" + ERROR_CODE=$1 + ERROR_MESSAGE=$2 + ERROR_ACTION=$3 + if [ "$JSON_OUTPUT" -eq 0 ]; then + printf 'Error: %s\nAction: %s\n' "$ERROR_MESSAGE" "$ERROR_ACTION" >&2 + fi + emit_status + exit 1 } -run() { - if [ "$DRY_RUN" -eq 1 ]; then - say "[dry-run] $*" +run_install() { + if [ "$JSON_OUTPUT" -eq 1 ]; then + "$@" >&2 else "$@" fi } -if [ "$DRY_RUN" -eq 0 ]; then - if ! command -v python3 >/dev/null 2>&1; then - say "Error: Python 3.11+ is required." >&2 - exit 1 +if [ "$DRY_RUN" -eq 1 ]; then + say "[dry-run] prefer: uv tool install --force $REPO_URL" + say "[dry-run] fallback: pipx install --force $REPO_URL" + say "[dry-run] fallback: system Python 3.11+ -m pip install --user --upgrade $REPO_URL" + say "[dry-run] discover the user tool executable directory and normal login-shell PATH" + say "[dry-run] verify nomnom --version with a sanitized user/system PATH" + say "[dry-run] verify and parse nomnom doctor --json before the first food log" + say "Base product/barcode capture works; to enable no-label generic-food lookup, one free USDA setup remains." + say "Voluntary one-time action: run 'nomnom setup' in your own terminal." + if [ "$JSON_OUTPUT" -eq 1 ]; then + STATUS="dry_run" + emit_status fi - if ! python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 11))'; then - say "Error: Python 3.11+ is required." >&2 - exit 1 + exit 0 +fi + +INSTALL_METHOD="" +INSTALL_BIN_DIR="" +INSTALL_FAILURES="" + +if command -v uv >/dev/null 2>&1; then + if run_install uv tool install --force "$REPO_URL"; then + INSTALL_METHOD="uv" + INSTALL_BIN_DIR=$(uv tool dir --bin 2>/dev/null || true) + [ -n "$INSTALL_BIN_DIR" ] || INSTALL_BIN_DIR=${UV_TOOL_BIN_DIR:-"$HOME/.local/bin"} + else + INSTALL_FAILURES="uv" + note "uv tool install failed; trying the next user-level installer." fi -else - say "[dry-run] check for Python 3.11+" fi -if command -v pipx >/dev/null 2>&1; then - run pipx install --force "$REPO_URL" -else - say "pipx not found; using the user-site pip fallback." - run python3 -m pip install --user --upgrade "$REPO_URL" +if [ -z "$INSTALL_METHOD" ] && command -v pipx >/dev/null 2>&1; then + if run_install pipx install --force "$REPO_URL"; then + INSTALL_METHOD="pipx" + INSTALL_BIN_DIR=$(pipx environment --value PIPX_BIN_DIR 2>/dev/null || true) + [ -n "$INSTALL_BIN_DIR" ] || INSTALL_BIN_DIR=${PIPX_BIN_DIR:-"$HOME/.local/bin"} + else + INSTALL_FAILURES="${INSTALL_FAILURES:+$INSTALL_FAILURES,}pipx" + note "pipx install failed; trying the system-Python user-site fallback." + fi fi -if [ -d "${HOME}/.hermes" ]; then - SKILL_DIR="${HOME}/.hermes/skills/nomnomcli" - run mkdir -p "$SKILL_DIR" - if [ -f "skill/SKILL.md" ]; then - run cp "skill/SKILL.md" "$SKILL_DIR/SKILL.md" - elif command -v curl >/dev/null 2>&1; then - run curl -fsSL "$SKILL_URL" -o "$SKILL_DIR/SKILL.md" +append_path() { + _append_current=$1 + _append_dir=$2 + if [ -z "$_append_dir" ]; then + printf '%s' "$_append_current" + elif [ -z "$_append_current" ]; then + printf '%s' "$_append_dir" else - say "Warning: curl is required to install the Hermes skill." >&2 + case ":$_append_current:" in + *":$_append_dir:"*) printf '%s' "$_append_current" ;; + *) printf '%s:%s' "$_append_current" "$_append_dir" ;; + esac + fi +} + +python_search_path() { + _python_result="" + _python_old_ifs=$IFS + IFS=: + for _python_dir in ${PATH:-}; do + [ -n "$_python_dir" ] || continue + if [ -n "${VIRTUAL_ENV:-}" ]; then + case "$_python_dir" in + "$VIRTUAL_ENV"|"$VIRTUAL_ENV"/*) continue ;; + esac + fi + case "$_python_dir" in + *"/.venv"|*"/.venv/"*|*"/venv/"*|*"/.hermes/"*|*"/.codex/"*) continue ;; + esac + _python_result=$(append_path "$_python_result" "$_python_dir") + done + IFS=$_python_old_ifs + printf '%s' "$_python_result" +} + +find_system_python() { + _find_path=$(python_search_path) + _find_seen="" + _find_old_ifs=$IFS + IFS=: + for _find_dir in $_find_path; do + for _find_name in python3 python3.14 python3.13 python3.12 python3.11; do + _find_candidate="$_find_dir/$_find_name" + [ -x "$_find_candidate" ] || continue + case ":$_find_seen:" in + *":$_find_candidate:"*) continue ;; + esac + _find_seen="${_find_seen:+$_find_seen:}$_find_candidate" + if ! /usr/bin/env -u VIRTUAL_ENV -u PYTHONPATH -u PYTHONHOME \ + "$_find_candidate" -c ' +import pip # noqa: F401 +import sys +base_prefix = getattr(sys, "base_prefix", sys.prefix) +raise SystemExit(0 if sys.version_info >= (3, 11) and sys.prefix == base_prefix else 1) +' >/dev/null 2>&1; then + continue + fi + _find_resolved=$(/usr/bin/env -u VIRTUAL_ENV -u PYTHONPATH -u PYTHONHOME \ + "$_find_candidate" -c ' +import os, sys +print(os.path.realpath(sys.executable)) +' 2>/dev/null || true) + [ -n "$_find_resolved" ] || _find_resolved=$_find_candidate + if [ -n "${VIRTUAL_ENV:-}" ]; then + case "$_find_resolved" in + "$VIRTUAL_ENV"|"$VIRTUAL_ENV"/*) continue ;; + esac + fi + IFS=$_find_old_ifs + printf '%s' "$_find_candidate" + return 0 + done + done + IFS=$_find_old_ifs + return 1 +} + +if [ -z "$INSTALL_METHOD" ]; then + SYSTEM_PYTHON=$(find_system_python || true) + if [ -z "$SYSTEM_PYTHON" ]; then + fail \ + "system_python_not_found" \ + "No non-virtualenv system Python 3.11+ was found for the user-site fallback." \ + "Install uv or pipx, or install Python 3.11+ in your normal shell PATH, then rerun this installer." + fi + note "No isolated tool installer completed; using the system-Python user-site fallback." + if ! run_install /usr/bin/env \ + -u VIRTUAL_ENV -u PYTHONPATH -u PYTHONHOME -u PIP_REQUIRE_VIRTUALENV \ + "$SYSTEM_PYTHON" -m pip install --user --upgrade "$REPO_URL"; then + fail \ + "installation_failed" \ + "The system-Python user-site installation failed${INSTALL_FAILURES:+ after $INSTALL_FAILURES failed}." \ + "Review the installer output, ensure Git and network access are available, then rerun." fi + INSTALL_METHOD="user-site" + INSTALL_BIN_DIR=$(/usr/bin/env -u VIRTUAL_ENV -u PYTHONPATH -u PYTHONHOME \ + "$SYSTEM_PYTHON" -c ' +import sysconfig +print(sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user"))) +' 2>/dev/null || true) + [ -n "$INSTALL_BIN_DIR" ] || INSTALL_BIN_DIR="$HOME/.local/bin" +fi + +CANDIDATE_DIRS="" +for _candidate_dir in \ + "$INSTALL_BIN_DIR" \ + "${UV_TOOL_BIN_DIR:-}" \ + "${PIPX_BIN_DIR:-}" \ + "${XDG_BIN_HOME:-}" \ + "$HOME/.local/bin" \ + "$HOME/bin"; do + [ -n "$_candidate_dir" ] || continue + CANDIDATE_DIRS=$(append_path "$CANDIDATE_DIRS" "$_candidate_dir") +done + +_candidate_old_ifs=$IFS +IFS=: +for _candidate_dir in $CANDIDATE_DIRS; do + if [ -x "$_candidate_dir/nomnom" ]; then + EXECUTABLE="$_candidate_dir/nomnom" + break + fi +done +IFS=$_candidate_old_ifs + +if [ -z "$EXECUTABLE" ]; then + fail \ + "executable_not_found" \ + "$INSTALL_METHOD completed but no user-level nomnom executable was found." \ + "Inspect $INSTALL_BIN_DIR and rerun with --status-json for structured diagnostics." +fi + +normal_login_path() { + _login_base="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/local/bin" + _login_shell=${SHELL:-/bin/sh} + if [ ! -x "$_login_shell" ]; then + printf '%s' "$_login_base" + return + fi + _login_environment=$(/usr/bin/env -i \ + HOME="$HOME" \ + USER="${USER:-}" \ + LOGNAME="${LOGNAME:-${USER:-}}" \ + SHELL="$_login_shell" \ + PATH="$_login_base" \ + "$_login_shell" -lc '/usr/bin/env' 2>/dev/null || true) + _login_path=$(printf '%s\n' "$_login_environment" | awk ' + /^PATH=/ { sub(/^PATH=/, ""); path = $0 } + END { printf "%s", path } + ') + if [ -n "$_login_path" ]; then + printf '%s' "$_login_path" + else + printf '%s' "$_login_base" + fi +} + +sanitize_normal_path() { + _sanitize_source=$1 + _sanitize_result="" + _sanitize_old_ifs=$IFS + IFS=: + for _sanitize_dir in $_sanitize_source; do + [ -n "$_sanitize_dir" ] || continue + if [ -n "${VIRTUAL_ENV:-}" ]; then + case "$_sanitize_dir" in + "$VIRTUAL_ENV"|"$VIRTUAL_ENV"/*) continue ;; + esac + fi + case "$_sanitize_dir" in + *"/.venv"|*"/.venv/"*|*"/venv/"*|*"/.hermes/"*|*"/.codex/"*) continue ;; + esac + case "$_sanitize_dir" in + "$INSTALL_BIN_DIR"|"${UV_TOOL_BIN_DIR:-__unset__}"|"${PIPX_BIN_DIR:-__unset__}"|\ + "$HOME/.local/bin"|"$HOME/bin"|"${XDG_BIN_HOME:-__unset__}"|\ + /bin|/bin/*|/sbin|/sbin/*|/usr/bin|/usr/bin/*|/usr/sbin|/usr/sbin/*|\ + /usr/local/bin|/usr/local/bin/*|/opt/homebrew/bin|/opt/homebrew/bin/*|\ + /opt/local/bin|/opt/local/bin/*) + _sanitize_result=$(append_path "$_sanitize_result" "$_sanitize_dir") + ;; + esac + done + IFS=$_sanitize_old_ifs + printf '%s' "$_sanitize_result" +} + +LOGIN_PATH=$(normal_login_path) +SANITIZED_LOGIN_PATH=$(sanitize_normal_path "$LOGIN_PATH") +VERIFY_PATH=$(append_path "$INSTALL_BIN_DIR" "$SANITIZED_LOGIN_PATH") + +# Bootstrap verification observes only the target user's default configuration. +# Do not inherit an invoking agent's XDG roots or NOMNOM_* overrides/secrets. +run_verification() { + /usr/bin/env -i \ + HOME="$HOME" \ + USER="${USER:-}" \ + LOGNAME="${LOGNAME:-${USER:-}}" \ + SHELL="${SHELL:-/bin/sh}" \ + PATH="$VERIFY_PATH" \ + XDG_CONFIG_HOME="$HOME/.config" \ + "$@" +} + +VERIFIED_EXECUTABLE=$(PATH="$VERIFY_PATH" command -v nomnom 2>/dev/null || true) +if [ -z "$VERIFIED_EXECUTABLE" ]; then + fail \ + "verification_failed" \ + "nomnom is not executable under the sanitized user/system verification PATH." \ + "Ensure $INSTALL_BIN_DIR is executable and rerun the installer." +fi + +if ! VERSION=$(run_verification nomnom --version 2>&1); then + fail \ + "version_verification_failed" \ + "The installed nomnom executable failed --version verification." \ + "Run $EXECUTABLE --version and review the reported error." +fi + +if ! DOCTOR_OUTPUT=$(run_verification nomnom doctor --json 2>&1); then + fail \ + "doctor_verification_failed" \ + "The installed nomnom executable failed doctor JSON verification." \ + "Run $EXECUTABLE doctor --json and review the structured error." +fi + +doctor_usda_bool() { + _doctor_key=$1 + _doctor_python=$(awk ' + NR == 1 && /^#!/ { print substr($0, 3); exit } + ' "$EXECUTABLE") + [ -n "$_doctor_python" ] && [ -x "$_doctor_python" ] || return 1 + printf '%s' "$DOCTOR_OUTPUT" | /usr/bin/env -u VIRTUAL_ENV -u PYTHONPATH -u PYTHONHOME \ + "$_doctor_python" -c ' +import json +import sys + +try: + value = json.load(sys.stdin)["providers"]["usda"][sys.argv[1]] +except (json.JSONDecodeError, KeyError, TypeError): + raise SystemExit(1) +if type(value) is not bool: + raise SystemExit(1) +print(str(value).lower()) +' "$_doctor_key" +} + +USDA_CONFIGURED=$(doctor_usda_bool configured) +USDA_REACHABLE=$(doctor_usda_bool reachable) +if [ -z "$USDA_CONFIGURED" ] || [ -z "$USDA_REACHABLE" ]; then + fail \ + "doctor_json_invalid" \ + "nomnom doctor --json did not contain the expected USDA configured/reachable status." \ + "Run $EXECUTABLE doctor --json and reinstall if its schema is incompatible." +fi + +LOGIN_EXECUTABLE=$(PATH="$SANITIZED_LOGIN_PATH" command -v nomnom 2>/dev/null || true) +if [ "$LOGIN_EXECUTABLE" != "$EXECUTABLE" ]; then + STATUS="installed_path_repair_needed" + PATH_REPAIR="export PATH=\"$INSTALL_BIN_DIR:\$PATH\"" +elif [ "$USDA_CONFIGURED" = "true" ] && [ "$USDA_REACHABLE" = "true" ]; then + STATUS="installed_and_ready" else - say "Hermes directory not found; skipping agent skill installation." - say "Later, copy skill/SKILL.md to ~/.hermes/skills/nomnomcli/SKILL.md." + STATUS="installed_needs_provider_setup" +fi + +if [ -d "$HOME/.hermes" ]; then + SKILL_DIR="$HOME/.hermes/skills/nomnomcli" + if mkdir -p "$SKILL_DIR"; then + if [ -f "skill/SKILL.md" ]; then + cp "skill/SKILL.md" "$SKILL_DIR/SKILL.md" + elif command -v curl >/dev/null 2>&1; then + curl -fsSL "$SKILL_URL" -o "$SKILL_DIR/SKILL.md" || note "Warning: agent skill download failed." + else + note "Warning: curl is required to install the optional Hermes skill." + fi + else + note "Warning: the optional Hermes skill directory could not be created." + fi +fi + +if [ "$JSON_OUTPUT" -eq 0 ]; then + say "Installed: $EXECUTABLE" + say "Version: $VERSION" + say "Status: $STATUS" + if [ -n "$PATH_REPAIR" ]; then + say "One-time PATH repair: $PATH_REPAIR" + fi + if [ "$USDA_CONFIGURED" != "true" ] || [ "$USDA_REACHABLE" != "true" ]; then + say "Base product/barcode capture works; to enable no-label generic-food lookup, one free USDA setup remains." + if [ -t 1 ]; then + say "That connection is voluntary; do it when you are ready." + fi + say "One action: run 'nomnom setup' in your own terminal." + fi fi -say "nomnomcli installation complete." -say "Next: run 'nomnom setup' in an interactive terminal before the first food log." -say "Then verify provider readiness with 'nomnom doctor --json'." +emit_status +exit 0 diff --git a/nomnomcli/cli.py b/nomnomcli/cli.py index f71a92d..19fb247 100644 --- a/nomnomcli/cli.py +++ b/nomnomcli/cli.py @@ -11,7 +11,7 @@ from nomnomcli.errors import NomnomError from nomnomcli.foods import FoodRepository from nomnomcli.models import scale_food, total_items -from nomnomcli.onboarding import doctor_report, setup_providers +from nomnomcli.onboarding import doctor_report, setup_providers, setup_status_report from nomnomcli.parser import parse_free_text from nomnomcli.recipes import fetch_recipe, recipe_portion, save_recipe @@ -79,7 +79,13 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") commands = parser.add_subparsers(dest="command", required=True) - commands.add_parser("setup", help="configure and validate nutrition providers") + setup = commands.add_parser( + "setup", help="make the optional one-time generic-food provider connection" + ) + setup.add_argument( + "--status", action="store_true", help="report connection state without prompting" + ) + setup.add_argument("--json", action="store_true", help="machine-readable status JSON") doctor = commands.add_parser("doctor", help="probe provider readiness") doctor.add_argument("--json", action="store_true", help="machine-readable JSON output") @@ -163,9 +169,37 @@ def _build_parser() -> argparse.ArgumentParser: def _run(args: argparse.Namespace) -> int: if args.command == "setup": + if args.status: + result = setup_status_report() + if args.json: + print(_json_output(result)) + else: + usda = result["providers"]["usda"] + print(f"USDA connection: {result['status']}") + print(f"Purpose: {usda['purpose']}") + print(f"Official free signup: {usda['signup_url']}") + if usda["next_action"]: + print( + f"Next: {usda['next_action']['message']} " + f"({usda['next_action']['command']})" + ) + return 0 + if args.json: + raise NomnomError( + "invalid_arguments", + "--json is available with prompt-free setup status", + details={"action": "Run nomnom setup --status --json"}, + ) + print("One-time connection (optional)") + print( + "Base product/barcode capture works; to enable no-label generic-food lookup, " + "one free USDA setup remains." + ) print("Open Food Facts: free, no account or key; branded packaged foods.") print("USDA FoodData Central: free API key; generic/raw foods and fallback.") - print("USDA signup: https://fdc.nal.usda.gov/api-key-signup.html") + print("Official free USDA signup: https://fdc.nal.usda.gov/api-key-signup.html") + print("Enter the key privately below; it is hidden and never printed by nomnom.") + print("Validating the USDA connection before saving...") result = setup_providers(interactive=sys.stdin.isatty()) off_product_status = ( "reachable" @@ -184,6 +218,7 @@ def _run(args: argparse.Namespace) -> int: "USDA: reachable; key source " f"{result['providers']['usda']['key_source']}" ) + print("Connected: USDA no-label generic-food lookup is ready.") if result.get("config_path"): print(f"Saved secure provider config: {result['config_path']} (0600)") return 0 diff --git a/nomnomcli/onboarding.py b/nomnomcli/onboarding.py index 6247f3a..a1dec14 100644 --- a/nomnomcli/onboarding.py +++ b/nomnomcli/onboarding.py @@ -54,6 +54,43 @@ def doctor_report( } +def setup_status_report( + *, + config: ProviderConfig | None = None, + off_client: OpenFoodFactsClient | None = None, + usda_client: USDAClient | None = None, +) -> dict: + """Return prompt-free provider setup state without credential material.""" + report = doctor_report( + config=config, + off_client=off_client, + usda_client=usda_client, + ) + usda = report["providers"]["usda"] + usda.update( + { + "purpose": "no-label generic-food lookup", + "signup_url": USDA_SETUP_URL, + } + ) + if usda["configured"] and usda["reachable"]: + report["status"] = "connected" + usda["next_action"] = None + elif usda["configured"]: + report["status"] = "connection_unreachable" + usda["next_action"] = { + "command": "nomnom setup", + "message": "Run the one-time connection again to validate or replace the key.", + } + else: + report["status"] = "setup_required" + usda["next_action"] = { + "command": "nomnom setup", + "message": "Run the one-time connection in your terminal.", + } + return {"status": report.pop("status"), **report} + + def setup_providers( *, interactive: bool, diff --git a/skill/SKILL.md b/skill/SKILL.md index d2959de..1b33008 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -10,26 +10,46 @@ description: >- Use `nomnom` as the only source of nutrition numbers. Never estimate calories, macros, weights, or serving conversions in the agent context. -## Install +## Mandatory install protocol -If `nomnom --help` is unavailable, ask permission and run: +This protocol is required whenever `nomnom` is unavailable. Ask permission, then run the single +user-level bootstrap and parse its JSON: ```sh -curl -sL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh \ + | sh -s -- --status-json ``` -Before the first food log, run setup in the user's interactive terminal, then verify readiness: - -```sh -nomnom setup -nomnom doctor --json -``` - -Explain only actionable results. Open Food Facts is keyless. USDA signup is at -; setup validates before writing the key to the -owner-only XDG user config (`0600`). Credentials stay local and must never enter the repository, -database, shell history, agent transcript, or logs. `NOMNOM_USDA_KEY` is the non-interactive option -and overrides stored config. +Never run `pip install -e` (or any editable install) inside a Hermes/agent virtualenv, never run a +package installer from a Hermes/agent virtualenv, and never make an agent-private executable the +user's command. The bootstrap must select uv tool, pipx, or a non-venv Python 3.11+ user site itself. + +Follow this exact sequence: + +1. Parse the installer `status`, `executable`, `version`, `error`, and `path_repair` fields. Accept + only `installed_and_ready` or `installed_needs_provider_setup` as a completed shell install. For + `installed_path_repair_needed`, give the returned one-time repair command and do not claim the + install is complete. For `error`, explain its returned action. +2. Derive the executable directory from the returned path. Verify `nomnom --version` and run + `nomnom doctor --json` with a sanitized user/system-only environment containing that user + executable directory plus only ordinary locations such as `~/.local/bin`, `~/bin`, + `/usr/local/bin`, `/usr/bin`, `/bin`, `/opt/homebrew/bin`, and `/opt/local/bin`. For bootstrap + verification, set `XDG_CONFIG_HOME=$HOME/.config` and clear every `NOMNOM_*` override: never + inherit agent XDG roots, credentials, database paths, or status settings. Exclude Hermes, Codex, + the current project, temporary directories, and every virtualenv path. Parse the doctor JSON; do + not infer readiness from exit status or human text. +3. Run `nomnom setup --status --json` and parse its prompt-free result. If USDA is not configured + and reachable, say exactly once: "Base product/barcode capture works; to enable no-label generic-food lookup, one free USDA setup remains." Offer exactly one voluntary action: + `nomnom setup` in the user's own interactive terminal. Do not open a browser or run interactive + setup automatically. +4. Agents must never type, receive, echo, or persist a USDA key or any other user secret. Do not ask + for it in chat. Secret entry belongs only in the user's terminal through `nomnom setup`, which + links to , validates the key, and stores it in the + owner-only XDG config (`0600`). +5. Before every first meal after install (and before retrying after deferred setup), run and parse + `nomnom doctor --json` plus `nomnom setup --status --json`. If USDA setup remains deferred, use a + friendly barcode, package-photo, or exact local-cache flow. Never let a raw + `usda_key_required` error become the user's onboarding experience. ## Log free text diff --git a/tests/test_cli.py b/tests/test_cli.py index 9d79239..adb5673 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -359,11 +359,50 @@ def isatty(self): assert main(["setup"]) == 0 output = capsys.readouterr().out + assert "One-time connection" in output + assert "no-label generic-food lookup" in output + assert "https://fdc.nal.usda.gov/api-key-signup.html" in output + assert "Validating" in output + assert "Connected" in output assert "product/barcode lookup (no key): reachable" in output assert "full-text resolution: unavailable" in output assert "Product reachability does not imply full-text readiness." in output +def test_cli_setup_status_json_is_prompt_free_and_actionable(monkeypatch, capsys): + expected = { + "status": "setup_required", + "providers": { + "openfoodfacts": { + "configured": True, + "product_lookup_reachable": True, + "full_text_search_ready": True, + }, + "usda": { + "configured": False, + "reachable": False, + "key_source": None, + "purpose": "no-label generic-food lookup", + "signup_url": "https://fdc.nal.usda.gov/api-key-signup.html", + "next_action": { + "command": "nomnom setup", + "message": "Run the one-time connection in your terminal.", + }, + }, + }, + } + monkeypatch.setattr("nomnomcli.cli.setup_status_report", lambda: expected) + monkeypatch.setattr( + "nomnomcli.cli.setup_providers", + lambda **_: pytest.fail("status mode must not enter interactive setup"), + ) + + assert main(["setup", "--status", "--json"]) == 0 + captured = capsys.readouterr() + assert captured.err == "" + assert json.loads(captured.out) == expected + + def test_cli_off_alternatives_are_additive_json(user_db, monkeypatch, capsys): monkeypatch.setenv("NOMNOM_DB_PATH", str(user_db)) matches = [ diff --git a/tests/test_config.py b/tests/test_config.py index 7d33211..63140fc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,7 +7,7 @@ from nomnomcli.config import ProviderConfig from nomnomcli.errors import NomnomError -from nomnomcli.onboarding import doctor_report, setup_providers +from nomnomcli.onboarding import doctor_report, setup_providers, setup_status_report class HealthyOFF: @@ -254,3 +254,40 @@ def probe(self): "product_lookup_reachable": True, "full_text_search_ready": False, } + + +def test_setup_status_is_actionable_and_never_contains_key(tmp_path): + path = tmp_path / "config.toml" + config = ProviderConfig(environ={}, config_path=path) + config.store_usda_key("never-emit-this-placeholder") + + report = setup_status_report( + config=config, off_client=HealthyOFF(), usda_client=HealthyUSDA() + ) + + assert report["status"] == "connected" + assert report["providers"]["usda"] == { + "configured": True, + "reachable": True, + "key_source": "user_config", + "purpose": "no-label generic-food lookup", + "signup_url": "https://fdc.nal.usda.gov/api-key-signup.html", + "next_action": None, + } + assert "never-emit-this-placeholder" not in json.dumps(report) + + +def test_setup_status_unconfigured_has_one_safe_next_action(tmp_path): + report = setup_status_report( + config=ProviderConfig(environ={}, config_path=tmp_path / "missing.toml"), + off_client=HealthyOFF(), + usda_client=HealthyUSDA(), + ) + + assert report["status"] == "setup_required" + assert report["providers"]["usda"]["configured"] is False + assert report["providers"]["usda"]["reachable"] is False + assert report["providers"]["usda"]["next_action"] == { + "command": "nomnom setup", + "message": "Run the one-time connection in your terminal.", + } diff --git a/tests/test_install.py b/tests/test_install.py index e9d2aeb..e2cbe79 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1,19 +1,470 @@ from __future__ import annotations +import json +import os import subprocess from pathlib import Path +import pytest -def test_installer_prompts_setup_and_doctor_before_first_log(): - root = Path(__file__).resolve().parents[1] +ROOT = Path(__file__).resolve().parents[1] +REPO_URL = "git+https://github.com/maxjustships/nomnomcli" + + +def _executable(path: Path, content: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + path.chmod(0o755) + return path + + +def _nomnom_fixture( + tmp_path: Path, *, usda_ready: bool = False, doctor_payload: str | None = None +) -> Path: + if doctor_payload is None: + doctor_payload = json.dumps( + { + "providers": { + "openfoodfacts": { + "configured": True, + "product_lookup_reachable": True, + "full_text_search_ready": True, + }, + "usda": { + "configured": usda_ready, + "reachable": usda_ready, + "key_source": None, + }, + } + }, + separators=(",", ":"), + ) + return _executable( + tmp_path / "nomnom-fixture", + f"""#!/usr/bin/python3 +import os +import sys +from pathlib import Path + +with open(Path(os.environ["HOME"]).parent / "trace.log", "a", encoding="utf-8") as trace: + print(f"nomnom {{' '.join(sys.argv[1:])}}", file=trace) +if any(os.environ.get(name) for name in ("VIRTUAL_ENV", "PYTHONPATH", "PYTHONHOME")): + print("private Python environment leaked into verification", file=sys.stderr) + raise SystemExit(90) +if sys.argv[1:] == ["--version"]: + print("nomnom 0.4.0") +elif sys.argv[1:] == ["doctor", "--json"]: + print({doctor_payload!r}) +else: + raise SystemExit(2) +""", + ) + + +def _base_environment(tmp_path: Path, harness_bin: Path, nomnom_fixture: Path) -> dict[str, str]: + home = tmp_path / "home" + home.mkdir() + trace = tmp_path / "trace.log" + database = tmp_path / "existing.sqlite3" + database.write_bytes(b"existing-user-database-must-not-change") + return { + "HOME": str(home), + "PATH": f"{harness_bin}:/usr/bin:/bin", + "SHELL": "/bin/sh", + "TRACE": str(trace), + "FAKE_NOMNOM": str(nomnom_fixture), + "NOMNOM_DB_PATH": str(database), + "XDG_CONFIG_HOME": str(tmp_path / "config"), + } + + +def _run_installer(environment: dict[str, str], *arguments: str) -> subprocess.CompletedProcess: + return subprocess.run( + ["/bin/sh", "install.sh", *arguments], + cwd=ROOT, + env=environment, + check=False, + capture_output=True, + text=True, + ) + + +@pytest.mark.parametrize( + ("usda_ready", "expected_status"), + [ + (False, "installed_needs_provider_setup"), + (True, "installed_and_ready"), + ], +) +def test_installer_prefers_uv_and_reports_provider_state_without_network( + tmp_path, usda_ready, expected_status +): + harness_bin = tmp_path / "harness-bin" + tool_bin = tmp_path / "home" / ".local" / "bin" + fixture = _nomnom_fixture(tmp_path, usda_ready=usda_ready) + environment = _base_environment(tmp_path, harness_bin, fixture) + environment["UV_TOOL_BIN_DIR"] = str(tool_bin) + Path(environment["HOME"], ".profile").write_text( + 'PATH="$HOME/.local/bin:$PATH"\nexport PATH\n', encoding="utf-8" + ) + _executable( + harness_bin / "uv", + """#!/bin/sh +printf 'uv %s\n' "$*" >> "$TRACE" +if [ "$1 $2 $3" = "tool install --force" ]; then + mkdir -p "$UV_TOOL_BIN_DIR" + cp "$FAKE_NOMNOM" "$UV_TOOL_BIN_DIR/nomnom" + exit 0 +fi +if [ "$1 $2 $3" = "tool dir --bin" ]; then + printf '%s\n' "$UV_TOOL_BIN_DIR" + exit 0 +fi +exit 2 +""", + ) + _executable( + harness_bin / "pipx", + """#!/bin/sh +printf 'pipx %s\n' "$*" >> "$TRACE" +exit 99 +""", + ) + + before = Path(environment["NOMNOM_DB_PATH"]).read_bytes() + result = _run_installer(environment, "--status-json") + after = Path(environment["NOMNOM_DB_PATH"]).read_bytes() + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload == { + "status": expected_status, + "executable": str(tool_bin / "nomnom"), + "version": "nomnom 0.4.0", + "error": None, + "path_repair": None, + } + trace = Path(environment["TRACE"]).read_text(encoding="utf-8") + assert f"uv tool install --force {REPO_URL}" in trace + assert "pipx " not in trace + assert "nomnom --version" in trace + assert "nomnom doctor --json" in trace + assert before == after + + +@pytest.mark.parametrize( + "doctor_payload", + [ + pytest.param( + ( + "{\n" + ' "providers": {\n' + ' "openfoodfacts": {"configured": true, "full_text_search_ready": false, ' + '"product_lookup_reachable": true},\n' + ' "usda": {"configured": false, "key_source": null, "reachable": false}\n' + " }\n" + "}" + ), + id="multiline-doctor-output-from-smoke", + ), + pytest.param( + ( + "{\n" + ' "providers": {\n' + ' "openfoodfacts": {"configured": true, "full_text_search_ready": true, ' + '"product_lookup_reachable": true},\n' + ' "usda": {\n' + ' "diagnostic": {"configured": true, "reachable": true},\n' + ' "reachable": false,\n' + ' "key_source": null,\n' + ' "configured": false\n' + " }\n" + " }\n" + "}" + ), + id="usda-fields-reordered-with-nested-true-values", + ), + ], +) +def test_installer_reads_only_top_level_usda_provider_booleans(tmp_path, doctor_payload): + harness_bin = tmp_path / "harness-bin" + tool_bin = tmp_path / "home" / ".local" / "bin" + fixture = _nomnom_fixture(tmp_path, doctor_payload=doctor_payload) + environment = _base_environment(tmp_path, harness_bin, fixture) + environment["UV_TOOL_BIN_DIR"] = str(tool_bin) + Path(environment["HOME"], ".profile").write_text( + 'PATH="$HOME/.local/bin:$PATH"\nexport PATH\n', encoding="utf-8" + ) + _executable( + harness_bin / "uv", + """#!/bin/sh +if [ "$1 $2 $3" = "tool install --force" ]; then + mkdir -p "$UV_TOOL_BIN_DIR" + cp "$FAKE_NOMNOM" "$UV_TOOL_BIN_DIR/nomnom" + exit 0 +fi +if [ "$1 $2 $3" = "tool dir --bin" ]; then + printf '%s\n' "$UV_TOOL_BIN_DIR" + exit 0 +fi +exit 2 +""", + ) + + result = _run_installer(environment, "--json") + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["status"] == "installed_needs_provider_setup" + assert payload["path_repair"] is None + + +def test_installer_uses_pipx_when_uv_is_unavailable(tmp_path): + harness_bin = tmp_path / "harness-bin" + tool_bin = tmp_path / "pipx-bin" + fixture = _nomnom_fixture(tmp_path, usda_ready=False) + environment = _base_environment(tmp_path, harness_bin, fixture) + environment["PIPX_BIN_DIR"] = str(tool_bin) + _executable( + harness_bin / "pipx", + """#!/bin/sh +printf 'pipx %s\n' "$*" >> "$TRACE" +if [ "$1 $2 $3" = "install --force git+https://github.com/maxjustships/nomnomcli" ]; then + mkdir -p "$PIPX_BIN_DIR" + cp "$FAKE_NOMNOM" "$PIPX_BIN_DIR/nomnom" + exit 0 +fi +if [ "$1 $2 $3" = "environment --value PIPX_BIN_DIR" ]; then + printf '%s\n' "$PIPX_BIN_DIR" + exit 0 +fi +exit 2 +""", + ) + + result = _run_installer(environment, "--json") + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["status"] == "installed_path_repair_needed" + assert payload["executable"] == str(tool_bin / "nomnom") + assert payload["version"] == "nomnom 0.4.0" + assert f'export PATH="{tool_bin}:$PATH"' in payload["path_repair"] + assert payload["error"] is None + assert f"pipx install --force {REPO_URL}" in Path(environment["TRACE"]).read_text() + + human_result = _run_installer(environment) + + assert human_result.returncode == 0, human_result.stderr + assert "Status: installed_path_repair_needed" in human_result.stdout + assert "One-time PATH repair:" in human_result.stdout + assert "Base product/barcode capture works" in human_result.stdout + assert "One action: run 'nomnom setup' in your own terminal." in human_result.stdout + + +def test_installer_user_site_fallback_skips_agent_venv_python(tmp_path): + agent_bin = tmp_path / "agent-venv" / "bin" + system_bin = tmp_path / "system-bin" + tool_bin = tmp_path / "home" / ".local" / "bin" + fixture = _nomnom_fixture(tmp_path, usda_ready=True) + environment = _base_environment(tmp_path, system_bin, fixture) + environment["PATH"] = f"{agent_bin}:{system_bin}:/usr/bin:/bin" + environment["VIRTUAL_ENV"] = str(agent_bin.parent) + _executable( + agent_bin / "python3", + """#!/bin/sh +printf 'agent-python %s\n' "$*" >> "$TRACE" +exit 0 +""", + ) + _executable( + system_bin / "python3.11", + """#!/bin/sh +printf 'system-python %s\n' "$*" >> "$TRACE" +if [ "$1" = "-c" ]; then + case "$2" in + *sysconfig*) printf '%s\n' "$HOME/.local/bin" ;; + *sys.executable*) printf '%s\n' "$0" ;; + esac + exit 0 +fi +if [ "$1 $2" = "-m pip" ]; then + mkdir -p "$HOME/.local/bin" + cp "$FAKE_NOMNOM" "$HOME/.local/bin/nomnom" + exit 0 +fi +exit 2 +""", + ) + + result = _run_installer(environment, "--status-json") + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["executable"] == str(tool_bin / "nomnom") + trace = Path(environment["TRACE"]).read_text(encoding="utf-8") + assert "agent-python -m pip" not in trace + assert f"system-python -m pip install --user --upgrade {REPO_URL}" in trace + + +def test_installer_venv_only_fallback_is_actionable_structured_error(tmp_path): + agent_bin = tmp_path / "agent-venv" / "bin" + utilities = tmp_path / "utilities" + utilities.mkdir() + for name in ("awk", "env"): + os.symlink(Path("/usr/bin") / name, utilities / name) + fixture = _nomnom_fixture(tmp_path) + environment = _base_environment(tmp_path, utilities, fixture) + environment["PATH"] = f"{agent_bin}:{utilities}" + environment["VIRTUAL_ENV"] = str(agent_bin.parent) + _executable(agent_bin / "python3", "#!/bin/sh\nexit 0\n") + + result = _run_installer(environment, "--json") + + assert result.returncode == 1 + payload = json.loads(result.stdout) + assert payload["status"] == "error" + assert payload["executable"] is None + assert payload["version"] is None + assert payload["error"]["code"] == "system_python_not_found" + assert "Python 3.11+" in payload["error"]["message"] + assert "uv" in payload["error"]["action"] + assert "pipx" in payload["error"]["action"] + + +def test_installer_verification_ignores_agent_xdg_and_nomnom_environment(tmp_path): + harness_bin = tmp_path / "harness-bin" + tool_bin = tmp_path / "home" / ".local" / "bin" + fixture = _executable( + tmp_path / "nomnom-fixture", + """#!/usr/bin/python3 +import json +import os +import sys +from pathlib import Path + +tracked = ( + "XDG_CONFIG_HOME", + "XDG_CACHE_HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "NOMNOM_USDA_KEY", + "NOMNOM_GENERIC_PROXY_POLICY", + "NOMNOM_DB_PATH", + "NOMNOM_DISABLE_OFF", + "NOMNOM_OFFLINE", +) +with open(Path(os.environ["HOME"]).parent / "trace.log", "a", encoding="utf-8") as trace: + print(json.dumps({name: os.environ.get(name) for name in tracked}, sort_keys=True), file=trace) + +if sys.argv[1:] == ["--version"]: + print("nomnom 0.4.0") +elif sys.argv[1:] == ["doctor", "--json"]: + config_home = Path(os.environ["XDG_CONFIG_HOME"]) + configured = bool(os.environ.get("NOMNOM_USDA_KEY")) or ( + config_home / "nomnomcli" / "config.toml" + ).exists() + print(json.dumps({"providers": {"usda": {"configured": configured, "reachable": configured}}})) +else: + raise SystemExit(2) +""", + ) + environment = _base_environment(tmp_path, harness_bin, fixture) + environment.update( + { + "UV_TOOL_BIN_DIR": str(tool_bin), + "XDG_CONFIG_HOME": str(tmp_path / "agent-config"), + "XDG_CACHE_HOME": str(tmp_path / "agent-cache"), + "XDG_DATA_HOME": str(tmp_path / "agent-data"), + "XDG_STATE_HOME": str(tmp_path / "agent-state"), + "NOMNOM_USDA_KEY": "agent-usda-key", + "NOMNOM_GENERIC_PROXY_POLICY": "exact_only", + "NOMNOM_DISABLE_OFF": "1", + "NOMNOM_OFFLINE": "1", + } + ) + agent_config = Path(environment["XDG_CONFIG_HOME"]) / "nomnomcli" + agent_config.mkdir(parents=True) + (agent_config / "config.toml").write_text( + "[providers.usda]\napi_key = 'agent-config-key'\n", encoding="utf-8" + ) + Path(environment["HOME"], ".profile").write_text( + 'PATH="$HOME/.local/bin:$PATH"\nexport PATH\n', encoding="utf-8" + ) + _executable( + harness_bin / "uv", + """#!/bin/sh +if [ "$1 $2 $3" = "tool install --force" ]; then + mkdir -p "$UV_TOOL_BIN_DIR" + cp "$FAKE_NOMNOM" "$UV_TOOL_BIN_DIR/nomnom" + exit 0 +fi +if [ "$1 $2 $3" = "tool dir --bin" ]; then + printf '%s\n' "$UV_TOOL_BIN_DIR" + exit 0 +fi +exit 2 +""", + ) + + result = _run_installer(environment, "--status-json") + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout)["status"] == "installed_needs_provider_setup" + invocation_environments = [ + json.loads(line) + for line in Path(environment["TRACE"]).read_text().splitlines() + if line.startswith("{") + ] + assert len(invocation_environments) == 2 + for invocation_environment in invocation_environments: + assert invocation_environment == { + "NOMNOM_DB_PATH": None, + "NOMNOM_DISABLE_OFF": None, + "NOMNOM_GENERIC_PROXY_POLICY": None, + "NOMNOM_OFFLINE": None, + "NOMNOM_USDA_KEY": None, + "XDG_CACHE_HOME": None, + "XDG_CONFIG_HOME": str(Path(environment["HOME"]) / ".config"), + "XDG_DATA_HOME": None, + "XDG_STATE_HOME": None, + } + + +def test_installer_dry_run_surfaces_one_voluntary_setup_action(): result = subprocess.run( - ["sh", "install.sh", "--dry-run"], - cwd=root, + ["/bin/sh", "install.sh", "--dry-run"], + cwd=ROOT, check=True, capture_output=True, text=True, ) - assert "nomnom setup" in result.stdout + assert "uv tool install --force" in result.stdout + assert result.stdout.count("nomnom setup") == 1 + assert "one free USDA setup remains" in result.stdout assert "nomnom doctor --json" in result.stdout assert "before the first food log" in result.stdout + + +def test_agent_skill_contains_the_mandatory_issue_21_protocol(): + skill = (ROOT / "skill" / "SKILL.md").read_text(encoding="utf-8") + required_sentence = ( + "Base product/barcode capture works; to enable no-label generic-food lookup, " + "one free USDA setup remains." + ) + + assert "Mandatory install protocol" in skill + assert "--status-json" in skill + assert "sanitized user/system-only environment" in skill + assert "XDG_CONFIG_HOME=$HOME/.config" in skill + assert "clear every `NOMNOM_*` override" in skill + assert "nomnom --version" in skill + assert "nomnom doctor --json" in skill + assert "nomnom setup --status --json" in skill + assert required_sentence in skill + assert "exactly one voluntary action" in skill + assert "must never type, receive, echo, or persist" in skill + assert "Before every first meal" in skill + assert "local-cache" in skill + assert "Never run `pip install -e`" in skill