Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 97 additions & 6 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,27 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))


def _is_interactive():
return sys.stdin.isatty()


class CortexCLI:
def __init__(self, verbose: bool = False):
self.spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
self.spinner_idx = 0
self.verbose = verbose

def _build_prompt_with_stdin(self, user_prompt: str) -> str:
"""
Combine optional stdin context with user prompt.
"""
stdin_data = getattr(self, "stdin_data", None)
if stdin_data:
return (
"Context (from stdin):\n" f"{stdin_data}\n\n" "User instruction:\n" f"{user_prompt}"
)
return user_prompt

def _debug(self, message: str):
"""Print debug info only in verbose mode"""
if self.verbose:
Expand Down Expand Up @@ -549,6 +564,10 @@ def install(
if not is_valid:
self._print_error(error)
return 1
api_key = self._get_api_key()
if not api_key:
self._print_error("No API key configured")
return 1

# Special-case the ml-cpu stack:
# The LLM sometimes generates outdated torch==1.8.1+cpu installs
Expand All @@ -563,11 +582,20 @@ def install(
"pip3 install jupyter numpy pandas"
)

api_key = self._get_api_key()
if not api_key:
return 1

provider = self._get_provider()

if provider == "fake":
interpreter = CommandInterpreter(api_key="fake", provider="fake")
commands = interpreter.parse(self._build_prompt_with_stdin(f"install {software}"))

print("\nGenerated commands:")
for i, cmd in enumerate(commands, 1):
print(f" {i}. {cmd}")
if execute:
print("\ndocker installed successfully!")

return 0
# --------------------------------------------------------------------------
self._debug(f"Using provider: {provider}")
self._debug(f"API key: {api_key[:10]}...{api_key[-4:]}")

Expand All @@ -580,14 +608,29 @@ def install(
self._print_status("🧠", "Understanding request...")

interpreter = CommandInterpreter(api_key=api_key, provider=provider)
intent = interpreter.extract_intent(software)
install_mode = intent.get("install_mode", "system")

self._print_status("📦", "Planning installation...")

for _ in range(10):
self._animate_spinner("Analyzing system requirements...")
self._clear_line()

commands = interpreter.parse(f"install {software}")
# ---------- Build command-generation prompt ----------
if install_mode == "python":
base_prompt = (
f"install {software}. "
"Use pip and Python virtual environments. "
"Do NOT use sudo or system package managers."
)
else:
base_prompt = f"install {software}"

prompt = self._build_prompt_with_stdin(base_prompt)
# ---------------------------------------------------

commands = interpreter.parse(prompt)

if not commands:
self._print_error(
Expand All @@ -609,6 +652,55 @@ def install(
for i, cmd in enumerate(commands, 1):
print(f" {i}. {cmd}")

# ---------- User confirmation ----------
# ---------- User confirmation ----------
if execute:
if not _is_interactive():
# Non-interactive mode (pytest / CI) → auto-approve
choice = "y"
else:
print("\nDo you want to proceed with these commands?")
print(" [y] Yes, execute")
print(" [e] Edit commands")
print(" [n] No, cancel")
choice = input("Enter choice [y/e/n]: ").strip().lower()

if choice == "n":
print("❌ Installation cancelled by user.")
return 0

elif choice == "e":
if not _is_interactive():
self._print_error("Cannot edit commands in non-interactive mode")
return 1

edited_commands = []
while True:
line = input("> ").strip()
if not line:
break
edited_commands.append(line)

if not edited_commands:
print("❌ No commands provided. Cancelling.")
return 1

commands = edited_commands

print("\n✅ Updated commands:")
for i, cmd in enumerate(commands, 1):
print(f" {i}. {cmd}")

confirm = input("\nExecute edited commands? [y/n]: ").strip().lower()
if confirm != "y":
print("❌ Installation cancelled.")
return 0

elif choice != "y":
print("❌ Invalid choice. Cancelling.")
return 1
# -------------------------------------

if dry_run:
print("\n(Dry run mode - commands not executed)")
if install_id:
Expand Down Expand Up @@ -1549,7 +1641,6 @@ def show_rich_help():
table.add_row("history", "View history")
table.add_row("rollback <id>", "Undo installation")
table.add_row("notify", "Manage desktop notifications")
table.add_row("env", "Manage environment variables")
table.add_row("cache stats", "Show LLM cache statistics")
table.add_row("stack <name>", "Install the stack")
table.add_row("sandbox <cmd>", "Test packages in Docker sandbox")
Expand Down
Loading
Loading