Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ claude-code-transcripts local --limit 20
Import sessions directly from the Claude API:

```bash
# List available web sessions
claude-code-transcripts web --list

# Interactive session picker
claude-code-transcripts web

Expand All @@ -87,7 +90,17 @@ claude-code-transcripts web SESSION_ID
claude-code-transcripts web SESSION_ID --gist
```

The session picker displays sessions grouped by their associated GitHub repository:
Use `--list` to display all available sessions without converting:

```bash
# List all sessions
claude-code-transcripts web --list

# List sessions for a specific repo
claude-code-transcripts web --list --repo owner/repo
```

The session list displays sessions grouped by their associated GitHub repository:

```
simonw/datasette 2025-01-15T10:30:00 Fix the bug in query parser
Expand Down
45 changes: 45 additions & 0 deletions src/claude_code_transcripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,12 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
is_flag=True,
help="Open the generated index.html in your default browser (default if no -o specified).",
)
@click.option(
"--list",
"list_only",
is_flag=True,
help="List available sessions and exit without converting.",
)
def web_cmd(
session_id,
output,
Expand All @@ -1993,6 +1999,7 @@ def web_cmd(
gist,
include_json,
open_browser,
list_only,
):
"""Select and convert a web session from the Claude API to HTML.

Expand Down Expand Up @@ -2027,6 +2034,44 @@ def web_cmd(
if not sessions:
raise click.ClickException(f"No sessions found for repo: {repo}")

# If --list flag is set, display sessions and exit
if list_only:
click.echo(f"\nFound {len(sessions)} session(s):\n")
click.echo("-" * 100)

# Group by repo
by_repo = {}
for session in sessions:
session_repo = session.get("repo") or "(no repo)"
if session_repo not in by_repo:
by_repo[session_repo] = []
by_repo[session_repo].append(session)

# Display sessions grouped by repo
for session_repo in sorted(by_repo.keys()):
click.echo(f"\n{session_repo}")
click.echo("-" * 100)
for s in by_repo[session_repo]:
sid = s.get("id", "N/A")
title = s.get("title", "(no title)")
created_at = s.get("created_at", "")

# Format the date
date_str = created_at[:19] if created_at else "N/A"

# Truncate title if too long
if len(title) > 60:
title = title[:57] + "..."

click.echo(f" {sid} {date_str} {title}")

click.echo("\n" + "-" * 100)
click.echo(f"\nTotal: {len(sessions)} session(s)")
click.echo(
"\nTo export a session, run: claude-code-transcripts web <session-id>"
)
return

# Build choices for questionary
choices = []
for s in sessions:
Expand Down
149 changes: 149 additions & 0 deletions tests/test_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Tests for web command."""

import json
from unittest.mock import Mock, patch

import pytest
from click.testing import CliRunner

from claude_code_transcripts import cli


class TestWebCommandList:
"""Tests for web command --list flag."""

@patch("claude_code_transcripts.fetch_sessions")
@patch("claude_code_transcripts.get_access_token_from_keychain")
@patch("claude_code_transcripts.get_org_uuid_from_config")
def test_list_displays_sessions_grouped_by_repo(
self, mock_get_org, mock_get_token, mock_fetch
):
"""Should display sessions grouped by repo when --list flag is used."""
# Setup mocks
mock_get_token.return_value = "test-token"
mock_get_org.return_value = "test-org-uuid"

# Mock API response with sessions
mock_fetch.return_value = {
"data": [
{
"id": "session-123",
"title": "Fix authentication bug",
"created_at": "2026-01-30T10:00:00Z",
"session_context": {
"outcomes": [
{
"type": "git_repository",
"git_info": {"repo": "owner/repo1", "type": "github"},
}
]
},
},
{
"id": "session-456",
"title": "Add new feature",
"created_at": "2026-01-29T15:30:00Z",
"session_context": {
"outcomes": [
{
"type": "git_repository",
"git_info": {"repo": "owner/repo1", "type": "github"},
}
]
},
},
{
"id": "session-789",
"title": "Update documentation",
"created_at": "2026-01-28T09:15:00Z",
"session_context": {},
},
]
}

runner = CliRunner()
result = runner.invoke(cli, ["web", "--list"])

# Verify exit code
assert result.exit_code == 0

# Verify output contains session IDs
assert "session-123" in result.output
assert "session-456" in result.output
assert "session-789" in result.output

# Verify output contains titles
assert "Fix authentication bug" in result.output
assert "Add new feature" in result.output
assert "Update documentation" in result.output

# Verify output shows grouping by repo
assert "owner/repo1" in result.output
assert "(no repo)" in result.output

# Verify count is displayed
assert "3 session(s)" in result.output or "Total: 3" in result.output

@patch("claude_code_transcripts.fetch_sessions")
@patch("claude_code_transcripts.get_access_token_from_keychain")
@patch("claude_code_transcripts.get_org_uuid_from_config")
def test_list_with_repo_filter(self, mock_get_org, mock_get_token, mock_fetch):
"""Should filter sessions by repo when --repo flag is used with --list."""
mock_get_token.return_value = "test-token"
mock_get_org.return_value = "test-org-uuid"

mock_fetch.return_value = {
"data": [
{
"id": "session-123",
"title": "Session in repo1",
"created_at": "2026-01-30T10:00:00Z",
"session_context": {
"outcomes": [
{
"type": "git_repository",
"git_info": {"repo": "owner/repo1", "type": "github"},
}
]
},
},
{
"id": "session-456",
"title": "Session in repo2",
"created_at": "2026-01-29T15:30:00Z",
"session_context": {
"outcomes": [
{
"type": "git_repository",
"git_info": {"repo": "owner/repo2", "type": "github"},
}
]
},
},
]
}

runner = CliRunner()
result = runner.invoke(cli, ["web", "--list", "--repo", "owner/repo1"])

assert result.exit_code == 0
assert "session-123" in result.output
assert "Session in repo1" in result.output
# Should not show session from repo2
assert "session-456" not in result.output
assert "Session in repo2" not in result.output

@patch("claude_code_transcripts.fetch_sessions")
@patch("claude_code_transcripts.get_access_token_from_keychain")
@patch("claude_code_transcripts.get_org_uuid_from_config")
def test_list_with_no_sessions(self, mock_get_org, mock_get_token, mock_fetch):
"""Should display appropriate message when no sessions found."""
mock_get_token.return_value = "test-token"
mock_get_org.return_value = "test-org-uuid"
mock_fetch.return_value = {"data": []}

runner = CliRunner()
result = runner.invoke(cli, ["web", "--list"])

assert result.exit_code != 0
assert "No sessions found" in result.output