Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
599fc55
added troubleshoot functionality
KrishnaShuk Jan 12, 2026
f5dc03a
registered command
KrishnaShuk Jan 12, 2026
8f65a96
added tests
KrishnaShuk Jan 12, 2026
05a889a
Merge branch 'main' into feat/troubleshootCommand
Anshgrover23 Jan 13, 2026
e602992
applied suggestions
KrishnaShuk Jan 13, 2026
50ca564
Merge branch 'feat/troubleshootCommand' of https://github.com/Krishna…
KrishnaShuk Jan 13, 2026
c82acf4
added documentation
KrishnaShuk Jan 13, 2026
f49bc9b
minor changes
KrishnaShuk Jan 13, 2026
03b6b8d
firejail sandboxing
KrishnaShuk Jan 15, 2026
a2a8b56
added human support path
KrishnaShuk Jan 16, 2026
d509f75
learning functionality
KrishnaShuk Jan 16, 2026
3f0ce36
Merge branch 'main' into feat/troubleshootCommand
KrishnaShuk Jan 16, 2026
22184a8
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 16, 2026
762e3de
small fix
KrishnaShuk Jan 16, 2026
d55887c
Merge branch 'main' into feat/troubleshootCommand
Anshgrover23 Jan 16, 2026
a47c093
applied recommendations
KrishnaShuk Jan 17, 2026
49e5dab
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2026
54f4669
minor change
KrishnaShuk Jan 17, 2026
9c694f5
minor change
KrishnaShuk Jan 17, 2026
7b4c871
changes
KrishnaShuk Jan 17, 2026
4483e5a
Merge branch 'main' into feat/troubleshootCommand
Anshgrover23 Jan 17, 2026
12ea612
Merge branch 'main' into feat/troubleshootCommand
Anshgrover23 Jan 17, 2026
87d7fba
Merge branch 'main' into feat/troubleshootCommand
Anshgrover23 Jan 17, 2026
50f169e
small fix
KrishnaShuk Jan 17, 2026
a03e8a5
Merge branch 'main' into feat/troubleshootCommand
KrishnaShuk Jan 17, 2026
7e1e6b1
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2026
a1af7aa
minor change
KrishnaShuk Jan 17, 2026
0675d8c
suggested changes
KrishnaShuk Jan 17, 2026
5ff1b1c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2026
53941c0
added suggestions
KrishnaShuk Jan 17, 2026
8eb9720
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2026
96b72ca
minor fix
KrishnaShuk Jan 17, 2026
931ea80
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2026
544bd7e
ch md file
KrishnaShuk Jan 17, 2026
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
10 changes: 7 additions & 3 deletions cortex/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,12 @@ def _call_fake(self, question: str, system_prompt: str) -> str:
return f"You have Python {platform.python_version()} installed."
return "I cannot answer that question in test mode."

