Skip to content
Open
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
80 changes: 74 additions & 6 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,28 @@ def _confirm_risky_operation(self, prediction: FailurePrediction) -> bool:

# --- End Sandbox Commands ---

def monitor(self, args: argparse.Namespace) -> int:
"""
Monitor system resource usage (CPU, RAM, Disk, Network) in real-time.

Args:
args: Parsed command-line arguments

Returns:
Exit code (0 for success)
"""
from cortex.monitor.monitor_ui import run_standalone_monitor

duration = getattr(args, "duration", None)
interval = getattr(args, "interval", 1.0)
export_path = getattr(args, "export", None)

return run_standalone_monitor(
duration=duration,
interval=interval,
export_path=export_path,
)

def ask(self, question: str, do_mode: bool = False) -> int:
"""Answer a natural language question about the system.

Expand Down Expand Up @@ -1544,11 +1566,11 @@ def install(
software: str,
execute: bool = False,
dry_run: bool = False,
max_retries: int = DEFAULT_MAX_RETRIES,
monitor: bool = False,
parallel: bool = False,
json_output: bool = False,
max_retries: int = DEFAULT_MAX_RETRIES,
) -> int:
"""Install software using the LLM-powered package manager."""
# Initialize installation history
history = InstallationHistory()
install_id = None
Expand Down Expand Up @@ -1773,6 +1795,7 @@ def parallel_log_callback(message: str, level: str = "info"):
stop_on_error=True,
progress_callback=progress_callback,
max_retries=max_retries,
enable_monitoring=monitor,
)

result = coordinator.execute()
Expand All @@ -1781,6 +1804,16 @@ def parallel_log_callback(message: str, level: str = "info"):
self._print_success(t("install.package_installed", package=software))
print(f"\n{t('progress.completed_in', seconds=f'{result.total_duration:.2f}')}")

# Display peak usage if monitoring was enabled
if monitor and result.peak_cpu is not None:
cpu_str = f"{result.peak_cpu:.0f}%"
ram_str = (
f"{result.peak_ram_gb:.1f} GB"
if result.peak_ram_gb is not None
else "N/A"
)
print(f"\n📊 Peak usage: CPU {cpu_str}, RAM {ram_str}")

# Record successful installation
if install_id:
history.update_installation(install_id, InstallationStatus.SUCCESS)
Expand Down Expand Up @@ -4964,6 +4997,42 @@ def main():
action="store_true",
help="Use voice input for software name (press F9 to record)",
)
install_parser.add_argument(
"--monitor",
action="store_true",
help="Monitor system resources during installation",
)

# Monitor command - real-time system resource monitoring
# Note: Monitoring is client-side using psutil. Daemon integration is intentionally
# out of scope to keep the feature self-contained and avoid cortexd dependencies.
monitor_parser = subparsers.add_parser(
"monitor",
help="Monitor system resource usage",
description="Track CPU, RAM, Disk, and Network usage in real-time.",
)
monitor_parser.add_argument(
"--duration",
"-d",
type=int,
metavar="SECONDS",
help="Run for fixed duration (seconds); omit for continuous monitoring",
)
monitor_parser.add_argument(
"--interval",
"-i",
type=float,
default=1.0,
metavar="SECONDS",
help="Sampling interval in seconds (default: 1.0)",
)
monitor_parser.add_argument(
"--export",
"-e",
type=str,
metavar="FILE",
help="Export metrics to file (JSON or CSV). Experimental feature.",
)
Comment on lines +5000 to +5035
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate monitor duration/interval to reject non‑positive values.

--interval 0 (or negative) can cause a tight loop or runtime errors; --duration 0 is also confusing. Add simple argparse validators to enforce positive numbers.

🔧 Suggested argparse validation
-    monitor_parser.add_argument(
+    monitor_parser.add_argument(
         "--duration",
         "-d",
-        type=int,
+        type=_positive_int,
         metavar="SECONDS",
         help="Run for fixed duration (seconds); omit for continuous monitoring",
     )
     monitor_parser.add_argument(
         "--interval",
         "-i",
-        type=float,
+        type=_positive_float,
         default=1.0,
         metavar="SECONDS",
         help="Sampling interval in seconds (default: 1.0)",
     )

You can define helpers near the parser setup:

def _positive_int(value: str) -> int:
    v = int(value)
    if v <= 0:
        raise argparse.ArgumentTypeError("Value must be > 0")
    return v

def _positive_float(value: str) -> float:
    v = float(value)
    if v <= 0:
        raise argparse.ArgumentTypeError("Value must be > 0")
    return v
🤖 Prompt for AI Agents
In `@cortex/cli.py` around lines 5000 - 5035, The monitor parser should reject
non-positive duration/interval values: add two argparse type validator helpers
(e.g., _positive_int and _positive_float) and use them when adding the
"--duration"/"-d" and "--interval"/"-i" arguments on monitor_parser (replace
type=int with type=_positive_int for duration and type=float with
type=_positive_float for interval while keeping the default for interval as
1.0); ensure the validators raise argparse.ArgumentTypeError on values <= 0 so
invalid inputs are rejected at parse time.


# Remove command - uninstall with impact analysis
remove_parser = subparsers.add_parser(
Expand Down Expand Up @@ -5648,13 +5717,12 @@ def main():
mode=getattr(args, "mode", None),
verbose=getattr(args, "verbose", False),
)
elif args.command == "monitor":
return cli.monitor(args)
elif args.command == "printer":
return cli.printer(
action=getattr(args, "action", "status"), verbose=getattr(args, "verbose", False)
)
elif args.command == "voice":
model = getattr(args, "model", None)
return cli.voice(continuous=not getattr(args, "single", False), model=model)
elif args.command == "ask":
do_mode = getattr(args, "do", False)
# Handle --mic flag for voice input
Expand Down Expand Up @@ -5723,7 +5791,7 @@ def main():
execute=args.execute,
dry_run=args.dry_run,
parallel=args.parallel,
json_output=args.json,
monitor=getattr(args, "monitor", False),
)
elif args.command == "remove":
# Handle --execute flag to override default dry-run
Expand Down
68 changes: 65 additions & 3 deletions cortex/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import json
import logging
import re
Expand All @@ -7,7 +9,7 @@
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any
from typing import TYPE_CHECKING, Any

from cortex.utils.retry import (
DEFAULT_MAX_RETRIES,
Expand All @@ -18,6 +20,9 @@
)
from cortex.validators import DANGEROUS_PATTERNS

if TYPE_CHECKING:
from cortex.monitor.sampler import PeakUsage, ResourceSampler

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -53,6 +58,10 @@ class InstallationResult:
total_duration: float
failed_step: int | None = None
error_message: str | None = None
# Monitoring data (optional)
peak_cpu: float | None = None
peak_ram_percent: float | None = None
peak_ram_gb: float | None = None


class InstallationCoordinator:
Expand All @@ -68,14 +77,18 @@ def __init__(
log_file: str | None = None,
progress_callback: Callable[[int, int, InstallationStep], None] | None = None,
max_retries: int = DEFAULT_MAX_RETRIES,
enable_monitoring: bool = False,
):
"""Initialize an installation run with optional logging and rollback."""
"""Initialize an installation run with optional logging, rollback, and monitoring."""
self.timeout = timeout
self.stop_on_error = stop_on_error
self.enable_rollback = enable_rollback
self.log_file = log_file
self.progress_callback = progress_callback
self.max_retries = max_retries
self.enable_monitoring = enable_monitoring
self._sampler: ResourceSampler | None = None
self._peak_usage: PeakUsage | None = None

if descriptions and len(descriptions) != len(commands):
raise ValueError("Number of descriptions must match number of commands")
Expand All @@ -100,7 +113,8 @@ def from_plan(
log_file: str | None = None,
progress_callback: Callable[[int, int, InstallationStep], None] | None = None,
max_retries: int = DEFAULT_MAX_RETRIES,
) -> "InstallationCoordinator":
enable_monitoring: bool = False,
) -> InstallationCoordinator:
"""Create a coordinator from a structured plan produced by an LLM.

