Skip to main content
Glama
cli.py16.8 kB
from __future__ import annotations import argparse import json import sys from .config import get_config from .living_reports.service import ReportService from .mcp.tools.create_report import VALID_TEMPLATES as REPORT_TEMPLATES from .query_optimizer import optimize_execution def _command_query_optimize(args: argparse.Namespace) -> int: try: report = optimize_execution( execution_id=args.execution_id, history_path=args.history, ) except Exception as exc: # pragma: no cover - CLI surface print(f"optimization failed: {exc}", file=sys.stderr) return 1 if args.format == "json": print(json.dumps(report, indent=2, ensure_ascii=False)) return 0 findings = report.get("findings") or [] print( "Execution: {exec_id}\nStatus: {status}\nDuration: {duration} ms\nRowcount: {rowcount}".format( exec_id=report.get("execution_id"), status=report.get("status"), duration=report.get("duration_ms"), rowcount=report.get("rowcount"), ) ) if report.get("objects"): objs = ", ".join( filter( None, [obj.get("name") if isinstance(obj, dict) else None for obj in report["objects"]], ) ) if objs: print(f"Objects: {objs}") print("Findings:") for finding in findings: msg = finding.get("message") level = finding.get("level", "info").upper() detail = finding.get("detail") print(f" - [{level}] {msg}") if detail: print(f" {detail}") return 0 # Report command handlers def _command_report_create(args: argparse.Namespace) -> int: """Create a new living report.""" try: service = ReportService() report_id = service.create_report(args.title, template=args.template, tags=args.tags) print(f"Created report '{args.title}' with ID: {report_id}") if args.template != "default": print(f"Using template: {args.template}") return 0 except ValueError as e: print(f"Failed to create report: {e}", file=sys.stderr) return 1 except Exception as e: print(f"Unexpected error: {e}", file=sys.stderr) return 1 def _command_report_evolve(args: argparse.Namespace) -> int: """Evolve a report with LLM assistance.""" try: from .mcp.tools.evolve_report import EvolveReportTool config = get_config() service = ReportService() # For now, call the tool directly (would normally go through MCP) tool = EvolveReportTool(config, service) # CLI interface currently supports discovery mode and explicit changes. # If proposed_changes is not provided, default to empty dict which # triggers structure discovery in dry-run mode. proposed_changes = getattr(args, "proposed_changes", None) or {} import asyncio result = asyncio.run( tool.execute( report_selector=args.selector, instruction=args.instruction, proposed_changes=proposed_changes, dry_run=args.dry_run, ) ) if result["status"] == "validation_failed": print("Validation failed:", file=sys.stderr) for issue in result["validation_issues"]: print(f" - {issue}", file=sys.stderr) return 1 if result["status"] == "dry_run_success": print("Dry run successful - changes would be applied") return 0 if result["status"] == "success": print(f"Report evolved successfully: {result['report_id']}") return 0 print(f"Unexpected result: {result}", file=sys.stderr) return 1 except Exception as e: print(f"Failed to evolve report: {e}", file=sys.stderr) return 1 def _command_report_render(args: argparse.Namespace) -> int: """Render report to final format.""" try: service = ReportService() result = service.render_report( report_id=args.selector, format=args.format, options=args.options, open_browser=args.open, dry_run=args.dry_run, ) if result["status"] == "success": output_path = result.get("output", {}).get("output_path") if output_path: print(f"✓ Report rendered successfully: {output_path}") else: print("✓ QMD file generated successfully (dry run)") if result.get("warnings"): print("⚠️ Warnings:") for warning in result["warnings"]: print(f" - {warning}") elif result["status"] == "quarto_missing": print( "❌ Quarto not found. Install from https://quarto.org/docs/get-started/", file=sys.stderr, ) print(" Or set IGLOO_QUARTO_BIN environment variable.", file=sys.stderr) return 1 elif result["status"] == "validation_failed": print("❌ Report validation failed:", file=sys.stderr) for error in result.get("validation_errors", []): print(f" - {error}", file=sys.stderr) return 1 elif result["status"] == "render_failed": print( f"❌ Rendering failed: {result.get('error', 'Unknown error')}", file=sys.stderr, ) return 1 return 0 except Exception as e: print(f"Failed to render report: {e}", file=sys.stderr) return 1 def _command_report_revert(args: argparse.Namespace) -> int: """Revert report to previous state.""" try: service = ReportService() report_id = service.resolve_report_selector(args.selector) service.revert_report(report_id, args.action_id) print(f"Reverted report {report_id} to action {args.action_id}") return 0 except Exception as e: print(f"Failed to revert report: {e}", file=sys.stderr) return 1 def _command_report_list(args: argparse.Namespace) -> int: """List reports.""" try: service = ReportService() reports = service.list_reports(status=args.status, tags=args.tags) if not reports: print("No reports found") return 0 print(f"{'ID':<36} {'Title':<30} {'Created':<20} {'Updated':<20} {'Status':<8} {'Tags'}") print("-" * 120) for report in reports: tags_str = ", ".join(report.get("tags", [])) print( f"{report['id']:<36} {report['title']:<30} {report['created_at']:<20} " f"{report['updated_at']:<20} {report['status']:<8} {tags_str}" ) return 0 except Exception as e: print(f"Failed to list reports: {e}", file=sys.stderr) return 1 def _command_report_archive(args: argparse.Namespace) -> int: """Archive a report.""" try: service = ReportService() report_id = service.resolve_report_selector(args.selector) service.archive_report(report_id, actor="cli") print(f"✓ Archived report: {report_id}") return 0 except Exception as e: print(f"❌ Failed to archive report: {e}", file=sys.stderr) return 1 def _command_report_delete(args: argparse.Namespace) -> int: """Delete a report (move to .trash).""" try: service = ReportService() report_id = service.resolve_report_selector(args.selector) if not args.force: # Confirmation prompt print(f"⚠️ This will move report {report_id} to .trash/") response = input("Continue? (y/N): ") if response.lower() != "y": print("Cancelled.") return 0 trash_location = service.delete_report(report_id, actor="cli") print(f"✓ Deleted report: {report_id}") print(f" Location: {trash_location}") print(" (Can be manually restored from .trash/ directory)") return 0 except Exception as e: print(f"❌ Failed to delete report: {e}", file=sys.stderr) return 1 def _command_report_tag(args: argparse.Namespace) -> int: """Modify report tags.""" try: service = ReportService() report_id = service.resolve_report_selector(args.selector) service.tag_report( report_id, tags_to_add=args.add, tags_to_remove=args.remove, actor="cli", ) # Show current tags outline = service.get_report_outline(report_id) tags = outline.metadata.get("tags", []) print(f"✓ Updated tags for report: {report_id}") print(f" Current tags: {', '.join(tags) if tags else '(none)'}") return 0 except Exception as e: print(f"❌ Failed to update tags: {e}", file=sys.stderr) return 1 def _command_report_fork(args: argparse.Namespace) -> int: """Fork an existing report.""" try: service = ReportService() source_id = service.resolve_report_selector(args.source_selector) new_id = service.fork_report(source_id, args.new_title, actor="cli") print("✓ Forked report successfully") print(f" Source: {source_id}") print(f" New report: {new_id}") print(f" Title: {args.new_title}") return 0 except Exception as e: print(f"❌ Failed to fork report: {e}", file=sys.stderr) return 1 def _command_report_synthesize(args: argparse.Namespace) -> int: """Synthesize multiple reports into one.""" try: service = ReportService() # Resolve all source selectors source_ids = [] for selector in args.source_selectors: report_id = service.resolve_report_selector(selector) source_ids.append(report_id) new_id = service.synthesize_reports(source_ids, args.title, actor="cli") print("✓ Synthesized report successfully") print(f" Sources: {', '.join(source_ids)}") print(f" New report: {new_id}") print(f" Title: {args.title}") return 0 except Exception as e: print(f"❌ Failed to synthesize reports: {e}", file=sys.stderr) return 1 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="igloo administrative CLI utilities for power users and system administrators" ) subparsers = parser.add_subparsers(dest="command", required=True) query_parser = subparsers.add_parser("query", help="Query tooling") query_sub = query_parser.add_subparsers(dest="query_command", required=True) optimize_parser = query_sub.add_parser("optimize", help="Analyze a recorded query execution") optimize_parser.add_argument( "--execution-id", dest="execution_id", default=None, help="Execution ID from execute_query (defaults to latest)", ) optimize_parser.add_argument( "--history", default=None, help="Optional override for query history path", ) optimize_parser.add_argument( "--format", choices=["text", "json"], default="text", help="Output format", ) optimize_parser.set_defaults(func=_command_query_optimize) # Report subcommand report_parser = subparsers.add_parser( "report", help="Administrative living reports management (MCP tools are primary for development)", ) report_sub = report_parser.add_subparsers(dest="report_command", required=True) # report create create_parser = report_sub.add_parser("create", help="Create a new living report") create_parser.add_argument("title", help="Report title") create_parser.add_argument( "--template", choices=list(REPORT_TEMPLATES), default="default", help="Report template to use", ) create_parser.add_argument( "--tags", nargs="*", default=[], help="Optional tags for the report", ) create_parser.set_defaults(func=_command_report_create) # report evolve evolve_parser = report_sub.add_parser("evolve", help="Evolve a report with LLM assistance") evolve_parser.add_argument("selector", help="Report ID or title") evolve_parser.add_argument("instruction", help="Evolution instruction") evolve_parser.add_argument( "--proposed-changes", type=json.loads, default=None, help="Structured proposed changes as JSON string. " "Omit (or use with --dry-run) to discover the current outline structure.", ) evolve_parser.add_argument( "--dry-run", action="store_true", help="Validate changes without applying them", ) evolve_parser.set_defaults(func=_command_report_evolve) # report render render_parser = report_sub.add_parser("render", help="Render report to final format") render_parser.add_argument("selector", help="Report ID or title") render_parser.add_argument( "--format", choices=["markdown", "html", "pdf"], default="html", help="Output format", ) render_parser.add_argument( "--open", action="store_true", help="Open rendered output in browser (HTML only)", ) render_parser.add_argument( "--dry-run", action="store_true", help="Generate QMD file only, skip Quarto rendering", ) render_parser.add_argument( "--options", type=json.loads, default=None, help='Additional Quarto options as JSON string (e.g., \'{"toc": true, "theme": "default"}\')', ) render_parser.set_defaults(func=_command_report_render) # report revert revert_parser = report_sub.add_parser("revert", help="Revert report to previous state") revert_parser.add_argument("selector", help="Report ID or title") revert_parser.add_argument("action_id", help="Action ID to revert to") revert_parser.set_defaults(func=_command_report_revert) # report list list_parser = report_sub.add_parser("list", help="List reports") list_parser.add_argument( "--status", choices=["active", "archived"], help="Filter by status", ) list_parser.add_argument( "--tags", nargs="*", help="Filter by tags", ) list_parser.set_defaults(func=_command_report_list) # report archive archive_parser = report_sub.add_parser("archive", help="Archive a report") archive_parser.add_argument("selector", help="Report ID or title") archive_parser.set_defaults(func=_command_report_archive) # report delete delete_parser = report_sub.add_parser("delete", help="Delete a report (move to .trash)") delete_parser.add_argument("selector", help="Report ID or title") delete_parser.add_argument( "--force", action="store_true", help="Skip confirmation prompt", ) delete_parser.set_defaults(func=_command_report_delete) # report tag tag_parser = report_sub.add_parser("tag", help="Modify report tags") tag_parser.add_argument("selector", help="Report ID or title") tag_parser.add_argument( "--add", nargs="*", default=[], help="Tags to add", ) tag_parser.add_argument( "--remove", nargs="*", default=[], help="Tags to remove", ) tag_parser.set_defaults(func=_command_report_tag) # report fork fork_parser = report_sub.add_parser("fork", help="Fork an existing report") fork_parser.add_argument("source_selector", help="Source report ID or title") fork_parser.add_argument("new_title", help="Title for forked report") fork_parser.set_defaults(func=_command_report_fork) # report synthesize synthesize_parser = report_sub.add_parser("synthesize", help="Synthesize multiple reports into one") synthesize_parser.add_argument( "source_selectors", nargs="+", help="Source report IDs or titles", ) synthesize_parser.add_argument( "--title", required=True, help="Title for synthesized report", ) synthesize_parser.set_defaults(func=_command_report_synthesize) return parser def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) handler = getattr(args, "func", None) if not handler: parser.print_help() return 2 return handler(args) if __name__ == "__main__": # pragma: no cover raise SystemExit(main())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Evan-Kim2028/igloo-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server