Skip to content

feat: add --session-id flag for exporting a single #103

Merged
cboos merged 8 commits intodaaain:mainfrom
chloelee767:claude/add-conversation-export-cli-deIod
Apr 12, 2026
Merged

feat: add --session-id flag for exporting a single #103
cboos merged 8 commits intodaaain:mainfrom
chloelee767:claude/add-conversation-export-cli-deIod

Conversation

@chloelee767
Copy link
Copy Markdown
Contributor

@chloelee767 chloelee767 commented Mar 13, 2026

Hello, this is a PR to propose adding a --session-id flag to allow exporting a single conversation by session ID.

Sample usage:

claude-code-log ~/.claude/projects/-path-to-project -o conversation.md -f md --session-id 73158d47-d83c-4f61-bca4-d62532664301

# just using the prefix is also supported
claude-code-log ~/.claude/projects/-path-to-project -o conversation.md -f md --session-id 73158d47

Summary by CodeRabbit

  • New Features

    • Add a --session-id option to export a single session (supports short‑prefix lookup), with optional automatic opening of the result.
    • Single‑session export can resolve sessions via the cache when a project path isn’t provided.
  • Bug Fixes / Robustness

    • Clearer errors for missing, ambiguous, or non‑existent session IDs; better handling of user paths and malformed project data.
    • Support for disabling cache during export.
  • Tests

    • New tests covering single‑session export, ID resolution, outputs, formats, and error cases.

claude and others added 4 commits March 12, 2026 02:04
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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds a single-session export flow: a new --session-id CLI option routes to generate_single_session_file, which resolves session IDs (exact or unique prefix), consults the cache if enabled, derives a session title, renders a single-session output file, writes it to disk, and returns early from the multi-project export path. Tests added.

Changes

Cohort / File(s) Summary
CLI Session Export Integration
claude_code_log/cli.py
Added --session-id option and session_id parameter to main, normalize/validate input_path, import/invoke generate_single_session_file, print result and optionally open browser, return early to skip multi-project flow.
Session Export Core Logic
claude_code_log/converter.py
Added build_session_title helper and generate_single_session_file (duplicate definition present in patch). Implements cache init/lookup, session ID resolution (exact or unique prefix), archived-session loading, message loading/deduplication, title derivation, rendering, and writing output.
Cache Lookup Utility
claude_code_log/cache.py
Added find_session_in_cache(session_id, projects_dir, db_path=None) to search cache DB for exact or prefix matches across projects, returning [] on DB absence/errors; exported via __all__.
TUI Title Centralization
claude_code_log/tui.py
Replaced inline per-session title logic with a call to build_session_title (import added).
CLI Tests
test/test_cli.py
New tests for --session-id: missing path, full ID, short-prefix resolution, non-existent IDs, output path behavior; additional CLI error handling tests for invalid format, empty project, malformed JSONL.
Session Export Unit Tests
test/test_session_export.py
Comprehensive tests for generate_single_session_file: input validation, exact/short/ambiguous ID resolution, default/custom output paths and extension handling, no-cache mode, session title derivation variants, fixtures and helpers for isolated project dirs and cache env var.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through JSONL and cache so deep,
Found a prefix, a title, a secret to keep.
Rendered a session, a single small file—
Tests steady my paws; I rest for a while.
🥕📄

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is incomplete and cuts off mid-sentence ('exporting a single'), making it vague and failing to clearly convey the main feature being added. Complete the title to clearly describe the feature, e.g., 'feat: add --session-id flag for exporting a single session' or similar phrasing that fully captures the intent.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Add explicit conflict checks for --session-id combinations.

--session-id can 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1cf46 and 142112b.

📒 Files selected for processing (4)
  • claude_code_log/cli.py
  • claude_code_log/converter.py
  • test/test_cli.py
  • test/test_session_export.py

Comment thread claude_code_log/converter.py Outdated
Comment thread claude_code_log/converter.py
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 be Optional[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

📥 Commits

Reviewing files that changed from the base of the PR and between 142112b and a2a67ac.

📒 Files selected for processing (2)
  • claude_code_log/converter.py
  • claude_code_log/tui.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between a2a67ac and 3b0c9f8.

📒 Files selected for processing (1)
  • claude_code_log/converter.py

@daaain
Copy link
Copy Markdown
Owner

daaain commented Mar 18, 2026

Hey, thanks a lot for this PR!

What do you think about dropping the input_path requirement?

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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0c9f8 and c83d614.

📒 Files selected for processing (3)
  • claude_code_log/cache.py
  • claude_code_log/cli.py
  • test/test_cli.py
✅ Files skipped from review due to trivial changes (1)
  • test/test_cli.py

Comment thread claude_code_log/cache.py
Comment on lines +1580 to +1597
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 []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread claude_code_log/cli.py
Comment on lines +659 to +709
# 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread claude_code_log/cli.py
@cboos cboos merged commit 082af9c into daaain:main Apr 12, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants