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
236 changes: 229 additions & 7 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

import psutil
from rich.markdown import Markdown

from cortex.api_key_detector import auto_detect_api_key, setup_api_key
Expand All @@ -24,6 +25,8 @@
from cortex.env_manager import EnvironmentManager, get_env_manager
from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType
from cortex.llm.interpreter import CommandInterpreter
from cortex.monitor.live_monitor_ui import MonitorUI
from cortex.monitor.resource_monitor import ResourceMonitor
from cortex.network_config import NetworkConfig
from cortex.notification_manager import NotificationManager
from cortex.role_manager import RoleManager
Expand Down Expand Up @@ -58,6 +61,123 @@ def __init__(self, verbose: bool = False):
self.spinner_idx = 0
self.verbose = verbose

def monitor(self, args: argparse.Namespace) -> int:
"""Show current system resource usage."""
resource_monitor = ResourceMonitor(interval=1.0)
duration = getattr(args, "duration", None)

console.print("System Health:")

metrics = self._collect_monitoring_metrics(resource_monitor, duration)
if metrics:
console.print(MonitorUI.format_system_health(metrics))

self._display_alerts(metrics)

export_result = self._handle_monitor_export(resource_monitor, args)
if export_result != 0:
return export_result

self._display_recommendations(resource_monitor)
return 0

Comment on lines +64 to +83
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 | 🟠 Major

Add audit logging for cortex monitor.

The new monitor command doesn’t record an audit entry. Please log this operation to the history DB so it’s traceable.

🛠️ Example (using existing history API)
         resource_monitor = ResourceMonitor(interval=1.0)
         duration = getattr(args, "duration", None)
+
+        history = InstallationHistory()
+        history.record_installation(
+            InstallationType.CONFIG,
+            ["monitor"],
+            [f"cortex monitor --duration {duration}" if duration else "cortex monitor"],
+            datetime.now(timezone.utc),
+        )
As per coding guidelines, please log all operations to ~/.cortex/history.db for audit purposes.
🤖 Prompt for AI Agents
In `@cortex/cli.py` around lines 64 - 83, Add an audit entry when the monitor
command runs by recording to the history DB (~/.cortex/history.db) inside the
monitor method: after parsing args and before or immediately after instantiating
ResourceMonitor (or right after console.print("System Health:")), call the
existing history API to create a record containing operation="monitor", the
provided args/duration, a timestamp, and the result code (use the export_result
or final return value). Update monitor to call the history logging function (use
the project’s history API/class you already have) and ensure failures to write
history are handled non-fatally (log a warning) so _collect_monitoring_metrics,
_display_alerts, _handle_monitor_export, and _display_recommendations keep
running.

def _display_recommendations(self, resource_monitor: ResourceMonitor) -> None:
"""Display performance recommendations."""
if not resource_monitor.history or len(resource_monitor.history) <= 1:
return

recommendations = resource_monitor.get_recommendations()
if recommendations:
console.print("\n[bold cyan]⚡ Performance Recommendations:[/bold cyan]")
for rec in recommendations:
console.print(f" • {rec}")

def _collect_monitoring_metrics(
self, resource_monitor: ResourceMonitor, duration: float | None
) -> dict[str, Any] | None:
"""Collect monitoring metrics based on duration."""

if duration:
# Run monitoring loop for the given duration
resource_monitor.monitor(duration)

# Show final snapshot after monitoring
summary = resource_monitor.get_summary()
if summary:
return summary["current"]
else:
console.print("[yellow]No monitoring data collected.[/yellow]")
return None
else:
return resource_monitor.sample()

def _display_alerts(self, metrics: dict[str, Any] | None) -> None:
"""Display alerts from metrics."""
if not metrics:
return

alerts = metrics.get("alerts", [])
if alerts:
console.print("\n[bold yellow]⚠️ Alerts:[/bold yellow]")
for alert in alerts:
console.print(f" • {alert}")

def _handle_monitor_export(
self, resource_monitor: ResourceMonitor, args: argparse.Namespace
) -> int:
"""Handle export of monitoring data."""
if not getattr(args, "export", None):
return 0

filename = self._export_monitor_data(
monitor=resource_monitor,
export=args.export,
output=args.output,
)

if filename:
cx_print(f"✓ Monitoring data exported to {filename}", "success")
return 0
else:
self._print_error("Failed to export monitoring data")
return 1

def _display_recommendations(self, resource_monitor: ResourceMonitor) -> None:
"""Display performance recommendations."""
if not resource_monitor.history or len(resource_monitor.history) <= 1:
return

recommendations = resource_monitor.get_recommendations()
if recommendations:
console.print("\n[bold cyan]⚡ Performance Recommendations:[/bold cyan]")
for rec in recommendations:
console.print(f" • {rec}")