Each plan entry should contain at minimum a ``command`` key and
Expand Down Expand Up @@ -135,6 +149,7 @@ def from_plan(
log_file=log_file,
progress_callback=progress_callback,
max_retries=max_retries,
enable_monitoring=enable_monitoring,
)

for rollback_cmd in rollback_commands:
Expand Down Expand Up @@ -263,13 +278,44 @@ def add_rollback_command(self, command: str):
"""Register a rollback command executed if a step fails."""
self.rollback_commands.append(command)

def _stop_monitoring_and_get_peaks(self) -> tuple[float | None, float | None, float | None]:
"""Stop the sampler and return (peak_cpu, peak_ram_percent, peak_ram_gb)."""
if not self._sampler:
return None, None, None
self._sampler.stop()
self._peak_usage = self._sampler.get_peak_usage()
return (
self._peak_usage.cpu_percent,
self._peak_usage.ram_percent,
self._peak_usage.ram_used_gb,
)

def execute(self) -> InstallationResult:
"""Run each installation step and capture structured results."""
start_time = time.time()
failed_step_index = None

self._log(f"Starting installation with {len(self.steps)} steps")

# Start monitoring if enabled
if self.enable_monitoring:
try:
from cortex.monitor.sampler import ResourceSampler

self._sampler = ResourceSampler(interval=1.0)
self._sampler.start()
# Only log if sampler actually started
if self._sampler.is_running:
self._log("Resource monitoring started")
else:
self._sampler = None
except ImportError:
self._log("Monitor module not available, skipping monitoring")
self._sampler = None
except Exception as e:
self._log(f"Failed to start monitoring: {e}")
self._sampler = None