def ask(self, question: str) -> str:
def ask(self, question: str, system_prompt: str | None = None) -> str:
"""Ask a natural language question about the system.

Args:
question: Natural language question
system_prompt: Optional override for the system prompt

Returns:
Human-readable answer string
Expand All @@ -302,8 +303,11 @@ def ask(self, question: str) -> str:
raise ValueError("Question cannot be empty")

question = question.strip()
context = self.info_gatherer.gather_context()
system_prompt = self._get_system_prompt(context)

# Use provided system prompt or generate default
if system_prompt is None:
context = self.info_gatherer.gather_context()
system_prompt = self._get_system_prompt(context)

# Cache lookup uses both question and system context (via system_prompt) for system-specific answers
cache_key = f"ask:{question}"
Expand Down
34 changes: 34 additions & 0 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2978,6 +2978,20 @@ def progress_callback(current: int, total: int, step: InstallationStep) -> None:
console.print(f"Error: {result.error_message}", style="red")
return 1

def doctor(self) -> int:
"""Run system health checks."""
from cortex.doctor import SystemDoctor

doc = SystemDoctor()
return doc.run_checks()

def troubleshoot(self, no_execute: bool = False) -> int:
"""Run interactive troubleshooter."""
from cortex.troubleshoot import Troubleshooter

troubleshooter = Troubleshooter(no_execute=no_execute)
return troubleshooter.start()

# --------------------------


Expand Down Expand Up @@ -3162,6 +3176,8 @@ def show_rich_help():
table.add_row("docker permissions", "Fix Docker bind-mount permissions")
table.add_row("sandbox <cmd>", "Test packages in Docker sandbox")
table.add_row("update", "Check for and install updates")
table.add_row("doctor", "System health check")
table.add_row("troubleshoot", "Interactive system troubleshooter")

console.print(table)
console.print()
Expand Down Expand Up @@ -3745,6 +3761,18 @@ def main():
)
# --------------------------

# Doctor command
doctor_parser = subparsers.add_parser("doctor", help="System health check")

# Troubleshoot command
troubleshoot_parser = subparsers.add_parser(
"troubleshoot", help="Interactive system troubleshooter"
)
troubleshoot_parser.add_argument(
"--no-execute",
action="store_true",
help="Disable automatic command execution (read-only mode)",
)
# License and upgrade commands
subparsers.add_parser("upgrade", help="Upgrade to Cortex Pro")
subparsers.add_parser("license", help="Show license status")
Expand Down Expand Up @@ -3958,6 +3986,12 @@ def main():
return 1
elif args.command == "env":
return cli.env(args)
elif args.command == "doctor":
return cli.doctor()
elif args.command == "troubleshoot":
return cli.troubleshoot(
no_execute=getattr(args, "no_execute", False),
)
elif args.command == "config":
return cli.config(args)
elif args.command == "upgrade":
Expand Down
100 changes: 100 additions & 0 deletions cortex/resolutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Resolution Manager for Cortex Troubleshooter.

This module handles the storage and retrieval of successful troubleshooting resolutions.
It uses a simple JSON file for storage and keyword matching for retrieval.
"""

import fcntl
import json
import os
import time
from pathlib import Path
from typing import TypedDict

MAX_RESOLUTIONS = 50
DEFAULT_SEARCH_LIMIT = 3


class Resolution(TypedDict):
issue: str
fix: str
timestamp: float


class ResolutionManager:
def __init__(self, storage_path: str = "~/.cortex/resolutions.json"):
self.storage_path = Path(os.path.expanduser(storage_path))
self._ensure_storage()

def _ensure_storage(self) -> None:
"""Ensure the storage file exists."""
if not self.storage_path.exists():
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.storage_path, "w") as f:
json.dump([], f)

def save(self, issue: str, fix: str) -> None:
"""Save a new resolution."""

resolution: Resolution = {
"issue": issue,
"fix": fix,
"timestamp": time.time(),
}

# Use r+ to allow reading and writing with a lock
with open(self.storage_path, "r+") as f:
# Acquire an exclusive lock to prevent race conditions
fcntl.flock(f, fcntl.LOCK_EX)
try:
try:
resolutions = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
resolutions = []

resolutions.append(resolution)

# Keep only the last N resolutions to prevent unlimited growth
if len(resolutions) > MAX_RESOLUTIONS:
resolutions = resolutions[-MAX_RESOLUTIONS:]

# Rewind to the beginning of the file to overwrite
f.seek(0)
f.truncate()
json.dump(resolutions, f, indent=2)
finally:
# Always release the lock
fcntl.flock(f, fcntl.LOCK_UN)

def search(self, query: str, limit: int = DEFAULT_SEARCH_LIMIT) -> list[Resolution]:
"""
Search for resolutions relevant to the query.

Uses simple keyword matching: finds resolutions where the issue description
shares words with the query.
"""
try:
with open(self.storage_path) as f:
resolutions: list[Resolution] = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return []

if not resolutions:
return []

query_words = set(query.lower().split())
scored_resolutions = []

for res in resolutions:
if "issue" not in res or "fix" not in res:
continue
issue_words = set(res["issue"].lower().split())
# Calculate overlap score
score = len(query_words.intersection(issue_words))
if score > 0:
scored_resolutions.append((score, res))

# Sort by score (descending) and take top N
scored_resolutions.sort(key=lambda x: x[0], reverse=True)
return [res for _, res in scored_resolutions[:limit]]
Loading
Loading