scripts-session-export
#!/usr/bin/env python3 """
title: "Session Export (sx)" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Generic session exporter for any LLM (Claude, Codex, Gemini)" keywords: ['export', 'session', 'sx', 'claude', 'codex', 'gemini', 'cusf'] tokens: ~500 created: 2026-01-28 updated: 2026-01-28 script_name: "session-export.py" language: python executable: true usage: "python3 scripts/session-export.py [options]" python_version: "3.10+" dependencies: [] modifies_files: true network_access: false requires_auth: false
Session Export (sx) - Generic LLM Session Exporter
Exports session data from any supported LLM (Claude, Codex, Gemini) to CODITECT Universal Session Format (CUSF).
Features:
- Auto-detect LLM from session file or environment
- Export current or past sessions
- Multiple output formats (JSONL, JSON, SQLite)
- Reconstruction-capable output with full provenance
Usage: # Export current session (auto-detects LLM) python3 session-export.py
# Export specific LLM's session
python3 session-export.py --llm claude
python3 session-export.py --llm codex
# Export specific session file
python3 session-export.py --source ~/.claude/projects/abc123/def456.jsonl
# Export to specific format
python3 session-export.py --format sqlite --output sessions.db
# Reconstruction mode (all metadata)
python3 session-export.py --reconstruct
Track: J.13 (Memory - Generic Session Export) Task: J.13.3.1 """
from future import annotations
import argparse import json import os import sys from datetime import datetime from pathlib import Path from typing import Optional
Add parent paths for imports
_script_dir = Path(file).resolve().parent _coditect_root = _script_dir.parent if str(_coditect_root) not in sys.path: sys.path.insert(0, str(_coditect_root)) if str(_script_dir) not in sys.path: sys.path.insert(0, str(_script_dir))
from core.cli_tool_detector import CLIToolDetector, get_detector from core.configuration_manager import ConfigurationManager, get_config from core.extractor_factory import ExtractorFactory, get_or_detect from core.cusf_formatter import CUSFFormatter from core.output_writer import OutputWriter, write_cusf
def get_default_output_path(llm: str, session_id: str, format: str) -> Path: """Generate default output path.""" output_dir = Path(os.path.expanduser(get_config("output.default_directory", "~/.coditect-data/exports/"))) output_dir.mkdir(parents=True, exist_ok=True)
date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"{llm}-{date_str}-{session_id[:8]}.{format}"
return output_dir / filename
def main() -> int: """Main entry point for session export.""" parser = argparse.ArgumentParser( description="Export LLM session to CODITECT Universal Session Format (CUSF)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples:
Export current session (auto-detect)
%(prog)s
Export Claude session
%(prog)s --llm claude
Export specific session file
%(prog)s --source ~/.claude/projects/abc/def.jsonl
Export to SQLite for reconstruction
%(prog)s --format sqlite --output sessions.db
List available sessions
%(prog)s --list
Dry run (show what would be exported)
%(prog)s --dry-run """ )
# Source options
source_group = parser.add_argument_group("Source Options")
source_group.add_argument(
"--llm",
choices=["claude", "codex", "gemini", "kimi"],
help="LLM to export from (auto-detected if not specified)"
)
source_group.add_argument(
"--source",
type=Path,
help="Specific session file or directory to export"
)
source_group.add_argument(
"--session-id",
help="Specific session ID to export"
)
# Output options
output_group = parser.add_argument_group("Output Options")
output_group.add_argument(
"--output", "-o",
type=Path,
help="Output file path (default: auto-generated)"
)
output_group.add_argument(
"--format", "-f",
choices=["jsonl", "json", "sqlite"],
default="jsonl",
help="Output format (default: jsonl)"
)
output_group.add_argument(
"--compress",
action="store_true",
help="Compress output with gzip"
)
output_group.add_argument(
"--append",
action="store_true",
help="Append to existing file (JSONL only)"
)
# Content options
content_group = parser.add_argument_group("Content Options")
content_group.add_argument(
"--no-tool-results",
action="store_true",
help="Exclude tool result content"
)
content_group.add_argument(
"--no-thinking",
action="store_true",
help="Exclude extended thinking content"
)
content_group.add_argument(
"--reconstruct",
action="store_true",
help="Include all metadata for reconstruction"
)
# Actions
action_group = parser.add_argument_group("Actions")
action_group.add_argument(
"--list",
action="store_true",
help="List available sessions"
)
action_group.add_argument(
"--dry-run",
action="store_true",
help="Show what would be exported without writing"
)
action_group.add_argument(
"--detect",
action="store_true",
help="Detect installed LLM CLI tools"
)
# General options
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Verbose output"
)
parser.add_argument(
"--quiet", "-q",
action="store_true",
help="Suppress non-error output"
)
parser.add_argument(
"--json-output",
action="store_true",
help="Output status as JSON"
)
args = parser.parse_args()
# Handle --detect action
if args.detect:
detector = get_detector()
tools = detector.detect_installed_tools()
if args.json_output:
print(json.dumps(detector.to_dict(), indent=2))
else:
print("Detected LLM CLI Tools:")
for name, status in tools.items():
marker = "✓" if status.installed else "✗"
version = f" ({status.version})" if status.version else ""
print(f" {marker} {name}{version}")
active = detector.get_detected_llm()
if active:
print(f"\nActive LLM: {active}")
return 0
# Determine source
source: Optional[Path] = args.source
llm: Optional[str] = args.llm
if source is None:
# Find source from CLI tool detector
detector = get_detector()
if llm:
# Use specified LLM
source = detector.get_active_session(llm)
if source is None:
locations = detector.get_session_locations(llm)
if locations:
source = locations[0]
else:
# Auto-detect active LLM
llm = detector.get_detected_llm()
if llm:
source = detector.get_active_session(llm)
if source is None:
if not args.quiet:
print("Error: No session found. Use --source to specify a file.", file=sys.stderr)
return 1
# Handle --list action
if args.list:
try:
extractor = get_or_detect(source, llm)
sessions = extractor.list_sessions(source)
if args.json_output:
print(json.dumps([
{**s.to_dict(), "source_path": str(s.source_path)}
for s in sessions
], indent=2))
else:
print(f"Sessions in {source}:")
for s in sessions[:20]:
date_str = s.started_at.strftime("%Y-%m-%d %H:%M") if s.started_at else "unknown"
print(f" {s.session_id}: {date_str}")
if len(sessions) > 20:
print(f" ... and {len(sessions) - 20} more")
except ValueError as e:
if not args.quiet:
print(f"Error: {e}", file=sys.stderr)
return 1
return 0
# Get extractor
try:
extractor = get_or_detect(source, llm)
llm = extractor.llm_name
except ValueError as e:
if not args.quiet:
print(f"Error: {e}", file=sys.stderr)
return 1
if args.verbose:
print(f"Using {llm} extractor for {source}")
# Extract session
result = extractor.extract(
source,
session_id=args.session_id,
include_tool_results=not args.no_tool_results,
include_thinking=not args.no_thinking
)
if not result.success:
if not args.quiet:
print(f"Extraction failed: {result.errors}", file=sys.stderr)
return 1
if args.verbose:
print(f"Extracted {result.entry_count} entries from session {result.metadata.session_id}")
# Dry run - just show stats
if args.dry_run:
if args.json_output:
print(json.dumps({
"would_export": True,
"llm": llm,
"session_id": result.metadata.session_id,
"entries": result.entry_count,
"messages": result.metadata.total_messages,
"tokens_input": result.metadata.total_tokens_input,
"tokens_output": result.metadata.total_tokens_output
}, indent=2))
else:
print(f"Would export session {result.metadata.session_id}")
print(f" LLM: {llm}")
print(f" Model: {result.metadata.llm_model or 'unknown'}")
print(f" Entries: {result.entry_count}")
print(f" Messages: {result.metadata.total_messages}")
print(f" Tokens: {result.metadata.total_tokens_input} in, {result.metadata.total_tokens_output} out")
return 0
# Determine output path
output_path = args.output
if output_path is None:
output_path = get_default_output_path(llm, result.metadata.session_id, args.format)
if args.verbose:
print(f"Writing to {output_path}")
# Format and write
formatter = CUSFFormatter(
include_raw=args.reconstruct,
include_thinking=not args.no_thinking
)
try:
with OutputWriter(output_path, format=args.format, compress=args.compress, append=args.append) as writer:
count = writer.write_all(formatter.format(result))
except IOError as e:
if not args.quiet:
print(f"Write error: {e}", file=sys.stderr)
return 1
# Output result
if args.json_output:
print(json.dumps({
"success": True,
"output": str(output_path),
"format": args.format,
"entries": count,
"session_id": result.metadata.session_id,
"llm": llm
}, indent=2))
elif not args.quiet:
print(f"✓ Exported {count} entries to {output_path}")
print(f" Session: {result.metadata.session_id}")
print(f" LLM: {llm}")
if result.warnings:
print(f" Warnings: {len(result.warnings)}")
return 0
if name == "main": sys.exit(main())