diff --git a/src/aipass/cli/.seedgo/bypass.json b/src/aipass/cli/.seedgo/bypass.json index 57fb26e1..b3b333d7 100644 --- a/src/aipass/cli/.seedgo/bypass.json +++ b/src/aipass/cli/.seedgo/bypass.json @@ -10,6 +10,21 @@ "standard": "json_structure", "reason": "Pure Python bootstrap \u2014 no branch imports by design" }, + { + "file": "apps/handlers/init/bootstrap.py", + "standard": "silent_catch", + "reason": "Pure Python bootstrap \u2014 cannot import prax (circular: prax depends on cli). _detect_aipass_home() catches detection failures silently by design." + }, + { + "file": "apps/handlers/init/bootstrap.py", + "standard": "error_handling", + "reason": "Same silent catch as silent_catch standard \u2014 already bypassed (pure Python, no prax import allowed)." + }, + { + "file": "apps/handlers/init/bootstrap.py", + "standard": "log_visibility", + "reason": "Pure Python bootstrap \u2014 cannot import prax system_logger (circular: prax depends on cli). Uses stdlib logging as fallback." + }, { "file": "apps/modules/display.py", "standard": "silent_catch", diff --git a/src/aipass/cli/apps/handlers/init/bootstrap.py b/src/aipass/cli/apps/handlers/init/bootstrap.py index 2e36a710..5fc20b6e 100644 --- a/src/aipass/cli/apps/handlers/init/bootstrap.py +++ b/src/aipass/cli/apps/handlers/init/bootstrap.py @@ -21,6 +21,7 @@ Business logic for `aipass init`. Creates the project scaffold: 9. .claude/settings.json — Claude Code hooks configuration 10. hooks/ — directory for user hooks 11. src/ — directory where agents live + 12. .ai_mail.local/inbox.json — empty project mailbox Projects are NOT citizens — no .trinity/ directory. Identity lives in the registry JSON. Init is re-runnable: existing files are skipped, not errors. @@ -31,12 +32,16 @@ RULES: - No hardcoded paths """ +import importlib.util import json +import logging import re import uuid from datetime import date from pathlib import Path +logger = logging.getLogger(__name__) + def _sanitize_name(raw: str) -> str: """Sanitize a project name for use in filenames. @@ -47,6 +52,23 @@ def _sanitize_name(raw: str) -> str: return re.sub(r"[^A-Z0-9_-]", "_", raw.upper()).strip("_") +def _detect_aipass_home() -> str | None: + """Detect the AIPass installation root from the aipass package location. + + Returns the parent of the src/ directory (the repo root). + Returns None if detection fails. + """ + try: + spec = importlib.util.find_spec("aipass") + if spec and spec.origin: + # aipass/__init__.py lives at src/aipass/__init__.py + # parent = src/aipass/, parent.parent = src/, parent.parent.parent = AIPass root + return str(Path(spec.origin).resolve().parent.parent.parent) + except Exception as exc: + logger.info("AIPASS_HOME detection skipped: %s", exc) + return None + + # --------------------------------------------------------------------------- # Template content generators # --------------------------------------------------------------------------- @@ -387,23 +409,40 @@ def _gitignore() -> str: ) -def _claude_settings() -> str: - """Generate .claude/settings.json — minimal hooks for prompt injection.""" +def _claude_settings(aipass_home: str | None = None) -> str: + """Generate .claude/settings.json — minimal hooks for prompt injection. + + Args: + aipass_home: Optional AIPass installation root to add as env.AIPASS_HOME. + """ + data: dict = { + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "cat .aipass/aipass_global_prompt.md 2>/dev/null || true", + } + ], + } + ] + } + } + if aipass_home: + data["env"] = {"AIPASS_HOME": aipass_home} + return json.dumps(data, indent=2, ensure_ascii=False) + "\n" + + +def _inbox_json() -> str: + """Generate .ai_mail.local/inbox.json — empty project mailbox structure.""" return json.dumps( { - "hooks": { - "UserPromptSubmit": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "cat .aipass/aipass_global_prompt.md 2>/dev/null || true", - } - ], - } - ] - } + "mailbox": "inbox", + "total_messages": 0, + "unread_count": 0, + "messages": [], }, indent=2, ensure_ascii=False, @@ -451,6 +490,7 @@ def init_project(target: Path, project_name: str | None = None) -> dict: registry_id = str(uuid.uuid4()) today = date.today().isoformat() created = [] + aipass_home = _detect_aipass_home() # 1. Registry (skip if exists — init is re-runnable) registry_filename = f"{name}_REGISTRY.json" @@ -549,7 +589,7 @@ def init_project(target: Path, project_name: str | None = None) -> dict: settings_path = claude_dir / "settings.json" if not settings_path.exists(): - settings_path.write_text(_claude_settings(), encoding="utf-8") + settings_path.write_text(_claude_settings(aipass_home), encoding="utf-8") created.append(str(settings_path)) # 10. hooks/ directory @@ -564,12 +604,21 @@ def init_project(target: Path, project_name: str | None = None) -> dict: src_dir.mkdir() created.append(str(src_dir)) + # 12. .ai_mail.local/inbox.json — empty project mailbox + mail_dir = target / ".ai_mail.local" + mail_dir.mkdir(exist_ok=True) + inbox_path = mail_dir / "inbox.json" + if not inbox_path.exists(): + inbox_path.write_text(_inbox_json(), encoding="utf-8") + created.append(str(inbox_path)) + return { "registry_id": registry_id, "registry_file": registry_filename, "project_name": name, "target": str(target), "created_files": created, + "aipass_home": aipass_home, } @@ -605,6 +654,7 @@ def update_project(target: Path) -> dict: updated: list[str] = [] already_current: list[str] = [] skipped: list[str] = [] + aipass_home: str | None = None # Managed directories — create if missing (graceful recovery). aipass_dir = target / ".aipass" @@ -623,13 +673,27 @@ def update_project(target: Path) -> dict: else: already_current.append(str(global_prompt_path)) + # settings.json — smart merge: preserve existing AIPASS_HOME, detect if missing settings_path = claude_dir / "settings.json" - generated = _claude_settings() - if not settings_path.exists() or settings_path.read_text(encoding="utf-8") != generated: - settings_path.write_text(generated, encoding="utf-8") + if not settings_path.exists(): + aipass_home = _detect_aipass_home() + settings_path.write_text(_claude_settings(aipass_home), encoding="utf-8") updated.append(str(settings_path)) else: - already_current.append(str(settings_path)) + existing_content = settings_path.read_text(encoding="utf-8") + try: + existing_env = json.loads(existing_content).get("env", {}) + except json.JSONDecodeError as exc: + logger.info("settings.json parse failed, rebuilding: %s", exc) + existing_env = {} + # Preserve existing AIPASS_HOME; detect and add if missing + aipass_home = existing_env.get("AIPASS_HOME") or _detect_aipass_home() + generated = _claude_settings(aipass_home) + if existing_content != generated: + settings_path.write_text(generated, encoding="utf-8") + updated.append(str(settings_path)) + else: + already_current.append(str(settings_path)) claude_md_path = target / "CLAUDE.md" generated = _with_source(_claude_md(name), claude_md_path) @@ -664,10 +728,21 @@ def update_project(target: Path) -> dict: ): skipped.append(skip_name) + # Mailbox — create if missing, never overwrite existing + mail_dir = target / ".ai_mail.local" + mail_dir.mkdir(exist_ok=True) + inbox_path = mail_dir / "inbox.json" + if not inbox_path.exists(): + inbox_path.write_text(_inbox_json(), encoding="utf-8") + updated.append(str(inbox_path)) + else: + skipped.append(str(inbox_path)) + return { "project_name": name, "target": str(target), "updated_files": updated, "already_current": already_current, "skipped_files": skipped, + "aipass_home": aipass_home, } diff --git a/src/aipass/cli/apps/modules/init_project.py b/src/aipass/cli/apps/modules/init_project.py index 1f908b21..0729800e 100644 --- a/src/aipass/cli/apps/modules/init_project.py +++ b/src/aipass/cli/apps/modules/init_project.py @@ -108,7 +108,7 @@ def print_help(): console.print("[bold cyan]WHAT INIT CREATES:[/bold cyan]") console.print() - files_text = """[bold]Project scaffold (11 items):[/bold] + files_text = """[bold]Project scaffold (12 items):[/bold] [green]1.[/green] [yellow]{NAME}_REGISTRY.json[/yellow] Project registry with UUID [green]2.[/green] [yellow].aipass/aipass_global_prompt.md[/yellow] Global prompt (injected every turn) @@ -118,9 +118,10 @@ def print_help(): [green]6.[/green] [yellow]README.md[/yellow] Getting started guide [green]7.[/green] [yellow]STATUS.local.md[/yellow] Project status [green]8.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores - [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + AIPASS_HOME [green]10.[/green] [yellow]hooks/[/yellow] User hooks directory - [green]11.[/green] [yellow]src/[/yellow] Agent directories live here""" + [green]11.[/green] [yellow]src/[/yellow] Agent directories live here + [green]12.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED)) console.print() diff --git a/src/aipass/cli/tests/test_bootstrap.py b/src/aipass/cli/tests/test_bootstrap.py index 4a0473ea..b0a413a4 100644 --- a/src/aipass/cli/tests/test_bootstrap.py +++ b/src/aipass/cli/tests/test_bootstrap.py @@ -91,6 +91,7 @@ def test_init_project_creates_all_expected_files(tmp_path): target / "STATUS.local.md", target / ".gitignore", target / ".claude" / "settings.json", + target / ".ai_mail.local" / "inbox.json", ] for f in expected_files: assert f.exists(), f"Expected file not created: {f}" @@ -105,8 +106,8 @@ def test_init_project_creates_all_expected_files(tmp_path): # No local prompt at project level (belongs in agent dirs only) assert not (target / ".aipass" / "aipass_local_prompt.md").exists() - # 9 files + 2 directories = 11 created_files entries - assert len(result["created_files"]) == 11 + # 10 files + 2 directories = 12 created_files entries + assert len(result["created_files"]) == 12 def test_init_project_return_dict_structure(tmp_path): @@ -122,6 +123,7 @@ def test_init_project_return_dict_structure(tmp_path): "project_name", "target", "created_files", + "aipass_home", } assert result["project_name"] == "ALPHA" assert result["registry_file"] == "ALPHA_REGISTRY.json" @@ -312,7 +314,7 @@ def test_init_project_auto_creates_target_dir(tmp_path): assert target.is_dir() assert result["project_name"] == "NESTED" - assert len(result["created_files"]) == 11 + assert len(result["created_files"]) == 12 def test_init_project_defaults_name_from_directory(tmp_path): @@ -367,6 +369,10 @@ def test_init_project_skips_existing_optional_files(tmp_path): src_dir = target / "src" src_dir.mkdir() + mail_dir = target / ".ai_mail.local" + mail_dir.mkdir() + (mail_dir / "inbox.json").write_text("{}\n", encoding="utf-8") + result = init_project(target, project_name="eta") # Only registry should be in created_files (everything else pre-existed) @@ -445,6 +451,7 @@ def test_update_project_return_dict_structure(tmp_path): "updated_files", "already_current", "skipped_files", + "aipass_home", } assert result["project_name"] == "UPD" assert result["target"] == str(target.resolve()) @@ -546,11 +553,148 @@ def test_update_project_creates_missing_managed_dirs(tmp_path): def test_update_project_skipped_files_count(tmp_path): - """update_project always skips exactly 4 user-owned files.""" + """update_project skips 4 user-owned files + existing mailbox = 5 total.""" target = tmp_path / "proj" target.mkdir() init_project(target, project_name="count") result = update_project(target) - assert len(result["skipped_files"]) == 4 + # 4 user-owned (registry, README, STATUS, .gitignore) + inbox.json = 5 + assert len(result["skipped_files"]) == 5 + + +# --------------------------------------------------------------------------- +# DPLAN-0121: AIPASS_HOME + mailbox tests +# --------------------------------------------------------------------------- + + +def test_init_project_creates_mailbox(tmp_path): + """init_project creates .ai_mail.local/inbox.json.""" + target = tmp_path / "proj" + target.mkdir() + + init_project(target, project_name="mail") + + assert (target / ".ai_mail.local" / "inbox.json").exists() + + +def test_init_project_mailbox_json_contents(tmp_path): + """inbox.json has valid empty mailbox structure.""" + target = tmp_path / "proj" + target.mkdir() + + init_project(target, project_name="mail") + + data = json.loads((target / ".ai_mail.local" / "inbox.json").read_text(encoding="utf-8")) + assert data["mailbox"] == "inbox" + assert data["total_messages"] == 0 + assert data["unread_count"] == 0 + assert data["messages"] == [] + + +def test_init_project_mailbox_not_overwritten_on_rerun(tmp_path): + """Re-running init skips existing inbox.json.""" + target = tmp_path / "proj" + target.mkdir() + init_project(target, project_name="mail") + + inbox = target / ".ai_mail.local" / "inbox.json" + inbox.write_text('{"custom": true}\n', encoding="utf-8") + + init_project(target, project_name="mail") + + assert json.loads(inbox.read_text(encoding="utf-8")) == {"custom": True} + + +def test_init_project_returns_aipass_home(tmp_path): + """init_project return dict includes aipass_home key.""" + target = tmp_path / "proj" + target.mkdir() + + result = init_project(target, project_name="home") + + assert "aipass_home" in result + assert result["aipass_home"] is None or isinstance(result["aipass_home"], str) + + +def test_init_project_settings_has_aipass_home_when_detected(tmp_path): + """When AIPASS_HOME is detected, settings.json includes env.AIPASS_HOME.""" + target = tmp_path / "proj" + target.mkdir() + + result = init_project(target, project_name="env") + + if result["aipass_home"] is None: + pytest.skip("AIPASS_HOME not detectable in this environment") + + settings = json.loads((target / ".claude" / "settings.json").read_text(encoding="utf-8")) + assert "env" in settings + assert settings["env"]["AIPASS_HOME"] == result["aipass_home"] + + +def test_update_project_creates_mailbox_if_missing(tmp_path): + """update_project creates inbox.json if it does not exist.""" + import shutil + + target = tmp_path / "proj" + target.mkdir() + init_project(target, project_name="newmail") + + # Remove the entire mailbox directory to simulate missing mailbox + shutil.rmtree(target / ".ai_mail.local") + + result = update_project(target) + + inbox = target / ".ai_mail.local" / "inbox.json" + assert inbox.exists() + assert str(inbox) in result["updated_files"] + + +def test_update_project_skips_existing_mailbox(tmp_path): + """update_project never overwrites an existing inbox.json.""" + target = tmp_path / "proj" + target.mkdir() + init_project(target, project_name="keepmail") + + inbox = target / ".ai_mail.local" / "inbox.json" + inbox.write_text( + '{"mailbox":"inbox","total_messages":5,"unread_count":2,"messages":["x"]}\n', + encoding="utf-8", + ) + + result = update_project(target) + + assert str(inbox) in result["skipped_files"] + assert json.loads(inbox.read_text(encoding="utf-8"))["total_messages"] == 5 + + +def test_update_project_returns_aipass_home(tmp_path): + """update_project return dict includes aipass_home key.""" + target = tmp_path / "proj" + target.mkdir() + init_project(target, project_name="uhome") + + result = update_project(target) + + assert "aipass_home" in result + assert result["aipass_home"] is None or isinstance(result["aipass_home"], str) + + +def test_update_project_adds_aipass_home_if_missing(tmp_path): + """update_project injects AIPASS_HOME into settings.json if env section is absent.""" + target = tmp_path / "proj" + target.mkdir() + init_project(target, project_name="addenv") + + settings_path = target / ".claude" / "settings.json" + data = json.loads(settings_path.read_text(encoding="utf-8")) + data.pop("env", None) + settings_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + + result = update_project(target) + + if result["aipass_home"] is not None: + new_data = json.loads(settings_path.read_text(encoding="utf-8")) + assert new_data.get("env", {}).get("AIPASS_HOME") == result["aipass_home"] + assert str(settings_path) in result["updated_files"] diff --git a/src/aipass/drone/apps/handlers/json/json_handler.py b/src/aipass/drone/apps/handlers/json/json_handler.py index a9943e64..c01599d4 100644 --- a/src/aipass/drone/apps/handlers/json/json_handler.py +++ b/src/aipass/drone/apps/handlers/json/json_handler.py @@ -77,9 +77,9 @@ def _atomic_write_json(path: Path, data: Any) -> None: with os.fdopen(fd, "w", encoding="utf-8") as fh: json.dump(data, fh, indent=2, ensure_ascii=False) os.replace(tmp_path, str(path)) - except Exception as exc: + except BaseException as exc: logger.warning("_atomic_write_json: failed for %s: %s", path, exc) - # Clean up temp file on failure + # Clean up temp file on failure — BaseException covers KeyboardInterrupt try: os.unlink(tmp_path) except OSError as cleanup_exc: @@ -310,34 +310,38 @@ def log_operation( if module_name is None: module_name = _get_caller_module_name() - ensure_module_jsons(module_name) + try: + ensure_module_jsons(module_name) - # Read rotation limit from config - config = load_json(module_name, "config") - max_entries = 100 - if config and "config" in config: - max_entries = config["config"].get("max_log_entries", 100) + # Read rotation limit from config + config = load_json(module_name, "config") + max_entries = 100 + if config and "config" in config: + max_entries = config["config"].get("max_log_entries", 100) - # Load existing log - log = load_json(module_name, "log") - if log is None: - log = [] + # Load existing log + log = load_json(module_name, "log") + if log is None: + log = [] - # Build entry - entry: dict[str, Any] = { - "timestamp": datetime.now().isoformat(), - "operation": operation, - } - if data: - entry["data"] = data + # Build entry + entry: dict[str, Any] = { + "timestamp": datetime.now().isoformat(), + "operation": operation, + } + if data: + entry["data"] = data - log.append(entry) + log.append(entry) - # FIFO rotation — keep only the most recent entries - if len(log) > max_entries: - log = log[-max_entries:] + # FIFO rotation — keep only the most recent entries + if len(log) > max_entries: + log = log[-max_entries:] - return save_json(module_name, "log", log) + return save_json(module_name, "log", log) + except Exception as exc: + logger.warning("log_operation: failed for %s/%s, skipping: %s", module_name, operation, exc) + return False def increment_counter( diff --git a/src/aipass/drone/tests/test_registry_handler.py b/src/aipass/drone/tests/test_registry_handler.py index 32fef4bc..6bc058ee 100644 --- a/src/aipass/drone/tests/test_registry_handler.py +++ b/src/aipass/drone/tests/test_registry_handler.py @@ -265,8 +265,12 @@ class TestLoadRegistry: class TestGetAllBranches: + @pytest.fixture(autouse=True) + def _isolate_home(self, monkeypatch): + """Prevent real AIPASS_HOME from leaking into test results.""" + monkeypatch.delenv("AIPASS_HOME", raising=False) + def test_returns_list_of_branch_dicts(self, registry_dir: Path): - """get_all_branches() returns a list of dicts for active branches.""" reg = _minimal_registry() _write_registry(registry_dir, reg) set_registry_path(registry_dir / "AIPASS_REGISTRY.json") diff --git a/src/aipass/drone/tests/test_resolver.py b/src/aipass/drone/tests/test_resolver.py index 4936d872..07e54007 100644 --- a/src/aipass/drone/tests/test_resolver.py +++ b/src/aipass/drone/tests/test_resolver.py @@ -272,6 +272,11 @@ class TestGetBranchInfo: # --------------------------------------------------------------------------- class TestListBranches: + @pytest.fixture(autouse=True) + def _isolate_home(self, monkeypatch): + """Prevent real AIPASS_HOME from leaking into test results.""" + monkeypatch.delenv("AIPASS_HOME", raising=False) + def test_default_status_active(self, populated_registry): """Default status='active' should exclude archived branches.""" result = list_branches() @@ -356,6 +361,11 @@ class TestHandleCommand: class TestWithSampleRegistry: """Tests using the sample_registry fixture from conftest.py.""" + @pytest.fixture(autouse=True) + def _isolate_home(self, monkeypatch): + """Prevent real AIPASS_HOME from leaking into test results.""" + monkeypatch.delenv("AIPASS_HOME", raising=False) + def test_resolve_via_sample(self, sample_registry): set_registry_path(sample_registry) try: diff --git a/src/aipass/prax/apps/modules/monitor.py b/src/aipass/prax/apps/modules/monitor.py index ae600b7e..263720e2 100755 --- a/src/aipass/prax/apps/modules/monitor.py +++ b/src/aipass/prax/apps/modules/monitor.py @@ -46,6 +46,7 @@ Architecture: - log_watcher.py → Log stream processing """ +import signal import sys import argparse import threading @@ -70,15 +71,9 @@ from aipass.prax.apps.handlers.monitoring import ( ModuleTracker, # module_tracker.py ) from aipass.prax.apps.handlers.monitoring.event_queue import MonitoringEvent -# NOTE: FileSystemEventHandler implementation lives in: -# aipass.prax.apps.handlers.monitoring.filesystem_handler.MonitoringFileHandler -# It handles trigger events for file_created/file_deleted/file_modified/file_moved +from aipass.prax.apps.modules.monitor_info import print_introspection as _print_introspection, print_help -# ============================================================================= -# UTILITY FUNCTIONS -# ============================================================================= - # ============================================================================= # PID CACHE - Maps branch names to active agent PIDs from dispatch lock files # ============================================================================= @@ -158,6 +153,11 @@ _file_watcher_thread: Optional[threading.Thread] = None _log_watcher_thread: Optional[threading.Thread] = None +def print_introspection(): + """Display module introspection - shows connected handlers and architecture.""" + _print_introspection() + + # ============================================================================= # CORE COMMAND HANDLER (Required for auto-discovery) # ============================================================================= @@ -560,103 +560,7 @@ def _print_status(): console.print() -# ============================================================================= -# INTROSPECTION (Module metadata and handler connections) -# ============================================================================= - -def print_introspection(): - """Display module introspection - shows connected handlers and architecture""" - console.print() - console.print("[bold cyan]PRAX Monitor Module[/bold cyan]") - console.print() - console.print("[yellow]Purpose:[/yellow]") - console.print(" Mission Control for autonomous branch monitoring") - console.print(" Unified console for file changes, logs, and module activity") - console.print() - - console.print("[yellow]Connected Handlers (apps/handlers/monitoring/):[/yellow]") - console.print() - console.print(" [cyan]1. unified_stream.py[/cyan]") - console.print(" [dim]→ print_event() - Terminal output formatting[/dim]") - console.print() - console.print(" [cyan]2. branch_detector.py[/cyan]") - console.print(" [dim]→ detect_branch_from_path() - Path-to-branch mapping[/dim]") - console.print() - console.print(" [cyan]3. interactive_filter.py[/cyan]") - console.print(" [dim]→ FilterState, parse_command() - Runtime filtering[/dim]") - console.print() - console.print(" [cyan]4. monitoring_filters.py[/cyan]") - console.print(" [dim]→ should_monitor(), get_priority() - Event filtering[/dim]") - console.print() - console.print(" [cyan]5. event_queue.py[/cyan]") - console.print(" [dim]→ MonitoringEvent, MonitoringQueue - Event buffering[/dim]") - console.print() - console.print(" [cyan]6. module_tracker.py[/cyan]") - console.print(" [dim]→ ModuleTracker - Module execution tracking[/dim]") - console.print() - console.print(" [cyan]7. file watcher (threaded)[/cyan]") - console.print(" [dim]→ Real-time file change detection using watchdog[/dim]") - console.print(" [green]STATUS: Active - monitors ECOSYSTEM_ROOT recursively[/green]") - console.print() - console.print(" [cyan]8. log monitor (threaded)[/cyan]") - console.print(" [dim]→ Log stream processing from SYSTEM_LOGS_DIR[/dim]") - console.print(" [green]STATUS: Active - watches *.log files for new entries[/green]") - console.print() - - console.print("[dim]Run 'drone @prax monitor --help' for usage[/dim]") - console.print() - - -# ============================================================================= -# HELP OUTPUT (Drone-compliant command documentation) -# ============================================================================= - -def print_help(): - """Drone-compliant help output - command syntax and examples""" - console.print() - console.print("[bold cyan]PRAX Monitor - Unified Branch Monitoring[/bold cyan]") - console.print() - - console.print("[yellow]Commands:[/yellow]") - console.print() - console.print(" [cyan]drone @prax monitor[/cyan]") - console.print(" Show module introspection") - console.print() - console.print(" [cyan]drone @prax monitor run[/cyan]") - console.print(" Start monitoring all branches") - console.print() - console.print(" [cyan]drone @prax monitor run all[/cyan]") - console.print(" Explicit all-branches monitoring") - console.print() - console.print(" [cyan]drone @prax monitor run [branches][/cyan]") - console.print(" Monitor specific branches (comma-separated)") - console.print(" Example: drone @prax monitor run seedgo,cli,flow") - console.print() - console.print(" [cyan]drone @prax monitor --help[/cyan]") - console.print(" Show this help") - console.print() - - console.print("[yellow]Interactive Mode Commands:[/yellow]") - console.print() - console.print(" [cyan]help[/cyan] Show available commands") - console.print(" [cyan]status[/cyan] Display current monitoring state") - console.print(" [cyan]filter [branches][/cyan] Adjust branch filter") - console.print(" [cyan]quit/exit[/cyan] Stop monitoring") - console.print() - - console.print("[yellow]Examples:[/yellow]") - console.print() - console.print(" [dim]# Monitor all branches[/dim]") - console.print(" $ drone @prax monitor run") - console.print() - console.print(" [dim]# Monitor specific branches[/dim]") - console.print(" $ drone @prax monitor run seedgo,cli,flow") - console.print() - - -# ============================================================================= # MAIN BLOCK (Standalone execution support) -# ============================================================================= if __name__ == "__main__": # Show introspection when run without arguments