# MONITOR HELPERS
def _get_latest_metrics(self, monitor: ResourceMonitor) -> dict:
"""Return latest collected metrics or take a fresh sample."""
return monitor.history[-1] if monitor.history else monitor.sample()
Comment on lines +157 to +159
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 | 🟠 Major

Refresh metrics instead of reusing stale samples.

Once history is non-empty, _get_latest_metrics returns the same sample forever, so installation monitoring isn’t real-time. Sample again when the interval has elapsed.

🛠️ Proposed fix
-    def _get_latest_metrics(self, monitor: ResourceMonitor) -> dict:
+    def _get_latest_metrics(self, monitor: ResourceMonitor) -> dict[str, Any]:
         """Return latest collected metrics or take a fresh sample."""
-        return monitor.history[-1] if monitor.history else monitor.sample()
+        if not monitor.history:
+            return monitor.sample()
+        last = monitor.history[-1]
+        if time.time() - last.get("timestamp", 0) >= monitor.interval:
+            return monitor.sample()
+        return last

Also applies to: 1039-1046

🤖 Prompt for AI Agents
In `@cortex/cli.py` around lines 157 - 159, _get_latest_metrics currently always
returns monitor.history[-1] once history exists, producing stale readings;
change it to return monitor.history[-1] only if the last sample is still fresh,
otherwise call monitor.sample() to get a new reading. Implement this by checking
the timestamp/age of the last entry (e.g., monitor.history[-1].timestamp or
monitor.last_sample_time) against the configured interval (e.g.,
monitor.sample_interval or monitor.interval) and call monitor.sample() when the
elapsed time >= interval; otherwise return monitor.history[-1]. Update the same
logic wherever _get_latest_metrics behavior is duplicated (lines referenced
1039-1046).


def _export_monitor_data(
self,
monitor: ResourceMonitor,
export: str,
output: str | None,
software: str | None = None,
) -> str | None:
"""Export monitoring data safely."""
from cortex.monitor import export_monitoring_data

if output:
filename = f"{output}.{export}"
else:
safe_name = "".join(c if c.isalnum() else "_" for c in (software or "monitor"))
filename = f"{safe_name}_monitoring.{export}"

if export_monitoring_data(monitor, export, filename): # Check if successful
return filename # Return filename on success
return None # Return None on failure