for i, step in enumerate(self.steps):
if self.progress_callback:
self.progress_callback(i + 1, len(self.steps), step)
Expand All @@ -285,6 +331,9 @@ def execute(self) -> InstallationResult:
if self.enable_rollback:
self._rollback()

# Stop monitoring on failure
peak_cpu, peak_ram_percent, peak_ram_gb = self._stop_monitoring_and_get_peaks()

total_duration = time.time() - start_time
self._log(f"Installation failed at step {i + 1}")

Expand All @@ -294,11 +343,21 @@ def execute(self) -> InstallationResult:
total_duration=total_duration,
failed_step=i,
error_message=step.error or "Command failed",
peak_cpu=peak_cpu,
peak_ram_percent=peak_ram_percent,
peak_ram_gb=peak_ram_gb,
)

total_duration = time.time() - start_time
all_success = all(s.status == StepStatus.SUCCESS for s in self.steps)

# Stop monitoring and capture peak usage
peak_cpu, peak_ram_percent, peak_ram_gb = self._stop_monitoring_and_get_peaks()
if peak_cpu is not None:
self._log(
f"Monitoring stopped. Peak CPU: {peak_cpu:.1f}%, Peak RAM: {peak_ram_gb:.1f}GB"
)

if all_success:
self._log("Installation completed successfully")
else:
Expand All @@ -312,6 +371,9 @@ def execute(self) -> InstallationResult:
error_message=(
self.steps[failed_step_index].error if failed_step_index is not None else None
),
peak_cpu=peak_cpu,
peak_ram_percent=peak_ram_percent,
peak_ram_gb=peak_ram_gb,
)

def verify_installation(self, verify_commands: list[str]) -> dict[str, bool]:
Expand Down
19 changes: 19 additions & 0 deletions cortex/monitor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Cortex Monitor Module

Real-time system resource monitoring for Cortex Linux.
"""

from cortex.monitor.sampler import (
AlertThresholds,
PeakUsage,
ResourceSample,
ResourceSampler,
)

__all__ = [
"AlertThresholds",
"PeakUsage",
"ResourceSample",
"ResourceSampler",
]
Loading