From 3963f3967246a6969c554b03163641d694f456f0 Mon Sep 17 00:00:00 2001 From: Jayaram Rajgopal <101493919+JayRaj21@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:02:50 -0700 Subject: [PATCH 1/2] Changed commands and fixed major bugs --- INSTALL_GUIDE.md | 10 +- daemoniq-imp.py | 237 ++++++++++++++++++------------------------ daemoniq-sovereign.py | 226 ++++++++++++++++++---------------------- install.sh | 25 +---- manifest.json | 4 +- 5 files changed, 221 insertions(+), 281 deletions(-) diff --git a/INSTALL_GUIDE.md b/INSTALL_GUIDE.md index 09b2f28..0735da6 100644 --- a/INSTALL_GUIDE.md +++ b/INSTALL_GUIDE.md @@ -6,6 +6,8 @@ - Python 3.8 or newer - [Ollama](https://ollama.com) — installed automatically during setup if not present +**A GPU is not required.** DaemonIQ runs entirely on your CPU. If you do have a GPU, Ollama will use it automatically to speed things up — but if you don't, everything works the same, just a little slower. + Check your Python version: ```bash @@ -67,11 +69,15 @@ Qwen2.5 comes in several sizes. The wizard maps your RAM to the right one: | 24 GB+ | qwen2.5:32b | ~20 GB | | Not sure | qwen2.5:7b | ~5 GB | +**You do not need a GPU for any of these.** Ollama runs on CPU by default. If your machine has a compatible GPU (NVIDIA, AMD, or Apple Silicon), Ollama will detect and use it automatically for faster responses. If not, the model runs on CPU — responses will be a bit slower, but the quality is identical. + +If you are unsure whether your machine can handle Sovereign, choose Imp — it works well on almost any modern laptop or desktop. + --- ## Ollama -Both Imp and Demon use Ollama to run the model locally. If Ollama is not installed, the setup wizard will offer to install it for you. To install it manually: +Both Imp and Sovereign use Ollama to run the AI model locally on your machine. If Ollama is not installed, the setup wizard will offer to install it for you. To install it manually: ```bash curl -fsSL https://ollama.com/install.sh | sh @@ -81,7 +87,7 @@ To pull a model manually instead of through the wizard: ```bash ollama pull llama3 # Imp -ollama pull qwen2.5:14b # Demon — adjust tag to match your RAM +ollama pull qwen2.5:14b # Sovereign — adjust tag to match your RAM ``` Ollama resumes interrupted downloads, so if a large model download fails partway through, simply run the pull command again. diff --git a/daemoniq-imp.py b/daemoniq-imp.py index e1ba8a4..2497d17 100644 --- a/daemoniq-imp.py +++ b/daemoniq-imp.py @@ -20,7 +20,7 @@ PRODUCT_NAME = "DaemonIQ" PRODUCT_TAGLINE = "Linux Troubleshooting Assistant" -PRODUCT_VERSION = "0.4.4" +PRODUCT_VERSION = "0.5.0" CLI_COMMAND = "daemoniq" DAEMON_LABEL = "daemoniq-demon" AI_PERSONA = PRODUCT_NAME @@ -487,8 +487,8 @@ def detect_distro(): "network_hw": ["lspci", "-nnk", "-d", "::0200"], # Network controllers "audio_hw": ["lspci", "-nnk", "-d", "::0401"], # Audio devices "block_devices": ["lsblk", "-o", "NAME,MODEL,TRAN,SIZE"], - "cpu_info": ["grep", "-m4", "model name\|cpu MHz\|siblings\|cpu cores", "/proc/cpuinfo"], - "memory_info": ["grep", "MemTotal\|MemAvailable", "/proc/meminfo"], + "cpu_info": ["sh", "-c", "grep -m4 'model name|cpu MHz|siblings|cpu cores' /proc/cpuinfo"], + "memory_info": ["sh", "-c", "grep 'MemTotal|MemAvailable' /proc/meminfo"], "firmware_info": ["dmesg", "-t", "-l", "err"], } @@ -746,7 +746,7 @@ def _call_api(sid: str, message: str, api_key: str = "") -> str: data=payload, method="POST", headers={"Content-Type": "application/json"}, ) - with urllib.request.urlopen(req, timeout=120) as resp: + with urllib.request.urlopen(req, timeout=600) as resp: data = json.loads(resp.read()) reply = data.get("message", {}).get("content", "") @@ -766,7 +766,7 @@ def _execute(block: ExecBlock) -> str: lines.append(f"$ {cmd}") try: r = subprocess.run( - cmd, shell=True, capture_output=True, text=True, timeout=120, + cmd, shell=True, capture_output=True, text=True, timeout=300, env={**os.environ, "DEBIAN_FRONTEND": "noninteractive"}, ) if r.stdout.strip(): lines.append(r.stdout.strip()) @@ -987,7 +987,7 @@ def _sep(): # ── Socket communication ────────────────────────────────────────────────────── -def _request(req: dict, timeout: int = 90) -> dict: +def _request(req: dict, timeout: int = 660) -> dict: try: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.settimeout(timeout) @@ -1003,9 +1003,9 @@ def _request(req: dict, timeout: int = 90) -> dict: s.close() return json.loads(data.decode()) except FileNotFoundError: - return {"error": f"Demon not running. Start with: {CLI_COMMAND} start"} + return {"error": f"Not running. Start with: {CLI_COMMAND} start"} except ConnectionRefusedError: - return {"error": f"Demon not responding. Try: {CLI_COMMAND} restart"} + return {"error": f"Not responding. Try: {CLI_COMMAND} restart"} except socket.timeout: return {"error": "Request timed out"} except Exception as e: @@ -1021,7 +1021,7 @@ def _daemon_running() -> bool: # ── Demon lifecycle ────────────────────────────────────────────────────────── def _start_daemon(foreground=False): if _daemon_running(): - print(f"{C.YELLOW}⚠ Demon already running{C.RESET}"); return True + print(f"{C.YELLOW}⚠ Already running{C.RESET}"); return True try: if foreground: os.execv(sys.executable, [sys.executable, __file__, "_daemon_fg"]) @@ -1035,13 +1035,13 @@ def _start_daemon(foreground=False): time.sleep(0.3) if _daemon_running(): pid = Path(PID_FILE).read_text().strip() if Path(PID_FILE).exists() else "?" - print(f"{C.GREEN}✓ {PRODUCT_NAME} demon started (PID {pid}){C.RESET}") + print(f"{C.GREEN}✓ {PRODUCT_NAME} started{C.RESET}") print(f"{C.DIM} Logs: tail -f {LOG_FILE}{C.RESET}") return True - print(f"{C.RED}✗ Demon failed to start. Check: tail {LOG_FILE}{C.RESET}") + print(f"{C.RED}✗ Failed to start. Check logs: {CLI_COMMAND} logs{C.RESET}") return False except Exception as e: - print(f"{C.RED}✗ Could not start demon: {e}{C.RESET}"); return False + print(f"{C.RED}✗ Could not start: {e}{C.RESET}"); return False def _stop_daemon(): if not Path(PID_FILE).exists(): @@ -1049,7 +1049,7 @@ def _stop_daemon(): try: pid = int(Path(PID_FILE).read_text().strip()) os.kill(pid, 15) - print(f"{C.GREEN}✓ Demon stopped (PID {pid}){C.RESET}") + print(f"{C.GREEN}✓ Stopped{C.RESET}") except ProcessLookupError: print(f"{C.YELLOW}⚠ Process not found, cleaning up...{C.RESET}") for f in [PID_FILE, SOCKET_PATH]: Path(f).unlink(missing_ok=True) @@ -1065,8 +1065,8 @@ def _show_hardware(): try: snap = json.load(open(HARDWARE_SNAPSHOT_FILE)) except Exception: - print(f"{C.RED}✗ Demon not running and no cached snapshot found.{C.RESET}") - print(f" Start the demon first: {C.CYAN}{CLI_COMMAND} start{C.RESET}") + print(f"{C.RED}✗ Not running and no cached snapshot found.{C.RESET}") + print(f" Start it first: {C.CYAN}{CLI_COMMAND} start{C.RESET}") return else: r = _request({"cmd": "hardware"}) @@ -1108,7 +1108,7 @@ def _show_hardware(): if len(lines) > 15: for line in lines[:15]: print(f" {C.MGRAY}{line}{C.RESET}") - print(f" {C.DIM}... ({len(lines) - 15} more lines — see demon log for full output){C.RESET}") + print(f" {C.DIM}... ({len(lines) - 15} more lines — run '{CLI_COMMAND} logs' for full output){C.RESET}") else: for line in lines: print(f" {C.MGRAY}{line}{C.RESET}") @@ -1116,22 +1116,33 @@ def _show_hardware(): def _show_status(): + _show_info() + +def _show_info(): if not _daemon_running(): - print(f"{C.RED}✗ Demon is NOT running{C.RESET}") - print(f" Start with: {C.CYAN}{CLI_COMMAND} start{C.RESET}"); return - r = _request({"cmd": "status"}) + print(f"\n {C.RED}✗ Background process is not running{C.RESET}") + print(f" Start with: {C.CYAN}{CLI_COMMAND} start{C.RESET}\n"); return + r = _request({"cmd": "status"}) + dr = _request({"cmd": "distro_info"}) if "error" in r: print(f"{C.RED}✗ {r['error']}{C.RESET}"); return - badge = f"{C.GREEN}[full support]{C.RESET}" if r.get("supported") else f"{C.YELLOW}[limited support]{C.RESET}" - print(f"{C.GREEN}✓ Demon running{C.RESET} PID: {C.BOLD}{r.get('pid')}{C.RESET}") - print(f" Distro: {C.CYAN}{r.get('distro','?')}{C.RESET} {badge}") - print(f" Sessions: {C.CYAN}{r.get('sessions',0)}{C.RESET}") - print(f" History: {C.CYAN}{r.get('history_entries',0)} commands{C.RESET}") + + support = f"{C.GREEN}✓ Full support{C.RESET}" if r.get("supported") else f"{C.YELLOW}⚠ Limited support{C.RESET}" + pms = ", ".join(dr.get("pkg_managers", []) or ["none detected"]) if "error" not in dr else "unknown" + + print(f"\n {C.CYAN}{C.BOLD}DaemonIQ Status{C.RESET}\n") + print(f" Process: {C.GREEN}running{C.RESET} (PID {r.get('pid')})") + print(f" Distro: {C.WHITE}{r.get('distro','?')}{C.RESET} {support}") + print(f" Pkg mgrs: {C.CYAN}{pms}{C.RESET}") + print(f" Model: {C.CYAN}{r.get('ai_backend', 'Ollama')}{C.RESET}") + print(f" Sessions: {C.CYAN}{r.get('sessions', 0)}{C.RESET}") print(f" Log: {C.DIM}{r.get('log')}{C.RESET}") - print(f" AI: {C.CYAN}{r.get('ai_backend', 'Ollama')}{C.RESET}") + if dr.get("support_note"): + print(f"\n {C.YELLOW}⚠ {dr['support_note']}{C.RESET}") + print() def _show_distro(): if not _daemon_running(): - print(f"{C.RED}✗ Demon not running{C.RESET}"); return + print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "distro_info"}) if "error" in r: print(f"{C.RED}✗ {r['error']}{C.RESET}"); return badge = f"{C.GREEN}✓ Fully supported{C.RESET}" if r.get("supported") else f"{C.YELLOW}⚠ Limited support{C.RESET}" @@ -1196,22 +1207,12 @@ def _get_shell_history() -> list: def _repl(session: str, api_key: str = "", auto_exec: bool = False): print(_banner()) - dr = _request({"cmd": "distro_info"}) - if "error" not in dr: - badge = f"{C.GREEN}[full support]{C.RESET}" if dr.get("supported") else f"{C.YELLOW}[limited support]{C.RESET}" - pms = ", ".join(dr.get("pkg_managers",[]) or ["none detected"]) - print(f"{C.MGRAY}Session: {C.CYAN}{session}{C.RESET} | " - f"{C.WHITE}{dr.get('distro_name')}{C.RESET} {badge} | " - f"pkg: {C.CYAN}{pms}{C.RESET}") - if not dr.get("supported") and dr.get("support_note"): - print(f"{C.YELLOW}⚠ {dr['support_note']}{C.RESET}") - print(f"{C.MGRAY}Model: {C.CYAN}{OLLAMA_MODEL}{C.RESET} | Type {C.CYAN}help{C.MGRAY} for commands | {C.CYAN}Ctrl+C{C.MGRAY} to exit{C.RESET}") + print(f" {C.DIM}Type {C.RESET}{C.CYAN}help{C.DIM} for commands | {C.CYAN}Ctrl+C{C.DIM} to exit{C.RESET}") _sep() hist = _get_shell_history() if hist: _request({"cmd": "history_import", "commands": hist}) - print(f"{C.DIM}📜 Bound {len(hist)} shell commands to memory{C.RESET}\n") while True: try: @@ -1219,36 +1220,42 @@ def _repl(session: str, api_key: str = "", auto_exec: bool = False): f"{C.GREEN}you@{CLI_COMMAND}{C.RESET}{C.DGRAY}:{C.RESET}{C.CYAN}~{C.RESET}$ " ).strip() except (KeyboardInterrupt, EOFError): - print(f"\n{C.MGRAY}Goodbye. {PRODUCT_NAME} demon keeps running in background.{C.RESET}") + print(f"\n{C.MGRAY}The demon lingers.{C.RESET}") break if not user_input: continue # ── Built-in commands ────────────────────────────────────────────── - if user_input in ("exit", "quit", "q"): - print(f"{C.MGRAY}Exiting. Use '{CLI_COMMAND} stop' to halt the demon.{C.RESET}") + if user_input in ("close", "exit", "quit", "q"): + print(f"{C.MGRAY}Use '{CLI_COMMAND} stop' to shut down completely.{C.RESET}") break elif user_input == "help": print(f""" -{C.CYAN}{C.BOLD}{PRODUCT_NAME} REPL Commands:{C.RESET} - {C.GREEN}help{C.RESET} Show this help - {C.GREEN}status{C.RESET} Show demon status - {C.GREEN}distro{C.RESET} Show distro & package manager info - {C.GREEN}history{C.RESET} Show imported shell history (last 20) - {C.GREEN}clear{C.RESET} Clear current session context - {C.GREEN}exec on/off{C.RESET} Toggle auto-execution of suggested fixes - {C.GREEN}exit / quit{C.RESET} Exit REPL (demon stays running) - -{C.CYAN}Example prompts:{C.RESET} - "sudo apt upgrade gives E: dpkg was interrupted..." - "analyze my history for problems" - "how do I fix broken dependencies?" - "give me UX improvement tips" - "run the fix" / "apply it" (requires exec on) +{C.CYAN}{C.BOLD} ── Help Menu ───────────────────────────────────────────────{C.RESET} + +{C.CYAN} Conversation{C.RESET} + Type any question or error message to get a diagnosis and fix. + +{C.CYAN} Session commands{C.RESET} + {C.GREEN}exec on{C.RESET} Automatically apply suggested fixes without confirmation + {C.GREEN}exec off{C.RESET} Show suggested fixes but do not apply them (default) + {C.GREEN}clear{C.RESET} Wipe the conversation history and start fresh + {C.GREEN}history{C.RESET} Show your last 20 recorded shell commands + +{C.CYAN} System information{C.RESET} + {C.GREEN}info{C.RESET} Show status, distro, model, and package managers + {C.GREEN}hardware{C.RESET} Show detected hardware, drivers, and kernel errors + +{C.CYAN} Program control{C.RESET} + {C.GREEN}close{C.RESET} Close this session (program keeps running in background) + {C.GREEN}stop / end{C.RESET} Shut down the program completely + {C.GREEN}help{C.RESET} Show this menu +{C.CYAN} ────────────────────────────────────────────────────────────{C.RESET} """) - elif user_input == "status": _show_status() - elif user_input == "distro": _show_distro() + elif user_input == "status": _show_info() + elif user_input == "info": _show_info() + elif user_input == "distro": _show_info() elif user_input == "clear": _request({"cmd": "session_clear", "session": session}) print(f"{C.GREEN}✓ Memory wiped{C.RESET}") @@ -1373,24 +1380,15 @@ def _install_ollama() -> bool: def _setup_ollama(model: str): - """Ensure Ollama is installed, running, and the model is pulled.""" + """Ensure Ollama is installed, running, and the model is pulled — automatically.""" if not shutil.which("ollama"): - print(f" {C.YELLOW}⚠{C.RESET} Ollama is not installed.") - try: - ans = input(" Install it now? [y/n] ").strip().lower() - except (KeyboardInterrupt, EOFError): - ans = "n" - if ans == "y": - if _install_ollama(): - print(f" {C.GREEN}✓{C.RESET} Ollama installed.") - else: - print(f" {C.RED}✗{C.RESET} Install failed.") - print(f" Install manually: https://ollama.com/download") - print(f" Then run: ollama pull {model}\n") - return + print(f" {C.DIM}Ollama not found — installing...{C.RESET}") + if _install_ollama(): + print(f" {C.GREEN}✓{C.RESET} Ollama installed.") else: - print(f" Install later: https://ollama.com/download") - print(f" Then: ollama pull {model}\n") + print(f" {C.RED}✗{C.RESET} Ollama install failed.") + print(f" Install manually: https://ollama.com/download") + print(f" Then run: ollama pull {model}") return if not _ollama_running(): @@ -1400,21 +1398,14 @@ def _setup_ollama(model: str): base = model.split(":")[0] pulled = _ollama_models() if any(base in m for m in pulled): - print(f" {C.GREEN}✓{C.RESET} {model} is already downloaded.") + print(f" {C.GREEN}✓{C.RESET} {model} already downloaded.") return - print() - try: - ans = input(f" Download {model} now? (may be several GB) [y/n] ").strip().lower() - except (KeyboardInterrupt, EOFError): - ans = "n" - if ans == "y": - if _pull_model(model): - print(f" {C.GREEN}✓{C.RESET} {model} ready.") - else: - print(f" {C.YELLOW}⚠{C.RESET} Download failed. Run: ollama pull {model}") + print(f" {C.DIM}Downloading {model} — this may take several minutes...{C.RESET}") + if _pull_model(model): + print(f" {C.GREEN}✓{C.RESET} {model} ready.") else: - print(f" {C.DIM}Download later: ollama pull {model}{C.RESET}") + print(f" {C.YELLOW}⚠{C.RESET} Download failed. Run manually: ollama pull {model}") def _patch_script_model(script_name: str, old_pattern: str, new_model: str): @@ -1497,11 +1488,14 @@ def _draw(selected: int): return selected -def run_setup(): - print(_banner()) - print(f"{C.CYAN}{C.BOLD} Welcome to {PRODUCT_NAME}!{C.RESET}") - print(f" Answer a couple of quick questions and you're ready to go.\n") - _sep() +def run_setup(show_welcome: bool = True): + # Suppress banner if launched directly from installer (already shown) + if os.environ.get("DAEMONIQ_FRESH_INSTALL") != "1" and show_welcome: + print(_banner()) + if show_welcome: + print(f"\n{C.CYAN}{C.BOLD} Welcome to {PRODUCT_NAME}!{C.RESET}") + print(f" Answer a couple of quick questions and you're ready to go.\n") + _sep() cfg = _load_config() @@ -1572,18 +1566,12 @@ def _detect_distro_id() -> str: _sep() - print(f" {C.CYAN}{C.BOLD}2) Sovereign{C.RESET}") - print( " Best local quality. Needs 9GB+ RAM.") - print( " Stronger reasoning for complex problems.") - print(f" {C.DIM}(Qwen2.5 via Ollama){C.RESET}") - print() - try: tier_options = [ "Imp — Runs on most machines (4GB+ RAM) (Llama 3 via Ollama)", "Sovereign — Best local quality (9GB+ RAM) (Qwen2.5 via Ollama)", ] - tier_idx = _arrow_select("Use arrow keys to select a tier, Enter to confirm", tier_options, 0) + tier_idx = _arrow_select("Use arrow keys to select a model, Enter to confirm", tier_options, 0) except (KeyboardInterrupt, EOFError): print(f"\n{C.YELLOW} Setup cancelled. Run '{CLI_COMMAND} setup' any time.{C.RESET}\n") return @@ -1761,7 +1749,7 @@ def run_update(patch_path: str = ""): _append_changelog(patch_version, f"Manually applied patch from {patch_path}") print(f" {C.GREEN}✓{C.RESET} Patch applied — now at v{patch_version}") - print(f" {C.DIM}Restart the demon to apply changes: {CLI_COMMAND} restart{C.RESET}\n") + print(f" {C.DIM}Restart to apply: {CLI_COMMAND} restart{C.RESET}\n") def run_rollback(): @@ -1798,7 +1786,7 @@ def run_rollback(): try: _shutil.copy2(bak_path, install_path) os.chmod(install_path, 0o755) - print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart the demon: {CLI_COMMAND} restart\n") + print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart to apply: {CLI_COMMAND} restart\n") except Exception as e: print(f" {C.RED}✗{C.RESET} Rollback failed: {e}\n") @@ -1812,16 +1800,8 @@ def run_uninstall(): """ import shutil as _shutil - print(f"\n {C.CYAN}{C.BOLD}Uninstall {PRODUCT_NAME}{C.RESET}\n") - print(f" This will remove:") - print(f" {C.DIM} All scripts and config: {INSTALL_DIR}{C.RESET}") - print(f" {C.DIM} The demoniq command: ~/.local/bin/{CLI_COMMAND}{C.RESET}") - print(f" {C.DIM} The PATH entry in your shell config{C.RESET}") - print(f" {C.DIM} The systemd service (if installed){C.RESET}") - print() - try: - ans = input(f" {C.RED}Are you sure? This cannot be undone. [y/n]:{C.RESET} ").strip().lower() + ans = input(f"\n {C.RED}Uninstall {PRODUCT_NAME}? This cannot be undone. [y/n]:{C.RESET} ").strip().lower() except (KeyboardInterrupt, EOFError): print(f"\n {C.DIM}Uninstall cancelled.{C.RESET}\n") return @@ -1829,12 +1809,9 @@ def run_uninstall(): print(f" {C.DIM}Uninstall cancelled.{C.RESET}\n") return - print() - # 1. Stop the demon if _daemon_running(): _stop_daemon() - print(f" {C.GREEN}✓{C.RESET} Demon stopped") else: Path(SOCKET_PATH).unlink(missing_ok=True) Path(PID_FILE).unlink(missing_ok=True) @@ -1847,7 +1824,6 @@ def run_uninstall(): capture_output=True) os.remove(svc_file) subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) - print(f" {C.GREEN}✓{C.RESET} systemd service removed") except Exception as e: print(f" {C.YELLOW}⚠{C.RESET} Could not remove systemd service: {e}") @@ -1855,12 +1831,10 @@ def run_uninstall(): launcher = os.path.expanduser(f"~/.local/bin/{CLI_COMMAND}") if os.path.exists(launcher): os.remove(launcher) - print(f" {C.GREEN}✓{C.RESET} Removed ~/.local/bin/{CLI_COMMAND}") # 4. Remove install directory (scripts, config, backups, snapshots) if os.path.exists(INSTALL_DIR): _shutil.rmtree(INSTALL_DIR) - print(f" {C.GREEN}✓{C.RESET} Removed {INSTALL_DIR}") # 5. Remove PATH entry from shell config shell = os.environ.get("SHELL", "") @@ -1885,7 +1859,6 @@ def run_uninstall(): ] if len(filtered) < len(lines): open(rc, "w").writelines(filtered) - print(f" {C.GREEN}✓{C.RESET} Removed PATH entry from {rc}") except Exception as e: print(f" {C.YELLOW}⚠{C.RESET} Could not clean {rc}: {e}") @@ -2037,7 +2010,7 @@ def run_update(force: bool = False): _append_changelog(remote_version, changelog_notes) print(f" {C.GREEN}✓{C.RESET} Updated to v{remote_version}.") - print(f" {C.DIM}Restart the demon to apply: {CLI_COMMAND} restart{C.RESET}\n") + print(f" {C.DIM}Restart to apply: {CLI_COMMAND} restart{C.RESET}\n") def run_rollback(): @@ -2074,7 +2047,7 @@ def run_rollback(): try: _shutil.copy2(bak_path, install_path) os.chmod(install_path, 0o755) - print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart the demon: {CLI_COMMAND} restart\n") + print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart to apply: {CLI_COMMAND} restart\n") except Exception as e: print(f" {C.RED}✗{C.RESET} Rollback failed: {e}\n") @@ -2092,15 +2065,14 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( f"Examples:\n" - f" {CLI_COMMAND} start Start the demon\n" + f" {CLI_COMMAND} start Start the background process\n" f" {CLI_COMMAND} Open interactive REPL\n" f" {CLI_COMMAND} \"apt lock error\" One-shot question\n" f" {CLI_COMMAND} --exec \"fix broken pkg\" Ask and auto-apply fix\n" - f" {CLI_COMMAND} --session work Use a named session\n" - f" {CLI_COMMAND} status Check demon health\n" + f" {CLI_COMMAND} info Show status and system info\n" f" {CLI_COMMAND} distro Show distro info\n" - f" {CLI_COMMAND} stop Stop the demon\n" - f" {CLI_COMMAND} logs Tail demon logs" + f" {CLI_COMMAND} stop Stop the background process\n" + f" {CLI_COMMAND} logs View live logs" ), ) parser.add_argument("message", nargs="?", help="One-shot message (skips REPL)") @@ -2113,31 +2085,30 @@ def main(): api_key = "" # unused in local mode — Ollama needs no key - SUBCMDS = {"start","stop","restart","status","logs","history","sessions","distro","setup","hardware","version","update","rollback","uninstall"} + SUBCMDS = {"start","stop","end","restart","status","logs","history","sessions","distro","setup","hardware","version","update","rollback","uninstall","info"} if args.message in SUBCMDS: cmd = args.message if cmd == "start": _start_daemon() - elif cmd == "stop": + elif cmd in ("stop", "end"): _stop_daemon() elif cmd == "restart": _stop_daemon(); time.sleep(1); _start_daemon() elif cmd == "status": - _show_status() + _show_info() + elif cmd == "info": + _show_info() elif cmd == "distro": - if not _daemon_running(): - print(f"{C.YELLOW}⚡ Starting {PRODUCT_NAME} demon...{C.RESET}") - _start_daemon() - _show_distro() + _show_info() elif cmd == "logs": os.execvp("tail", ["tail", "-f", LOG_FILE]) elif cmd == "history": - if not _daemon_running(): print(f"{C.RED}✗ Demon not running{C.RESET}"); return + if not _daemon_running(): print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "history_get"}) for c in r.get("history", [])[-50:]: print(c) elif cmd == "sessions": - if not _daemon_running(): print(f"{C.RED}✗ Demon not running{C.RESET}"); return + if not _daemon_running(): print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "sessions_list"}) for s in r.get("sessions", []): print(s) elif cmd == "setup": @@ -2160,7 +2131,7 @@ def main(): # Ensure demon is running if not _daemon_running(): - print(f"{C.YELLOW}⚡ Starting {PRODUCT_NAME} demon...{C.RESET}") + print(f"{C.YELLOW}⚡ Starting...{C.RESET}") if not _start_daemon(): sys.exit(1) # No API key needed — check Ollama is reachable instead @@ -2173,7 +2144,7 @@ def main(): # One-shot mode if args.message: - print(f"{C.DIM}thinking...{C.RESET}", end="\r", flush=True) + print(f" {C.DIM}consulting the ether...{C.RESET}", flush=True) r = _request({ "cmd": "chat", "message": args.message, "session": args.session, "auto_exec": args.auto_exec, diff --git a/daemoniq-sovereign.py b/daemoniq-sovereign.py index ccbdb84..ea29226 100644 --- a/daemoniq-sovereign.py +++ b/daemoniq-sovereign.py @@ -21,7 +21,7 @@ PRODUCT_NAME = "DaemonIQ" PRODUCT_TAGLINE = "Linux Troubleshooting Assistant" -PRODUCT_VERSION = "0.4.4" +PRODUCT_VERSION = "0.5.0" CLI_COMMAND = "daemoniq" DAEMON_LABEL = "daemoniq-demon" AI_PERSONA = PRODUCT_NAME @@ -500,8 +500,8 @@ def detect_distro(): "network_hw": ["lspci", "-nnk", "-d", "::0200"], # Network controllers "audio_hw": ["lspci", "-nnk", "-d", "::0401"], # Audio devices "block_devices": ["lsblk", "-o", "NAME,MODEL,TRAN,SIZE"], - "cpu_info": ["grep", "-m4", "model name\|cpu MHz\|siblings\|cpu cores", "/proc/cpuinfo"], - "memory_info": ["grep", "MemTotal\|MemAvailable", "/proc/meminfo"], + "cpu_info": ["sh", "-c", "grep -m4 'model name|cpu MHz|siblings|cpu cores' /proc/cpuinfo"], + "memory_info": ["sh", "-c", "grep 'MemTotal|MemAvailable' /proc/meminfo"], "firmware_info": ["dmesg", "-t", "-l", "err"], } @@ -772,7 +772,7 @@ def _call_api(sid: str, message: str, api_key: str = "") -> str: headers={"Content-Type": "application/json"}, ) # Qwen2.5 larger models can be slower — generous timeout - with urllib.request.urlopen(req, timeout=180) as resp: + with urllib.request.urlopen(req, timeout=600) as resp: data = json.loads(resp.read()) reply = data.get("message", {}).get("content", "") @@ -792,7 +792,7 @@ def _execute(block: ExecBlock) -> str: lines.append(f"$ {cmd}") try: r = subprocess.run( - cmd, shell=True, capture_output=True, text=True, timeout=120, + cmd, shell=True, capture_output=True, text=True, timeout=300, env={**os.environ, "DEBIAN_FRONTEND": "noninteractive"}, ) if r.stdout.strip(): lines.append(r.stdout.strip()) @@ -1013,7 +1013,7 @@ def _sep(): # ── Socket communication ────────────────────────────────────────────────────── -def _request(req: dict, timeout: int = 90) -> dict: +def _request(req: dict, timeout: int = 660) -> dict: try: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.settimeout(timeout) @@ -1029,9 +1029,9 @@ def _request(req: dict, timeout: int = 90) -> dict: s.close() return json.loads(data.decode()) except FileNotFoundError: - return {"error": f"Demon not running. Start with: {CLI_COMMAND} start"} + return {"error": f"Not running. Start with: {CLI_COMMAND} start"} except ConnectionRefusedError: - return {"error": f"Demon not responding. Try: {CLI_COMMAND} restart"} + return {"error": f"Not responding. Try: {CLI_COMMAND} restart"} except socket.timeout: return {"error": "Request timed out"} except Exception as e: @@ -1047,7 +1047,7 @@ def _daemon_running() -> bool: # ── Demon lifecycle ────────────────────────────────────────────────────────── def _start_daemon(foreground=False): if _daemon_running(): - print(f"{C.YELLOW}⚠ Demon already running{C.RESET}"); return True + print(f"{C.YELLOW}⚠ Already running{C.RESET}"); return True try: if foreground: os.execv(sys.executable, [sys.executable, __file__, "_daemon_fg"]) @@ -1061,13 +1061,13 @@ def _start_daemon(foreground=False): time.sleep(0.3) if _daemon_running(): pid = Path(PID_FILE).read_text().strip() if Path(PID_FILE).exists() else "?" - print(f"{C.GREEN}✓ {PRODUCT_NAME} demon started (PID {pid}){C.RESET}") + print(f"{C.GREEN}✓ {PRODUCT_NAME} started{C.RESET}") print(f"{C.DIM} Logs: tail -f {LOG_FILE}{C.RESET}") return True - print(f"{C.RED}✗ Demon failed to start. Check: tail {LOG_FILE}{C.RESET}") + print(f"{C.RED}✗ Failed to start. Check logs: {CLI_COMMAND} logs{C.RESET}") return False except Exception as e: - print(f"{C.RED}✗ Could not start demon: {e}{C.RESET}"); return False + print(f"{C.RED}✗ Could not start: {e}{C.RESET}"); return False def _stop_daemon(): if not Path(PID_FILE).exists(): @@ -1075,7 +1075,7 @@ def _stop_daemon(): try: pid = int(Path(PID_FILE).read_text().strip()) os.kill(pid, 15) - print(f"{C.GREEN}✓ Demon stopped (PID {pid}){C.RESET}") + print(f"{C.GREEN}✓ Stopped{C.RESET}") except ProcessLookupError: print(f"{C.YELLOW}⚠ Process not found, cleaning up...{C.RESET}") for f in [PID_FILE, SOCKET_PATH]: Path(f).unlink(missing_ok=True) @@ -1091,8 +1091,8 @@ def _show_hardware(): try: snap = json.load(open(HARDWARE_SNAPSHOT_FILE)) except Exception: - print(f"{C.RED}✗ Demon not running and no cached snapshot found.{C.RESET}") - print(f" Start the demon first: {C.CYAN}{CLI_COMMAND} start{C.RESET}") + print(f"{C.RED}✗ Not running and no cached snapshot found.{C.RESET}") + print(f" Start it first: {C.CYAN}{CLI_COMMAND} start{C.RESET}") return else: r = _request({"cmd": "hardware"}) @@ -1134,7 +1134,7 @@ def _show_hardware(): if len(lines) > 15: for line in lines[:15]: print(f" {C.MGRAY}{line}{C.RESET}") - print(f" {C.DIM}... ({len(lines) - 15} more lines — see demon log for full output){C.RESET}") + print(f" {C.DIM}... ({len(lines) - 15} more lines — run '{CLI_COMMAND} logs' for full output){C.RESET}") else: for line in lines: print(f" {C.MGRAY}{line}{C.RESET}") @@ -1142,22 +1142,33 @@ def _show_hardware(): def _show_status(): + _show_info() + +def _show_info(): if not _daemon_running(): - print(f"{C.RED}✗ Demon is NOT running{C.RESET}") - print(f" Start with: {C.CYAN}{CLI_COMMAND} start{C.RESET}"); return - r = _request({"cmd": "status"}) + print(f"\n {C.RED}✗ Background process is not running{C.RESET}") + print(f" Start with: {C.CYAN}{CLI_COMMAND} start{C.RESET}\n"); return + r = _request({"cmd": "status"}) + dr = _request({"cmd": "distro_info"}) if "error" in r: print(f"{C.RED}✗ {r['error']}{C.RESET}"); return - badge = f"{C.GREEN}[full support]{C.RESET}" if r.get("supported") else f"{C.YELLOW}[limited support]{C.RESET}" - print(f"{C.GREEN}✓ Demon running{C.RESET} PID: {C.BOLD}{r.get('pid')}{C.RESET}") - print(f" Distro: {C.CYAN}{r.get('distro','?')}{C.RESET} {badge}") - print(f" Sessions: {C.CYAN}{r.get('sessions',0)}{C.RESET}") - print(f" History: {C.CYAN}{r.get('history_entries',0)} commands{C.RESET}") + + support = f"{C.GREEN}✓ Full support{C.RESET}" if r.get("supported") else f"{C.YELLOW}⚠ Limited support{C.RESET}" + pms = ", ".join(dr.get("pkg_managers", []) or ["none detected"]) if "error" not in dr else "unknown" + + print(f"\n {C.CYAN}{C.BOLD}DaemonIQ Status{C.RESET}\n") + print(f" Process: {C.GREEN}running{C.RESET} (PID {r.get('pid')})") + print(f" Distro: {C.WHITE}{r.get('distro','?')}{C.RESET} {support}") + print(f" Pkg mgrs: {C.CYAN}{pms}{C.RESET}") + print(f" Model: {C.CYAN}{r.get('ai_backend', 'Ollama')}{C.RESET}") + print(f" Sessions: {C.CYAN}{r.get('sessions', 0)}{C.RESET}") print(f" Log: {C.DIM}{r.get('log')}{C.RESET}") - print(f" AI: {C.CYAN}{r.get('ai_backend', 'Ollama')}{C.RESET}") + if dr.get("support_note"): + print(f"\n {C.YELLOW}⚠ {dr['support_note']}{C.RESET}") + print() def _show_distro(): if not _daemon_running(): - print(f"{C.RED}✗ Demon not running{C.RESET}"); return + print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "distro_info"}) if "error" in r: print(f"{C.RED}✗ {r['error']}{C.RESET}"); return badge = f"{C.GREEN}✓ Fully supported{C.RESET}" if r.get("supported") else f"{C.YELLOW}⚠ Limited support{C.RESET}" @@ -1237,7 +1248,6 @@ def _repl(session: str, api_key: str = "", auto_exec: bool = False): hist = _get_shell_history() if hist: _request({"cmd": "history_import", "commands": hist}) - print(f"{C.DIM}📜 Bound {len(hist)} shell commands to memory{C.RESET}\n") while True: try: @@ -1245,36 +1255,42 @@ def _repl(session: str, api_key: str = "", auto_exec: bool = False): f"{C.GREEN}you@{CLI_COMMAND}{C.RESET}{C.DGRAY}:{C.RESET}{C.CYAN}~{C.RESET}$ " ).strip() except (KeyboardInterrupt, EOFError): - print(f"\n{C.MGRAY}Goodbye. {PRODUCT_NAME} demon keeps running in background.{C.RESET}") + print(f"\n{C.MGRAY}The demon lingers.{C.RESET}") break if not user_input: continue # ── Built-in commands ────────────────────────────────────────────── - if user_input in ("exit", "quit", "q"): - print(f"{C.MGRAY}Exiting. Use '{CLI_COMMAND} stop' to halt the demon.{C.RESET}") + if user_input in ("close", "exit", "quit", "q"): + print(f"{C.MGRAY}Use '{CLI_COMMAND} stop' to shut down completely.{C.RESET}") break elif user_input == "help": print(f""" -{C.CYAN}{C.BOLD}{PRODUCT_NAME} REPL Commands:{C.RESET} - {C.GREEN}help{C.RESET} Show this help - {C.GREEN}status{C.RESET} Show demon status - {C.GREEN}distro{C.RESET} Show distro & package manager info - {C.GREEN}history{C.RESET} Show imported shell history (last 20) - {C.GREEN}clear{C.RESET} Clear current session context - {C.GREEN}exec on/off{C.RESET} Toggle auto-execution of suggested fixes - {C.GREEN}exit / quit{C.RESET} Exit REPL (demon stays running) - -{C.CYAN}Example prompts:{C.RESET} - "sudo apt upgrade gives E: dpkg was interrupted..." - "analyze my history for problems" - "how do I fix broken dependencies?" - "give me UX improvement tips" - "run the fix" / "apply it" (requires exec on) +{C.CYAN}{C.BOLD} ── Help Menu ───────────────────────────────────────────────{C.RESET} + +{C.CYAN} Conversation{C.RESET} + Type any question or error message to get a diagnosis and fix. + +{C.CYAN} Session commands{C.RESET} + {C.GREEN}exec on{C.RESET} Automatically apply suggested fixes without confirmation + {C.GREEN}exec off{C.RESET} Show suggested fixes but do not apply them (default) + {C.GREEN}clear{C.RESET} Wipe the conversation history and start fresh + {C.GREEN}history{C.RESET} Show your last 20 recorded shell commands + +{C.CYAN} System information{C.RESET} + {C.GREEN}info{C.RESET} Show status, distro, model, and package managers + {C.GREEN}hardware{C.RESET} Show detected hardware, drivers, and kernel errors + +{C.CYAN} Program control{C.RESET} + {C.GREEN}close{C.RESET} Close this session (program keeps running in background) + {C.GREEN}stop / end{C.RESET} Shut down the program completely + {C.GREEN}help{C.RESET} Show this menu +{C.CYAN} ────────────────────────────────────────────────────────────{C.RESET} """) - elif user_input == "status": _show_status() - elif user_input == "distro": _show_distro() + elif user_input == "status": _show_info() + elif user_input == "info": _show_info() + elif user_input == "distro": _show_info() elif user_input == "clear": _request({"cmd": "session_clear", "session": session}) print(f"{C.GREEN}✓ Memory wiped{C.RESET}") @@ -1399,24 +1415,15 @@ def _install_ollama() -> bool: def _setup_ollama(model: str): - """Ensure Ollama is installed, running, and the model is pulled.""" + """Ensure Ollama is installed, running, and the model is pulled — automatically.""" if not shutil.which("ollama"): - print(f" {C.YELLOW}⚠{C.RESET} Ollama is not installed.") - try: - ans = input(" Install it now? [y/n] ").strip().lower() - except (KeyboardInterrupt, EOFError): - ans = "n" - if ans == "y": - if _install_ollama(): - print(f" {C.GREEN}✓{C.RESET} Ollama installed.") - else: - print(f" {C.RED}✗{C.RESET} Install failed.") - print(f" Install manually: https://ollama.com/download") - print(f" Then run: ollama pull {model}\n") - return + print(f" {C.DIM}Ollama not found — installing...{C.RESET}") + if _install_ollama(): + print(f" {C.GREEN}✓{C.RESET} Ollama installed.") else: - print(f" Install later: https://ollama.com/download") - print(f" Then: ollama pull {model}\n") + print(f" {C.RED}✗{C.RESET} Ollama install failed.") + print(f" Install manually: https://ollama.com/download") + print(f" Then run: ollama pull {model}") return if not _ollama_running(): @@ -1426,21 +1433,14 @@ def _setup_ollama(model: str): base = model.split(":")[0] pulled = _ollama_models() if any(base in m for m in pulled): - print(f" {C.GREEN}✓{C.RESET} {model} is already downloaded.") + print(f" {C.GREEN}✓{C.RESET} {model} already downloaded.") return - print() - try: - ans = input(f" Download {model} now? (may be several GB) [y/n] ").strip().lower() - except (KeyboardInterrupt, EOFError): - ans = "n" - if ans == "y": - if _pull_model(model): - print(f" {C.GREEN}✓{C.RESET} {model} ready.") - else: - print(f" {C.YELLOW}⚠{C.RESET} Download failed. Run: ollama pull {model}") + print(f" {C.DIM}Downloading {model} — this may take several minutes...{C.RESET}") + if _pull_model(model): + print(f" {C.GREEN}✓{C.RESET} {model} ready.") else: - print(f" {C.DIM}Download later: ollama pull {model}{C.RESET}") + print(f" {C.YELLOW}⚠{C.RESET} Download failed. Run manually: ollama pull {model}") def _patch_script_model(script_name: str, old_pattern: str, new_model: str): @@ -1523,11 +1523,14 @@ def _draw(selected: int): return selected -def run_setup(): - print(_banner()) - print(f"{C.CYAN}{C.BOLD} Welcome to {PRODUCT_NAME}!{C.RESET}") - print(f" Answer a couple of quick questions and you're ready to go.\n") - _sep() +def run_setup(show_welcome: bool = True): + # Suppress banner if launched directly from installer (already shown) + if os.environ.get("DAEMONIQ_FRESH_INSTALL") != "1" and show_welcome: + print(_banner()) + if show_welcome: + print(f"\n{C.CYAN}{C.BOLD} Welcome to {PRODUCT_NAME}!{C.RESET}") + print(f" Answer a couple of quick questions and you're ready to go.\n") + _sep() cfg = _load_config() @@ -1598,18 +1601,12 @@ def _detect_distro_id() -> str: _sep() - print(f" {C.CYAN}{C.BOLD}2) Sovereign{C.RESET}") - print( " Best local quality. Needs 9GB+ RAM.") - print( " Stronger reasoning for complex problems.") - print(f" {C.DIM}(Qwen2.5 via Ollama){C.RESET}") - print() - try: tier_options = [ "Imp — Runs on most machines (4GB+ RAM) (Llama 3 via Ollama)", "Sovereign — Best local quality (9GB+ RAM) (Qwen2.5 via Ollama)", ] - tier_idx = _arrow_select("Use arrow keys to select a tier, Enter to confirm", tier_options, 0) + tier_idx = _arrow_select("Use arrow keys to select a model, Enter to confirm", tier_options, 0) except (KeyboardInterrupt, EOFError): print(f"\n{C.YELLOW} Setup cancelled. Run '{CLI_COMMAND} setup' any time.{C.RESET}\n") return @@ -1787,7 +1784,7 @@ def run_update(patch_path: str = ""): _append_changelog(patch_version, f"Manually applied patch from {patch_path}") print(f" {C.GREEN}✓{C.RESET} Patch applied — now at v{patch_version}") - print(f" {C.DIM}Restart the demon to apply changes: {CLI_COMMAND} restart{C.RESET}\n") + print(f" {C.DIM}Restart to apply: {CLI_COMMAND} restart{C.RESET}\n") def run_rollback(): @@ -1824,7 +1821,7 @@ def run_rollback(): try: _shutil.copy2(bak_path, install_path) os.chmod(install_path, 0o755) - print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart the demon: {CLI_COMMAND} restart\n") + print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart to apply: {CLI_COMMAND} restart\n") except Exception as e: print(f" {C.RED}✗{C.RESET} Rollback failed: {e}\n") @@ -1838,16 +1835,8 @@ def run_uninstall(): """ import shutil as _shutil - print(f"\n {C.CYAN}{C.BOLD}Uninstall {PRODUCT_NAME}{C.RESET}\n") - print(f" This will remove:") - print(f" {C.DIM} All scripts and config: {INSTALL_DIR}{C.RESET}") - print(f" {C.DIM} The demoniq command: ~/.local/bin/{CLI_COMMAND}{C.RESET}") - print(f" {C.DIM} The PATH entry in your shell config{C.RESET}") - print(f" {C.DIM} The systemd service (if installed){C.RESET}") - print() - try: - ans = input(f" {C.RED}Are you sure? This cannot be undone. [y/n]:{C.RESET} ").strip().lower() + ans = input(f"\n {C.RED}Uninstall {PRODUCT_NAME}? This cannot be undone. [y/n]:{C.RESET} ").strip().lower() except (KeyboardInterrupt, EOFError): print(f"\n {C.DIM}Uninstall cancelled.{C.RESET}\n") return @@ -1855,12 +1844,9 @@ def run_uninstall(): print(f" {C.DIM}Uninstall cancelled.{C.RESET}\n") return - print() - # 1. Stop the demon if _daemon_running(): _stop_daemon() - print(f" {C.GREEN}✓{C.RESET} Demon stopped") else: Path(SOCKET_PATH).unlink(missing_ok=True) Path(PID_FILE).unlink(missing_ok=True) @@ -1873,7 +1859,6 @@ def run_uninstall(): capture_output=True) os.remove(svc_file) subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) - print(f" {C.GREEN}✓{C.RESET} systemd service removed") except Exception as e: print(f" {C.YELLOW}⚠{C.RESET} Could not remove systemd service: {e}") @@ -1881,12 +1866,10 @@ def run_uninstall(): launcher = os.path.expanduser(f"~/.local/bin/{CLI_COMMAND}") if os.path.exists(launcher): os.remove(launcher) - print(f" {C.GREEN}✓{C.RESET} Removed ~/.local/bin/{CLI_COMMAND}") # 4. Remove install directory (scripts, config, backups, snapshots) if os.path.exists(INSTALL_DIR): _shutil.rmtree(INSTALL_DIR) - print(f" {C.GREEN}✓{C.RESET} Removed {INSTALL_DIR}") # 5. Remove PATH entry from shell config shell = os.environ.get("SHELL", "") @@ -1911,7 +1894,6 @@ def run_uninstall(): ] if len(filtered) < len(lines): open(rc, "w").writelines(filtered) - print(f" {C.GREEN}✓{C.RESET} Removed PATH entry from {rc}") except Exception as e: print(f" {C.YELLOW}⚠{C.RESET} Could not clean {rc}: {e}") @@ -2063,7 +2045,7 @@ def run_update(force: bool = False): _append_changelog(remote_version, changelog_notes) print(f" {C.GREEN}✓{C.RESET} Updated to v{remote_version}.") - print(f" {C.DIM}Restart the demon to apply: {CLI_COMMAND} restart{C.RESET}\n") + print(f" {C.DIM}Restart to apply: {CLI_COMMAND} restart{C.RESET}\n") def run_rollback(): @@ -2100,7 +2082,7 @@ def run_rollback(): try: _shutil.copy2(bak_path, install_path) os.chmod(install_path, 0o755) - print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart the demon: {CLI_COMMAND} restart\n") + print(f" {C.GREEN}✓{C.RESET} Rolled back. Restart to apply: {CLI_COMMAND} restart\n") except Exception as e: print(f" {C.RED}✗{C.RESET} Rollback failed: {e}\n") @@ -2118,15 +2100,14 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( f"Examples:\n" - f" {CLI_COMMAND} start Start the demon\n" + f" {CLI_COMMAND} start Start the background process\n" f" {CLI_COMMAND} Open interactive REPL\n" f" {CLI_COMMAND} \"apt lock error\" One-shot question\n" f" {CLI_COMMAND} --exec \"fix broken pkg\" Ask and auto-apply fix\n" - f" {CLI_COMMAND} --session work Use a named session\n" - f" {CLI_COMMAND} status Check demon health\n" + f" {CLI_COMMAND} info Show status and system info\n" f" {CLI_COMMAND} distro Show distro info\n" - f" {CLI_COMMAND} stop Stop the demon\n" - f" {CLI_COMMAND} logs Tail demon logs" + f" {CLI_COMMAND} stop Stop the background process\n" + f" {CLI_COMMAND} logs View live logs" ), ) parser.add_argument("message", nargs="?", help="One-shot message (skips REPL)") @@ -2139,31 +2120,30 @@ def main(): api_key = "" # unused in local mode — Ollama needs no key - SUBCMDS = {"start","stop","restart","status","logs","history","sessions","distro","setup","hardware","version","update","rollback","uninstall"} + SUBCMDS = {"start","stop","end","restart","status","logs","history","sessions","distro","setup","hardware","version","update","rollback","uninstall","info"} if args.message in SUBCMDS: cmd = args.message if cmd == "start": _start_daemon() - elif cmd == "stop": + elif cmd in ("stop", "end"): _stop_daemon() elif cmd == "restart": _stop_daemon(); time.sleep(1); _start_daemon() elif cmd == "status": - _show_status() + _show_info() + elif cmd == "info": + _show_info() elif cmd == "distro": - if not _daemon_running(): - print(f"{C.YELLOW}⚡ Starting {PRODUCT_NAME} demon...{C.RESET}") - _start_daemon() - _show_distro() + _show_info() elif cmd == "logs": os.execvp("tail", ["tail", "-f", LOG_FILE]) elif cmd == "history": - if not _daemon_running(): print(f"{C.RED}✗ Demon not running{C.RESET}"); return + if not _daemon_running(): print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "history_get"}) for c in r.get("history", [])[-50:]: print(c) elif cmd == "sessions": - if not _daemon_running(): print(f"{C.RED}✗ Demon not running{C.RESET}"); return + if not _daemon_running(): print(f"{C.RED}✗ Not running{C.RESET}"); return r = _request({"cmd": "sessions_list"}) for s in r.get("sessions", []): print(s) elif cmd == "setup": @@ -2186,7 +2166,7 @@ def main(): # Ensure demon is running if not _daemon_running(): - print(f"{C.YELLOW}⚡ Starting {PRODUCT_NAME} demon...{C.RESET}") + print(f"{C.YELLOW}⚡ Starting...{C.RESET}") if not _start_daemon(): sys.exit(1) # No API key needed — check Ollama is reachable instead @@ -2199,7 +2179,7 @@ def main(): # One-shot mode if args.message: - print(f"{C.DIM}thinking...{C.RESET}", end="\r", flush=True) + print(f" {C.DIM}consulting the ether...{C.RESET}", flush=True) r = _request({ "cmd": "chat", "message": args.message, "session": args.session, "auto_exec": args.auto_exec, diff --git a/install.sh b/install.sh index e87e6a1..25ad6be 100644 --- a/install.sh +++ b/install.sh @@ -62,12 +62,10 @@ if [ "$PYMAJ" -lt 3 ] || { [ "$PYMAJ" -eq 3 ] && [ "$PYMIN" -lt 8 ]; }; then err "Python $PYVER found — ${PRODUCT} requires Python 3.8+." exit 1 fi -ok "Python $PYVER" # ══════════════════════════════════════════════════════════════════════════════ # 2 — Download or copy scripts # ══════════════════════════════════════════════════════════════════════════════ -hdr "Installing ${PRODUCT}" mkdir -p "$INSTALL_DIR" "$BIN_DIR" @@ -95,11 +93,9 @@ for script in "${SCRIPTS[@]}"; do if [ -n "$LOCAL" ]; then cp "$LOCAL" "$INSTALL_DIR/$script" chmod +x "$INSTALL_DIR/$script" - ok "Installed $script" INSTALLED=$((INSTALLED + 1)) elif _download "$script" "$INSTALL_DIR/$script"; then chmod +x "$INSTALL_DIR/$script" - ok "Downloaded $script" INSTALLED=$((INSTALLED + 1)) else warn "Could not get $script — skipping" @@ -138,12 +134,10 @@ SCRIPT="\$INSTALL_DIR/\$ACTIVE" exec python3 "\$SCRIPT" "\$@" LAUNCHER_EOF chmod +x "$LAUNCHER" -ok "Created 'daemoniq' command" # ══════════════════════════════════════════════════════════════════════════════ -# 4 — PATH (silent — no prompt) +# 4 — PATH # ══════════════════════════════════════════════════════════════════════════════ -hdr "Configuring PATH" SHELL_RC="" case "${SHELL:-}" in @@ -152,9 +146,7 @@ case "${SHELL:-}" in *) SHELL_RC="$HOME/.bashrc" ;; esac -if [[ ":${PATH}:" == *":${BIN_DIR}:"* ]]; then - ok "Already in PATH" -else +if [[ ":${PATH}:" != *":${BIN_DIR}:"* ]]; then if [ -n "$SHELL_RC" ] && ! grep -q "local/bin" "$SHELL_RC" 2>/dev/null; then if [[ "${SHELL:-}" == */fish ]]; then echo 'fish_add_path $HOME/.local/bin' >> "$SHELL_RC" @@ -163,14 +155,13 @@ else fi fi export PATH="$BIN_DIR:$PATH" - ok "Added ~/.local/bin to PATH in ${SHELL_RC##*/}" fi # ══════════════════════════════════════════════════════════════════════════════ # 5 — Auto-start on login (one optional question) # ══════════════════════════════════════════════════════════════════════════════ if command -v systemctl &>/dev/null && [ "${EUID:-$(id -u)}" -ne 0 ]; then - hdr "Auto-start on login (optional)" + echo "" read -r -p " Start DaemonIQ automatically when you log in? [y/n] " ans if [[ "${ans:-}" =~ ^[Yy]$ ]]; then SVCDIR="$HOME/.config/systemd/user" @@ -193,9 +184,6 @@ if command -v systemctl &>/dev/null && [ "${EUID:-$(id -u)}" -ne 0 ]; then } > "$SVCDIR/${DAEMON_LABEL}.service" systemctl --user daemon-reload systemctl --user enable "${DAEMON_LABEL}.service" 2>/dev/null || true - ok "Auto-start enabled" - else - info "Skipped — DaemonIQ starts on demand when you run it." fi fi @@ -209,10 +197,5 @@ echo "" # Add to PATH for this session so we can run setup immediately export PATH="$BIN_DIR:$PATH" -echo -e " Starting setup wizard...${R}" -echo "" -python3 "$INSTALL_DIR/daemoniq-imp.py" setup +DAEMONIQ_FRESH_INSTALL=1 python3 "$INSTALL_DIR/daemoniq-imp.py" setup -echo "" -echo -e "${GREEN}${BOLD} Ready. Run ${CYAN}daemoniq${GREEN} in a new terminal to start.${R}" -echo "" diff --git a/manifest.json b/manifest.json index ccc2f15..c0845ba 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.4.4", + "version": "0.5.5", "released": "2026-03-19", "variants": { "daemoniq-imp.py": "daemoniq-imp.py", @@ -7,5 +7,5 @@ }, "install_script": "install.sh", "min_python": "3.8", - "changelog": "- Added first-run setup wizard (Cloud / Light / Heavy selection)\n- Added distro selection step with auto-detection from /etc/os-release\n- Added hardware scanner (lspci, lsusb, lsmod, dmesg, DKMS, nvidia-smi)\n- Added daemoniq hardware command\n- Added deep driver knowledge (GPU, Wi-Fi, audio, DKMS, Secure Boot, firmware)\n- Added version control system (daemoniq version, update, rollback)\n- Added daemoniq uninstall command for clean self-removal\n- Simplified installer \u2014 silent install, one optional prompt\n- Connected update system to JayRaj21/DaemonIQ on GitHub\n- Fixed NameError: import os missing before hardware snapshot definition\n- Fixed GitHub branch URL (main \u2192 master)\n- Replaced automatic update system with manual patch application\n- daemoniq update now accepts a local patch file path\n- Removed network dependency from update command\n- Removed Cloud (Groq API) variant \u2014 no API dependencies\n- Renamed variants: light \u2192 imp, heavy \u2192 daemon\n- Renamed 'daemon' background process to 'demon' throughout\n- Renamed heavy variant from daemon to herald (avoids confusion with background process)\n- Applied subtle thematic naming (consulting the ether, memory wiped, etc.)\n- DAEMON_LABEL updated to daemoniq-demon (runtime paths)\n- Renamed herald variant to sovereign\n- New banner art with flame wisps and \u26e7 sigil accents\n- Default distro selection now correctly uses auto-detected distro\n- Updated banner with larger art and subtle demonic accents\n- Fixed setup wizard distro default to always use auto-detected distro\n- Redesigned banner with demon horns framing the logo\n- Tagline updated with occult sigil characters\n- Setup wizard distro default now always uses detected distro (falls back to Ubuntu)\n- Redesigned banner: full DaemonIQ name in ANSI Shadow, pentacle (\u26e4) corner decoration\n- Setup wizard distro default now correctly uses auto-detected distro\n- Banner redesigned: full DaemonIQ name, trident motifs flanking each side, red colour\n- Banner: full DaemonIQ in red, trident motifs flanking each side\n- Fixed banner: replaced Unicode box-drawing chars with plain ASCII (wider terminal compatibility)\n- Fixed installer: auto-start prompt changed from [y/N] to [y/n]\n- Restored block letter (\u2588\u2588) banner text with ASCII-only trident motif\n- Fixed NameError: _setup_ollama was missing from setup wizard\n- Fixed _pull_model calling 'imp' instead of 'ollama' (bad rename collision)\n- Replaced trident motif with ASCII pentacle on banner\n- Fixed installer banner to match program banner style\n- Fixed all [y/N] prompts to [y/n] across installer and program\n- Fixed pentacle layout: moved to corner placement, no longer overflows\n- Added arrow key navigation for all setup wizard selections\n- Combined install and setup into a single flow \u2014 no second step needed\n- Installer banner updated to match program banner style\n- Removed pentacle decoration from banner \u2014 clean block letters only\n- Redesigned I in banner: wider serif style to distinguish it from adjacent letters\n- Removed all motif decorations from banner \u2014 clean block letters only" + "changelog": "- Added first-run setup wizard (Cloud / Light / Heavy selection)\n- Added distro selection step with auto-detection from /etc/os-release\n- Added hardware scanner (lspci, lsusb, lsmod, dmesg, DKMS, nvidia-smi)\n- Added daemoniq hardware command\n- Added deep driver knowledge (GPU, Wi-Fi, audio, DKMS, Secure Boot, firmware)\n- Added version control system (daemoniq version, update, rollback)\n- Added daemoniq uninstall command for clean self-removal\n- Simplified installer \u2014 silent install, one optional prompt\n- Connected update system to JayRaj21/DaemonIQ on GitHub\n- Fixed NameError: import os missing before hardware snapshot definition\n- Fixed GitHub branch URL (main \u2192 master)\n- Replaced automatic update system with manual patch application\n- daemoniq update now accepts a local patch file path\n- Removed network dependency from update command\n- Removed Cloud (Groq API) variant \u2014 no API dependencies\n- Renamed variants: light \u2192 imp, heavy \u2192 daemon\n- Renamed 'daemon' background process to 'demon' throughout\n- Renamed heavy variant from daemon to herald (avoids confusion with background process)\n- Applied subtle thematic naming (consulting the ether, memory wiped, etc.)\n- DAEMON_LABEL updated to daemoniq-demon (runtime paths)\n- Renamed herald variant to sovereign\n- New banner art with flame wisps and \u26e7 sigil accents\n- Default distro selection now correctly uses auto-detected distro\n- Updated banner with larger art and subtle demonic accents\n- Fixed setup wizard distro default to always use auto-detected distro\n- Redesigned banner with demon horns framing the logo\n- Tagline updated with occult sigil characters\n- Setup wizard distro default now always uses detected distro (falls back to Ubuntu)\n- Redesigned banner: full DaemonIQ name in ANSI Shadow, pentacle (\u26e4) corner decoration\n- Setup wizard distro default now correctly uses auto-detected distro\n- Banner redesigned: full DaemonIQ name, trident motifs flanking each side, red colour\n- Banner: full DaemonIQ in red, trident motifs flanking each side\n- Fixed banner: replaced Unicode box-drawing chars with plain ASCII (wider terminal compatibility)\n- Fixed installer: auto-start prompt changed from [y/N] to [y/n]\n- Restored block letter (\u2588\u2588) banner text with ASCII-only trident motif\n- Fixed NameError: _setup_ollama was missing from setup wizard\n- Fixed _pull_model calling 'imp' instead of 'ollama' (bad rename collision)\n- Replaced trident motif with ASCII pentacle on banner\n- Fixed installer banner to match program banner style\n- Fixed all [y/N] prompts to [y/n] across installer and program\n- Fixed pentacle layout: moved to corner placement, no longer overflows\n- Added arrow key navigation for all setup wizard selections\n- Combined install and setup into a single flow \u2014 no second step needed\n- Installer banner updated to match program banner style\n- Removed pentacle decoration from banner \u2014 clean block letters only\n- Redesigned I in banner: wider serif style to distinguish it from adjacent letters\n- Removed all motif decorations from banner \u2014 clean block letters only\n- Fixed SyntaxWarning: invalid escape sequence on startup (grep commands)\n- Removed duplicate banner in setup wizard when launched from installer\n- Removed 'Starting setup wizard...' message \u2014 seamless install\u2192setup transition\n- Removed leftover Sovereign text block from model selection\n- Renamed 'tier' to 'model' in setup wizard prompt\n- Model download is now automatic \u2014 no prompt required\n- Removed distro/model/pkg info header from REPL \u2014 use 'info' command instead\n- Removed 'Bound N shell commands to memory' startup message\n- Merged status and distro commands into single 'info' command\n- Increased Ollama request timeout from 120s to 600s for CPU-only machines\n- Increased socket timeout from 90s to 660s\n- 'consulting the ether...' indicator now stays visible during long requests\n- Fixed SyntaxWarning on startup (hardware scanner grep escape sequences)\n- Clarified exit vs stop in help text\n- Renamed 'exit' to 'close' in REPL for clarity (exit/quit still work)\n- Simplified uninstall \u2014 single confirmation prompt, no verbose item list\n- Rewrote help menu: proper title, grouped commands, clear descriptions, removed session lines\n- Help menu: close/quit/q shown as one entry\n- Help menu: stop/end shown as one entry, 'end' added as alias for stop" } \ No newline at end of file From 00edda1354382466d7e6ded055af372603bd40fb Mon Sep 17 00:00:00 2001 From: Jayaram Rajgopal <101493919+JayRaj21@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:08:58 -0700 Subject: [PATCH 2/2] Updated readme --- README.md | 94 +++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 4592f43..9887cdb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DaemonIQ -A Linux troubleshooting assistant that runs as a background demon on your machine. Describe a problem in plain English — it diagnoses it, suggests a fix, and can apply the fix if you ask it to. It runs as a background process, always ready. +A Linux troubleshooting assistant that runs as a background process on your machine. Describe a problem in plain English — it diagnoses it, suggests a fix, and can apply the fix if you ask it to. No cloud API. No account required. Runs entirely on your hardware using a local AI model via [Ollama](https://ollama.com). @@ -65,14 +65,13 @@ Built-in knowledge covers `apt`, `dpkg`, `pip`, `snap`, and `flatpak`; NVIDIA, A ## Commands -### Interactive session +### Starting DaemonIQ | Command | Description | |---------|-------------| | `daemoniq` | Start an interactive session | -| `daemoniq "question"` | One-shot question, no session | +| `daemoniq "question"` | Ask a single question and exit | | `daemoniq --exec "question"` | Ask and automatically apply the fix | -| `daemoniq --session NAME "question"` | Ask within a named session | | `daemoniq --no-color` | Disable colour output | ### Inside a session @@ -81,32 +80,33 @@ Built-in knowledge covers `apt`, `dpkg`, `pip`, `snap`, and `flatpak`; NVIDIA, A |---------|-------------| | `exec on` | Apply fixes automatically when suggested | | `exec off` | Show fixes without applying them (default) | -| `clear` | Clear conversation history for this session | -| `history` | Show the last 20 recorded shell commands | -| `status` | Show whether the background process is running and which model is active | -| `distro` | Show detected distro and package managers | -| `help` | List available commands | -| `exit` | Close the interactive session (the background process keeps running) | +| `clear` | Wipe the conversation history and start fresh | +| `history` | Show your last 20 recorded shell commands | +| `info` | Show status, distro, model, and package managers | +| `hardware` | Show detected hardware, drivers, and kernel errors | +| `help` | Show the help menu | +| `close` | Close this session (program keeps running in background) | +| `stop` | Shut down the program completely | + +> `close` also accepts `quit` and `q`. `stop` also accepts `end`. ### Managing the background process | Command | Description | |---------|-------------| -| `daemoniq start` | Start the background process manually | +| `daemoniq start` | Start the background process | | `daemoniq stop` | Stop the background process | | `daemoniq restart` | Stop and restart the background process | -| `daemoniq status` | Show PID, distro, backend, and hardware summary | -| `daemoniq logs` | Stream the live log output of the background process | +| `daemoniq logs` | Stream the live log output | -### Configuration +### Configuration and information | Command | Description | |---------|-------------| -| `daemoniq setup` | Re-run the setup wizard | -| `daemoniq distro` | Show detected distro and package managers | +| `daemoniq info` | Show status, distro, model, and package managers | | `daemoniq hardware` | Show hardware snapshot — GPU, drivers, dmesg errors | | `daemoniq history` | Show the last 50 recorded shell commands | -| `daemoniq sessions` | List active named sessions | +| `daemoniq setup` | Re-run the setup wizard | ### Updates and maintenance @@ -118,40 +118,38 @@ Built-in knowledge covers `apt`, `dpkg`, `pip`, `snap`, and `flatpak`; NVIDIA, A | `daemoniq rollback` | Restore the previous version | | `daemoniq uninstall` | Remove DaemonIQ from this machine | -### Sessions - -Sessions maintain separate conversation histories, which is useful when working across different machines or projects at once: - -```bash -daemoniq --session server "nginx won't start after the last upgrade" -daemoniq --session laptop "bluetooth keeps disconnecting" -daemoniq sessions -``` - -The default session is named `default`. - --- ## Quick reference ``` -daemoniq Start a session -daemoniq "question" One-shot question -daemoniq --exec "question" Ask and apply fix -daemoniq --session NAME "q" Named session - -daemoniq setup Re-run setup -daemoniq start / stop / restart Start, stop, or restart the background process -daemoniq status / logs Check health and stream live log output -daemoniq distro / hardware System info -daemoniq history / sessions History and sessions -daemoniq version / update Version and patching -daemoniq rollback / uninstall Recovery and removal - -In a session: - exec on / off Toggle auto-apply - clear Reset conversation - exit End session +daemoniq Start a session +daemoniq "question" Ask a single question +daemoniq --exec "question" Ask and auto-apply the fix + +daemoniq start Start the background process +daemoniq stop Stop the background process +daemoniq restart Restart the background process +daemoniq logs View live logs +daemoniq info Show status and system info +daemoniq hardware Show hardware and driver info +daemoniq history Show recorded shell commands +daemoniq setup Re-run setup wizard +daemoniq version Show installed version +daemoniq update Apply a patch +daemoniq rollback Restore previous version +daemoniq uninstall Remove DaemonIQ + +Inside a session: + exec on Apply fixes automatically + exec off Show fixes, do not apply (default) + clear Wipe conversation history + history Show shell command history + info Show status and system info + hardware Show hardware and driver info + help Show the help menu + close Close this session + stop Shut down completely ``` --- @@ -159,6 +157,6 @@ In a session: ## Notes - Requires Python 3.8+ and [Ollama](https://ollama.com) -- Nothing is installed system-wide — all files live under `~/.daemoniq-demon/` (the background process install directory) -- "DaemonIQ" is a working name. To rename it, edit the `BRANDING` block at the top of any variant script +- A GPU is not required — DaemonIQ runs on CPU. If a GPU is present, Ollama will use it automatically. +- Nothing is installed system-wide — all files live under `~/.daemoniq-demon/` - Full installation instructions: [INSTALL_GUIDE.md](INSTALL_GUIDE.md)