# Define a method to handle Docker-specific permission repairs
def docker_permissions(self, args: argparse.Namespace) -> int:
"""Handle the diagnosis and repair of Docker file permissions.
Expand Down Expand Up @@ -817,7 +937,23 @@ def install(
execute: bool = False,
dry_run: bool = False,
parallel: bool = False,
monitor: bool = False,
export: str | None = None,
output: str | None = None,
):

# If --monitor is used, automatically enable execution and initialize the resource monitor.
resource_monitor = None
if monitor and not execute and not dry_run:
print(f"📊 Monitoring enabled for: {software}")
print("Note: Monitoring requires execution. Auto-enabling --execute flag.")
execute = True

Comment on lines +945 to +951
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 | 🟠 Major

Don’t auto‑enable execution when --monitor is set.

Auto‑enabling execution bypasses the dry‑run default and may run installs unintentionally. Require explicit --execute (or exit with a clear error).

🛠️ Proposed fix
         resource_monitor = None
         if monitor and not execute and not dry_run:
-            print(f"📊 Monitoring enabled for: {software}")
-            print("Note: Monitoring requires execution. Auto-enabling --execute flag.")
-            execute = True
+            self._print_error("Monitoring requires --execute. Re-run with --execute or --dry-run.")
+            return 1
As per coding guidelines, installation operations must remain dry‑run by default.
🤖 Prompt for AI Agents
In `@cortex/cli.py` around lines 945 - 951, The code currently auto-enables
execution when monitor is set (see variables monitor, execute, dry_run and the
resource_monitor initialization); remove the auto-enable and require explicit
--execute instead: replace the block that sets execute = True with logic that
detects monitor and not execute, prints a clear error/notice about requiring
--execute to run monitoring (preserving dry_run behavior), and then exit
non‑zero (e.g., SystemExit or exit(1)); leave resource_monitor as None unless
execution is explicitly requested.

if monitor:
resource_monitor = ResourceMonitor(interval=1.0)
console.print(f"Installing {software}...") # Simple print
cx_print("📊 Monitoring system resources during installation...", "info")

# Validate input first
is_valid, error = validate_install_request(software)
if not is_valid:
Expand Down Expand Up @@ -900,6 +1036,22 @@ def progress_callback(current, total, step):
print(f"\n[{current}/{total}] {status_emoji} {step.description}")
print(f" Command: {step.command}")

# Samples current system resources during each install step and displays live metrics.
if resource_monitor:
metrics = self._get_latest_metrics(resource_monitor)
if current == 1 or "compil" in step.description.lower():
from cortex.monitor.live_monitor_ui import MonitorUI

installation_display = MonitorUI.format_installation_metrics(metrics)
console.print("\n" + installation_display)

# Display alerts if any
alerts = metrics.get("alerts", [])
if alerts:
console.print("\n[yellow]⚠️ Resource Alert:[/yellow]")
for alert in alerts:
console.print(f" • {alert}")

print("\nExecuting commands...")

if parallel:
Expand Down Expand Up @@ -1003,6 +1155,39 @@ def parallel_log_callback(message: str, level: str = "info"):
self._print_success(f"{software} installed successfully!")
print(f"\nCompleted in {result.total_duration:.2f} seconds")

# Displays the highest CPU and memory usage recorded during the installation.
if monitor and resource_monitor:
summary = resource_monitor.get_summary()
peak = summary.get("peak", {})

from cortex.monitor.live_monitor_ui import MonitorUI

peak_display = MonitorUI.format_peak_usage(peak)
console.print("\n" + peak_display)

# Display performance recommendation
recommendations = resource_monitor.get_recommendations()
if recommendations:
console.print(
"\n[bold cyan]⚡ Performance Recommendations:[/bold cyan]"
)
for rec in recommendations:
console.print(f" • {rec}")

# Export if requested
if export:
filename = self._export_monitor_data(
monitor=resource_monitor,
export=export,
output=output,
software=software,
)

if filename:
cx_print(f"✓ Monitoring data exported to {filename}", "success")
else:
self._print_error("Failed to export monitoring data")

# Record successful installation
if install_id:
history.update_installation(install_id, InstallationStatus.SUCCESS)
Expand Down Expand Up @@ -1271,13 +1456,6 @@ def _display_summary_table(self, result, style: str, table_class) -> None:
console.print("\n[bold]📊 Impact Summary:[/bold]")
console.print(summary_table)

def _display_recommendations(self, recommendations: list) -> None:
"""Display recommendations."""
if recommendations:
console.print("\n[bold green]💡 Recommendations:[/bold green]")
for rec in recommendations:
console.print(f" • {rec}")

def _execute_removal(self, package: str, purge: bool = False) -> int:
"""Execute the actual package removal with audit logging"""
import datetime
Expand Down Expand Up @@ -2965,6 +3143,30 @@ def main():
# Demo command
demo_parser = subparsers.add_parser("demo", help="See Cortex in action")

# Monitor command
monitor_parser = subparsers.add_parser(
"monitor",
help="Show real-time system resource usage",
)

monitor_parser.add_argument(
"--export",
choices=["json", "csv"],
help="Export monitoring data to a file",
)

monitor_parser.add_argument(
"--output",
default="monitoring_data",
help="Output filename (without extension)",
)

monitor_parser.add_argument(
"--duration",
type=float,
help="Monitor for specified duration in seconds",
)

# Wizard command
wizard_parser = subparsers.add_parser("wizard", help="Configure API key interactively")

Expand Down Expand Up @@ -3067,6 +3269,21 @@ def main():
action="store_true",
help="Output impact analysis as JSON",
)
install_parser.add_argument(
"--monitor",
action="store_true",
help="Monitor system resources during installation",
)
install_parser.add_argument(
"--export",
choices=["json", "csv"],
help="Export monitoring data to a file (requires --monitor)",
)
install_parser.add_argument(
"--output",
default="installation_monitoring",
help="Output filename (without extension, used with --export)",
)

# Import command - import dependencies from package manager files
import_parser = subparsers.add_parser(
Expand Down Expand Up @@ -3566,6 +3783,8 @@ def main():

if args.command == "demo":
return cli.demo()
elif args.command == "monitor":
return cli.monitor(args)
elif args.command == "wizard":
return cli.wizard()
elif args.command == "status":
Expand Down Expand Up @@ -3596,6 +3815,9 @@ def main():
execute=args.execute,
dry_run=args.dry_run,
parallel=args.parallel,
monitor=args.monitor,
export=args.export,
output=args.output,
)
elif args.command == "remove":
# Handle --execute flag to override default dry-run
Expand Down
16 changes: 16 additions & 0 deletions cortex/monitor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .exporter import (
export_monitoring_data,
export_to_csv,
export_to_json,
)
from .live_monitor_ui import LiveMonitorUI, MonitorUI
from .resource_monitor import ResourceMonitor

__all__ = [
"ResourceMonitor",
"MonitorUI",
"LiveMonitorUI",
"export_to_csv",
"export_to_json",
"export_monitoring_data",
]
Loading
Loading