#!/usr/bin/env python3 """CAM CLI — Cursor Agents Manager.""" from __future__ import annotations import argparse import sys from pathlib import Path CAM_ROOT = Path(__file__).resolve().parent if str(CAM_ROOT) not in sys.path: sys.path.insert(0, str(CAM_ROOT)) from __version__ import __status__, __version__ # noqa: E402 from cam.config import load_config, resolve_config_path # noqa: E402 from cam.export import export_transcripts # noqa: E402 from cam.import_transcripts import import_transcripts, list_archive_contexts # noqa: E402 from cam.registry import ContextRegistry # noqa: E402 def _default_cam_command(config_path: Path | None) -> str: if config_path is not None: rel_config: Path | str = config_path try: rel_config = config_path.relative_to(CAM_ROOT.parent.parent) except ValueError: try: rel_config = Path("tools/cam") / config_path.relative_to(CAM_ROOT) except ValueError: rel_config = config_path return f"python tools/cam/cam.py -c {rel_config} export" return "python tools/cam/cam.py export" def _add_context_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--context", metavar="CODE", help="Single context code (e.g. w1-1, w1.git.001)", ) parser.add_argument( "--mask", metavar="PATTERN", help="Context code glob pattern (e.g. w1-*, w1.git.*)", ) def cmd_paths(config_path: Path) -> int: config = load_config(config_path) print(f"workstation_id: {config.workstation_id}") print(f"workstation_index: {config.workstation_index}") print(f"workstation_label: {config.workstation_label}") print(f"project_root: {config.project_root}") print(f"cursor_project_slug: {config.cursor_project_slug}") print(f"transcripts_dir: {config.transcripts_dir}") print(f"export_dir: {config.export_dir}") print(f"registry_file: {config.export_dir / config.registry_file}") print(f"index_language: {config.index_language}") print(f"auto_code_format: {config.auto_code_format}") print(f"config_file: {config.source_path}") return 0 def cmd_export( config_path: Path, context: str | None, context_mask: str | None, ) -> int: config = load_config(config_path) main_count, sub_count = export_transcripts( config, _default_cam_command(config_path), context=context, context_mask=context_mask, ) label = "" if context or context_mask: label = f" (filter: {context or context_mask})" print( f"Exported {main_count} main sessions and {sub_count} subagents " f"to {config.export_dir}{label}" ) return 0 def cmd_import( config_path: Path, dry_run: bool, source: Path | None, context: str | None, context_mask: str | None, ) -> int: config = load_config(config_path) result = import_transcripts( config, dry_run=dry_run, source=source, context=context, context_mask=context_mask, ) mode = "Dry-run:" if dry_run else "Imported:" label = "" if context or context_mask: label = f" filter={context or context_mask}" print( f"{mode} {result.copied} file(s) to {config.transcripts_dir}; " f"skipped {result.skipped} existing{label}" ) return 0 def cmd_list( config_path: Path, context_mask: str | None, ) -> int: config = load_config(config_path) rows = list_archive_contexts(config.export_dir, config, context_mask=context_mask) if not rows: print(f"No contexts in {config.export_dir}") return 0 print("CODE\tSESSION_ID\tTITLE\tRAW") for code, session_id, title, raw in rows: raw_flag = "yes" if raw else "missing" print(f"{code}\t{session_id}\t{title}\t{raw_flag}") print(f"Total: {len(rows)}") return 0 def cmd_code_list(config_path: Path) -> int: config = load_config(config_path) registry = ContextRegistry(config.export_dir, config) registry.load() print("CODE\tSESSION_ID\tWS\tMANUAL\tMARKDOWN") for record in sorted(registry.contexts.values(), key=lambda r: r.code): manual = "yes" if record.manual_code else "no" print( f"{record.code}\t{record.session_id}\t{record.created_workstation}\t" f"{manual}\t{record.markdown_file}" ) print(f"Total: {len(registry.contexts)}") return 0 def cmd_code_set(config_path: Path, target: str, new_code: str) -> int: config = load_config(config_path) registry = ContextRegistry(config.export_dir, config) registry.load() record = registry.set_code(target, new_code) print(f"Set context code: {record.session_id} -> {record.code}") print(f"Markdown: {record.markdown_file}") return 0 def cmd_code_show(config_path: Path, code: str) -> int: config = load_config(config_path) registry = ContextRegistry(config.export_dir, config) registry.load() record = registry.get_by_code(code) if record is None: record = registry.get_by_session(code) if record is None: raise ValueError(f"Unknown context or session: {code}") for key, value in record.to_dict().items(): print(f"{key}: {value}") return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="cam", description="Cursor Agents Manager — export and import agent transcripts", ) parser.add_argument( "-V", "--version", action="version", version=f"%(prog)s {__version__} ({__status__})", ) parser.add_argument( "-c", "--config", type=Path, help="Path to workstation YAML config (default: auto-detect)", ) sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("paths", help="Show resolved paths from config").set_defaults( handler="paths" ) export_parser = sub.add_parser("export", help="Export Cursor transcripts to project archive") _add_context_args(export_parser) export_parser.set_defaults(handler="export") list_parser = sub.add_parser("list", help="List contexts in export archive") list_parser.add_argument("--mask", metavar="PATTERN", help="Context code glob pattern") list_parser.set_defaults(handler="list") import_parser = sub.add_parser( "import", help="Import archived transcripts into Cursor agent-transcripts folder", ) import_parser.add_argument("--dry-run", action="store_true") import_parser.add_argument("--source", type=Path) _add_context_args(import_parser) import_parser.set_defaults(handler="import") code_parser = sub.add_parser("code", help="Manage context codes") code_sub = code_parser.add_subparsers(dest="code_command", required=True) code_sub.add_parser("list", help="List all context codes").set_defaults( code_handler="list" ) code_set = code_sub.add_parser("set", help="Assign or rename a context code") code_set.add_argument("target", help="Session UUID or existing context code") code_set.add_argument("new_code", help="New code (e.g. w1.git.001)") code_set.set_defaults(code_handler="set") code_show = code_sub.add_parser("show", help="Show context record") code_show.add_argument("code", help="Context code or session UUID") code_show.set_defaults(code_handler="show") code_parser.set_defaults(handler="code") return parser def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) try: config_path = resolve_config_path(CAM_ROOT, args.config) except FileNotFoundError as exc: print(f"Error: {exc}", file=sys.stderr) return 2 try: if args.handler == "paths": return cmd_paths(config_path) if args.handler == "export": return cmd_export(config_path, args.context, args.mask) if args.handler == "list": return cmd_list(config_path, args.mask) if args.handler == "import": return cmd_import( config_path, args.dry_run, args.source, args.context, args.mask, ) if args.handler == "code": if args.code_handler == "list": return cmd_code_list(config_path) if args.code_handler == "set": return cmd_code_set(config_path, args.target, args.new_code) if args.code_handler == "show": return cmd_code_show(config_path, args.code) except (FileNotFoundError, ValueError) as exc: print(f"Error: {exc}", file=sys.stderr) return 1 parser.error(f"Unknown command: {args.handler}") return 2 if __name__ == "__main__": raise SystemExit(main())