Release v0.1.0: context codes and readable archive file names.
- registry.yaml maps codes (w1-1, w1.git.001) to sessions and stable paths
- Markdown files named {code}--{slug}.md for Cursor @ picker
- Selective export/import via --context and --mask
- cam code list|set|show for manual code assignment
- Re-export updates same file per context code
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,7 +14,8 @@ if str(CAM_ROOT) not in sys.path:
|
||||
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_sessions # 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:
|
||||
@@ -31,50 +32,136 @@ def _default_cam_command(config_path: Path | None) -> str:
|
||||
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_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"index_language: {config.index_language}")
|
||||
print(f"config_file: {config.source_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) -> int:
|
||||
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))
|
||||
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}"
|
||||
f"to {config.export_dir}{label}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_import(config_path: Path, dry_run: bool, source: Path | None) -> int:
|
||||
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)
|
||||
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"
|
||||
f"skipped {result.skipped} existing{label}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_list(config_path: Path) -> int:
|
||||
def cmd_list(
|
||||
config_path: Path,
|
||||
context_mask: str | None,
|
||||
) -> int:
|
||||
config = load_config(config_path)
|
||||
sessions = list_archive_sessions(config.export_dir)
|
||||
if not sessions:
|
||||
print(f"No sessions in {config.export_dir / 'raw'}")
|
||||
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
|
||||
for session_id, parent_id, path in sessions:
|
||||
parent = parent_id or "-"
|
||||
print(f"{session_id}\tparent={parent}\t{path}")
|
||||
print(f"Total: {len(sessions)}")
|
||||
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
|
||||
|
||||
|
||||
@@ -101,27 +188,42 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
sub.add_parser("paths", help="Show resolved paths from config").set_defaults(
|
||||
handler="paths"
|
||||
)
|
||||
sub.add_parser("export", help="Export Cursor transcripts to project archive").set_defaults(
|
||||
handler="export"
|
||||
)
|
||||
sub.add_parser("list", help="List sessions in export archive").set_defaults(handler="list")
|
||||
|
||||
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",
|
||||
help="Show what would be copied without writing files",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--source",
|
||||
type=Path,
|
||||
help="Archive root (default: export.output_dir from config)",
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -139,11 +241,24 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.handler == "paths":
|
||||
return cmd_paths(config_path)
|
||||
if args.handler == "export":
|
||||
return cmd_export(config_path)
|
||||
return cmd_export(config_path, args.context, args.mask)
|
||||
if args.handler == "list":
|
||||
return cmd_list(config_path)
|
||||
return cmd_list(config_path, args.mask)
|
||||
if args.handler == "import":
|
||||
return cmd_import(config_path, args.dry_run, args.source)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user