feat: add --session-id flag for exporting a single #103
Conversation
Adds a new `--session-id` option to the CLI that exports a single conversation to HTML or Markdown without launching the TUI. Supports full session IDs and 8-char short prefixes, including archived sessions. https://claude.ai/code/session_01S99zF7j4LnfKCqbvyCXJV5
Tests cover input validation, session ID resolution (exact/prefix/ambiguous), output path logic, no-cache mode, and session title generation from cache metadata. https://claude.ai/code/session_01Gbx7zBdyjKkQq8frBfcP5M
📝 WalkthroughWalkthroughAdds a single-session export flow: a new Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/CLI
participant CLI as claude_code_log/cli.py
participant Converter as claude_code_log/converter.py
participant Cache as Cache DB
participant FS as File System
participant Renderer as Renderer
User->>CLI: run CLI with --session-id
CLI->>CLI: validate/normalize input_path
CLI->>Converter: generate_single_session_file(format, input_path, session_id, output, use_cache, image_mode)
Converter->>FS: load project transcripts (JSONL)
Converter->>Converter: resolve session_id (exact or unique prefix)
alt use_cache
Converter->>Cache: query for session metadata
Cache-->>Converter: session cache/meta (or empty)
end
Converter->>Converter: build_session_title(project_title, session_id, session_cache)
Converter->>Renderer: render session to requested format
Renderer-->>Converter: rendered content
Converter->>FS: write output file
FS-->>Converter: output path
Converter-->>CLI: return output path
CLI->>User: print success message (optionally open browser)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
claude_code_log/cli.py (1)
548-690:⚠️ Potential issue | 🟡 MinorAdd explicit conflict checks for
--session-idcombinations.
--session-idcan be silently ignored when combined with--tui(and is ambiguous with--all-projects). Fail fast with a clear CLI error.Suggested fix
try: + if session_id is not None and tui: + click.echo("Error: --session-id cannot be used with --tui", err=True) + sys.exit(1) + if session_id is not None and all_projects: + click.echo("Error: --session-id cannot be used with --all-projects", err=True) + sys.exit(1) + # Handle TUI mode if tui:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/cli.py` around lines 548 - 690, The CLI currently allows --session-id to be combined with conflicting flags (tui, all_projects) and may silently ignore session_id; add an explicit conflict check early in the command handler: if session_id is not None and tui is True or session_id is not None and all_projects is True, raise a clear click.UsageError or echo an error and sys.exit(1) so the command fails fast; update the top of the TUI/argument handling (near variables session_id, tui, all_projects) to validate these combinations before any project discovery/convert_project_path_to_claude_dir or generate_single_session_file calls, listing the conflicting flags in the message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/converter.py`:
- Around line 1680-1733: Single-session export currently passes raw
session_messages to renderer.generate_session and skips the deduplication step
used by the main export flow; locate the deduplication helper used elsewhere in
the codebase (e.g., dedupe_session_entries / deduplicate_messages / similar) and
apply it to session_messages (or via cache_manager if that provides dedupe)
before calling renderer.generate_session so duplicates/stutter are removed while
preserving existing use of session_messages, matched_id, session_title and
cache_manager.
- Around line 1640-1643: The code currently constructs cache_manager =
CacheManager(input_path, get_library_version()) when use_cache is true, but that
will raise and abort the export if CacheManager initialization fails (DB
lock/corruption/permission); wrap the CacheManager(...) call in a try/except
that catches Exception, logs the error (include exception details) and falls
back to disabling cache by setting cache_manager = None (and optionally set
use_cache = False) so the export proceeds without cache; reference the
CacheManager constructor call and the cache_manager variable when making this
change.
---
Outside diff comments:
In `@claude_code_log/cli.py`:
- Around line 548-690: The CLI currently allows --session-id to be combined with
conflicting flags (tui, all_projects) and may silently ignore session_id; add an
explicit conflict check early in the command handler: if session_id is not None
and tui is True or session_id is not None and all_projects is True, raise a
clear click.UsageError or echo an error and sys.exit(1) so the command fails
fast; update the top of the TUI/argument handling (near variables session_id,
tui, all_projects) to validate these combinations before any project
discovery/convert_project_path_to_claude_dir or generate_single_session_file
calls, listing the conflicting flags in the message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1fba01b5-3cbf-465f-8e15-9df030ddff46
📒 Files selected for processing (4)
claude_code_log/cli.pyclaude_code_log/converter.pytest/test_cli.pytest/test_session_export.py
There was a problem hiding this comment.
🧹 Nitpick comments (1)
claude_code_log/converter.py (1)
1472-1486: Consider adding a docstring and tightening the type hint.This is a public helper now imported by
tui.py. A brief docstring would clarify its purpose. Additionally,Optional[Any]could beOptional[SessionCacheData]for better type safety.📝 Suggested improvement
def build_session_title( project_title: str, session_id: str, - session_cache: Optional[Any], + session_cache: Optional[SessionCacheData], ) -> str: + """Build a display title for a session. + + Uses the session summary if available, otherwise the first user message + preview (truncated to 50 chars), falling back to "Session {id[:8]}". + """ if session_cache:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1472 - 1486, Add a short docstring to build_session_title describing its purpose, parameters (project_title, session_id, session_cache) and return value; tighten the type hint of session_cache from Optional[Any] to Optional[SessionCacheData] (import or forward-declare SessionCacheData if needed) and ensure any attribute access (summary, first_user_message) aligns with that type. Keep the existing logic intact and update the function signature and docstring accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@claude_code_log/converter.py`:
- Around line 1472-1486: Add a short docstring to build_session_title describing
its purpose, parameters (project_title, session_id, session_cache) and return
value; tighten the type hint of session_cache from Optional[Any] to
Optional[SessionCacheData] (import or forward-declare SessionCacheData if
needed) and ensure any attribute access (summary, first_user_message) aligns
with that type. Keep the existing logic intact and update the function signature
and docstring accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cff9a1a6-f1d3-44b6-b144-92588f7d143f
📒 Files selected for processing (2)
claude_code_log/converter.pyclaude_code_log/tui.py
There was a problem hiding this comment.
🧹 Nitpick comments (1)
claude_code_log/converter.py (1)
1661-1696: Consider lazy loading for single-session export performance.The function loads all messages from the directory (line 1661) before filtering to the target session (lines 1692-1696). For large projects, this is inefficient when exporting a single session.
This is acceptable for the current CLI use case, but if this becomes a hot path, consider:
- Using cache metadata to resolve session IDs without loading all messages
- Loading only the specific session's JSONL file when the full session ID is provided
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@claude_code_log/converter.py` around lines 1661 - 1696, The code currently calls load_directory_transcripts(input_path, cache_manager) and loads all messages into messages before resolving session_id; change this to resolve matched_id using cache metadata first (use cache_manager.get_cached_project_data() and project_cache.sessions) and only call load_directory_transcripts when necessary (e.g., if session_id is ambiguous or not found in cache), and if the provided session_id is a full ID, load just that session's JSONL file instead of the whole directory; update the logic around matched_id, messages, and session_messages (referencing load_directory_transcripts, cache_manager, project_cache, session_id, matched_id, messages, session_messages) so we lazily load transcripts for a single-session export.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@claude_code_log/converter.py`:
- Around line 1661-1696: The code currently calls
load_directory_transcripts(input_path, cache_manager) and loads all messages
into messages before resolving session_id; change this to resolve matched_id
using cache metadata first (use cache_manager.get_cached_project_data() and
project_cache.sessions) and only call load_directory_transcripts when necessary
(e.g., if session_id is ambiguous or not found in cache), and if the provided
session_id is a full ID, load just that session's JSONL file instead of the
whole directory; update the logic around matched_id, messages, and
session_messages (referencing load_directory_transcripts, cache_manager,
project_cache, session_id, matched_id, messages, session_messages) so we lazily
load transcripts for a single-session export.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9bc4602d-c3ea-47d6-9bc1-dc188181ff88
📒 Files selected for processing (1)
claude_code_log/converter.py
|
Hey, thanks a lot for this PR! What do you think about dropping the Session ids should be globally unique and now we have a SQLite cache, so it's a simple lookup. |
When --session-id is used without a project path, look up the session across all projects in the SQLite cache. This makes the common case simpler: `claude-code-log --session-id abc12345` instead of requiring the full project directory path. Adds find_session_in_cache() to cache.py that queries sessions by ID or prefix across all projects with a single SQL JOIN. Handles ambiguous prefixes matching different sessions with a clear error message. Falls back to a helpful error when the session isn't in cache, suggesting to provide a path or run claude-code-log first to populate the cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude_code_log/cache.py`:
- Around line 1580-1597: The except block in the session lookup silently
converts sqlite3.Error/OSError into an empty list (making errors look like
"session not found"); change this so cache I/O errors are propagated instead of
returning [] — either remove the except swallowing entirely or re-raise the
caught exception after logging (keep the logger.debug call). Update the handler
around the sqlite3.connect / conn.execute block (the try/except that currently
logs "Failed to find session in cache: %s") to raise the original exception (or
wrap it in a CacheError) so callers of the session lookup (the code that expects
[(project_path, session_id)]) can distinguish real "not found" results from
cache failures. Ensure conn.close() remains in the finally so the connection is
always closed.
In `@claude_code_log/cli.py`:
- Around line 659-709: The session-export branch (when session_id is set and
generate_single_session_file is called) currently returns early and therefore
silently ignores incompatible flags like tui, clear_cache, and clear_output; add
explicit validation for incompatible combinations before performing the export:
check the boolean flags tui, clear_cache, and clear_output (or corresponding
option names) at the start of the command handler or at the start of the
session_id branch and raise/echo an error and exit if any forbidden combo is
present (e.g., if session_id is set and tui is true -> error; if session_id is
set and clear_cache/clear_output true -> error) so that calls reaching
generate_single_session_file reject invalid flag combinations instead of
dropping them silently. Ensure you reference the same option variables
(session_id, tui, clear_cache, clear_output) and keep
generate_single_session_file usage intact.
- Around line 673-686: The code currently only treats ambiguity when multiple
distinct session IDs exist, but if the same full session_id appears in multiple
projects (matches list length > 1) it still silently picks matches[0]; update
the ambiguity check around matches/unique_ids so that any time len(matches) > 1
you treat it as ambiguous unless there is exactly one matching (project,path)
entry — i.e., check len(matches) > 1 and if so print the existing error message
and list all proj_path and session id entries (using the matches iterable,
proj_path and sid/session_id) and exit; only assign input_path and session_id
from matches[0] when there is exactly one match. Ensure you reference the same
variables used now: matches, unique_ids, session_id, input_path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7723060c-5828-4293-bdf8-db7785ce6e12
📒 Files selected for processing (3)
claude_code_log/cache.pyclaude_code_log/cli.pytest/test_cli.py
✅ Files skipped from review due to trivial changes (1)
- test/test_cli.py
| try: | ||
| conn = sqlite3.connect(actual_db_path, timeout=30.0) | ||
| conn.row_factory = sqlite3.Row | ||
| try: | ||
| rows = conn.execute( | ||
| """SELECT p.project_path, s.session_id | ||
| FROM sessions s | ||
| JOIN projects p ON s.project_id = p.id | ||
| WHERE s.session_id = ? OR s.session_id LIKE ? | ||
| ORDER BY s.first_timestamp DESC""", | ||
| (session_id, f"{session_id}%"), | ||
| ).fetchall() | ||
| return [(row["project_path"], row["session_id"]) for row in rows] | ||
| finally: | ||
| conn.close() | ||
| except (sqlite3.Error, OSError) as e: | ||
| logger.debug("Failed to find session in cache: %s", e) | ||
| return [] |
There was a problem hiding this comment.
Propagate cache lookup failures instead of reporting "not found".
Line 1595 converts SQLite/OS errors into [], but the caller treats [] as "session missing". A corrupt, locked, or unreadable cache will therefore send users down the wrong remediation path.
Suggested fix
try:
conn = sqlite3.connect(actual_db_path, timeout=30.0)
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
@@
).fetchall()
return [(row["project_path"], row["session_id"]) for row in rows]
finally:
conn.close()
except (sqlite3.Error, OSError) as e:
- logger.debug("Failed to find session in cache: %s", e)
- return []
+ raise RuntimeError(
+ f"Failed to query session cache at {actual_db_path}"
+ ) from e🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/cache.py` around lines 1580 - 1597, The except block in the
session lookup silently converts sqlite3.Error/OSError into an empty list
(making errors look like "session not found"); change this so cache I/O errors
are propagated instead of returning [] — either remove the except swallowing
entirely or re-raise the caught exception after logging (keep the logger.debug
call). Update the handler around the sqlite3.connect / conn.execute block (the
try/except that currently logs "Failed to find session in cache: %s") to raise
the original exception (or wrap it in a CacheError) so callers of the session
lookup (the code that expects [(project_path, session_id)]) can distinguish real
"not found" results from cache failures. Ensure conn.close() remains in the
finally so the connection is always closed.
| # Handle --session-id: export a single session by ID | ||
| if session_id is not None: | ||
| if input_path is None: | ||
| # Global lookup via cache | ||
| effective_projects_dir = projects_dir or get_default_projects_dir() | ||
| matches = find_session_in_cache(session_id, effective_projects_dir) | ||
| if not matches: | ||
| click.echo( | ||
| f"Error: Session '{session_id}' not found in cache. " | ||
| "Try providing a project directory path, or run " | ||
| "claude-code-log first to populate the cache.", | ||
| err=True, | ||
| ) | ||
| sys.exit(1) | ||
| if len(matches) > 1: | ||
| # Check if all matches resolve to the same session ID | ||
| unique_ids = {m[1] for m in matches} | ||
| if len(unique_ids) > 1: | ||
| click.echo( | ||
| f"Error: Ambiguous session ID prefix '{session_id}' " | ||
| "matches multiple sessions:", | ||
| err=True, | ||
| ) | ||
| for proj_path, sid in matches: | ||
| click.echo(f" {sid[:8]} in {proj_path}", err=True) | ||
| sys.exit(1) | ||
| input_path = Path(matches[0][0]) | ||
| session_id = matches[0][1] | ||
| else: | ||
| # Convert project path if needed | ||
| if not input_path.exists() or ( | ||
| input_path.is_dir() and not list(input_path.glob("*.jsonl")) | ||
| ): | ||
| claude_path = convert_project_path_to_claude_dir( | ||
| input_path, projects_dir | ||
| ) | ||
| if claude_path.exists(): | ||
| input_path = claude_path | ||
|
|
||
| output_path = generate_single_session_file( | ||
| output_format, | ||
| input_path, | ||
| session_id, | ||
| output, | ||
| not no_cache, | ||
| image_export_mode, | ||
| ) | ||
| click.echo(f"Successfully exported session to {output_path}") | ||
| if open_browser: | ||
| click.launch(str(output_path)) | ||
| return |
There was a problem hiding this comment.
Reject flag combinations that this early return currently skips.
Because the if tui: branch above returns before Line 659 runs, --tui --session-id ignores --session-id. The reverse also happens here: this branch returns before the --clear-cache and --clear-output handlers, so those flags are silently dropped too.
Suggested fix
try:
+ if tui and session_id is not None:
+ click.echo("Error: --tui cannot be used with --session-id", err=True)
+ sys.exit(1)
+ if session_id is not None and (clear_cache or clear_output):
+ click.echo(
+ "Error: --clear-cache/--clear-output cannot be used with --session-id",
+ err=True,
+ )
+ sys.exit(1)
+
# Handle TUI mode
if tui:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude_code_log/cli.py` around lines 659 - 709, The session-export branch
(when session_id is set and generate_single_session_file is called) currently
returns early and therefore silently ignores incompatible flags like tui,
clear_cache, and clear_output; add explicit validation for incompatible
combinations before performing the export: check the boolean flags tui,
clear_cache, and clear_output (or corresponding option names) at the start of
the command handler or at the start of the session_id branch and raise/echo an
error and exit if any forbidden combo is present (e.g., if session_id is set and
tui is true -> error; if session_id is set and clear_cache/clear_output true ->
error) so that calls reaching generate_single_session_file reject invalid flag
combinations instead of dropping them silently. Ensure you reference the same
option variables (session_id, tui, clear_cache, clear_output) and keep
generate_single_session_file usage intact.
Hello, this is a PR to propose adding a
--session-idflag to allow exporting a single conversation by session ID.Sample usage:
Summary by CodeRabbit
New Features
Bug Fixes / Robustness
Tests