Skip to content

Commit 2000721

Browse files
committed
--json option for saving session JSON
For every command that supports --gist add a --json option, if you pass --json then the .json version of the session that was retrieved from the API is included in the output directory (or temporary output directory) and teh path to that JSON file is shown in the output, along with its size in KB https://gistpreview.github.io/?5e11fdc8622af967450f790f052042c1
1 parent 721fa69 commit 2000721

File tree

3 files changed

+150
-6
lines changed

3 files changed

+150
-6
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ This will generate:
4040
- `-o, --output DIRECTORY` - output directory (default: current directory)
4141
- `--repo OWNER/NAME` - GitHub repo for commit links (auto-detected from git push output if not specified)
4242
- `--gist` - upload the generated HTML files to a GitHub Gist and output a preview URL
43+
- `--json` - include the original JSON session file in the output directory
4344

4445
### Publishing to GitHub Gist
4546

@@ -74,6 +75,21 @@ claude-code-publish session.json -o ./my-transcript --gist
7475

7576
**Requirements:** The `--gist` option requires the [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated (`gh auth login`).
7677

78+
### Including the JSON source
79+
80+
Use the `--json` option to include the original session JSON file in the output directory:
81+
82+
```bash
83+
claude-code-publish session.json -o ./my-transcript --json
84+
```
85+
86+
This will output:
87+
```
88+
JSON: ./my-transcript/session_ABC.json (245.3 KB)
89+
```
90+
91+
The JSON file preserves its original filename. This is useful for archiving the source data alongside the HTML output.
92+
7793
## Importing from Claude API
7894

7995
You can import sessions directly from the Claude API without needing to export a `session.json` file:
@@ -90,10 +106,15 @@ claude-code-publish import
90106

91107
# Import and publish to gist
92108
claude-code-publish import SESSION_ID --gist
109+
110+
# Import and save the JSON session data
111+
claude-code-publish import SESSION_ID --json
93112
```
94113

95114
On macOS, the API credentials are automatically retrieved from your keychain (requires being logged into Claude Code). On other platforms, provide `--token` and `--org-uuid` manually.
96115

116+
The `--json` option for the import command saves the session data fetched from the API as `{session_id}.json` in the output directory.
117+
97118
## Development
98119

99120
To contribute to this tool, first checkout the code. You can run the tests using `uv run`:

src/claude_code_publish/__init__.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import platform
77
import re
8+
import shutil
89
import subprocess
910
import tempfile
1011
from pathlib import Path
@@ -640,8 +641,7 @@ def inject_gist_preview_js(output_dir):
640641
# Insert the gist preview JS before the closing </body> tag
641642
if "</body>" in content:
642643
content = content.replace(
643-
"</body>",
644-
f"<script>{GIST_PREVIEW_JS}</script>\n</body>"
644+
"</body>", f"<script>{GIST_PREVIEW_JS}</script>\n</body>"
645645
)
646646
html_file.write_text(content)
647647

@@ -931,7 +931,13 @@ def cli():
931931
is_flag=True,
932932
help="Upload to GitHub Gist and output a gistpreview.github.io URL.",
933933
)
934-
def session(json_file, output, repo, gist):
934+
@click.option(
935+
"--json",
936+
"include_json",
937+
is_flag=True,
938+
help="Include the original JSON session file in the output directory.",
939+
)
940+
def session(json_file, output, repo, gist, include_json):
935941
"""Convert a Claude Code session JSON file to HTML."""
936942
# Determine output directory
937943
if gist and output is None:
@@ -943,8 +949,18 @@ def session(json_file, output, repo, gist):
943949
elif output is None:
944950
output = "."
945951

952+
output = Path(output)
946953
generate_html(json_file, output, github_repo=repo)
947954

955+
# Copy JSON file to output directory if requested
956+
if include_json:
957+
output.mkdir(exist_ok=True)
958+
json_source = Path(json_file)
959+
json_dest = output / json_source.name
960+
shutil.copy(json_file, json_dest)
961+
json_size_kb = json_dest.stat().st_size / 1024
962+
click.echo(f"JSON: {json_dest} ({json_size_kb:.1f} KB)")
963+
948964
if gist:
949965
# Inject gist preview JS and create gist
950966
inject_gist_preview_js(output)
@@ -1233,7 +1249,13 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
12331249
is_flag=True,
12341250
help="Upload to GitHub Gist and output a gistpreview.github.io URL.",
12351251
)
1236-
def import_session(session_id, output, token, org_uuid, repo, gist):
1252+
@click.option(
1253+
"--json",
1254+
"include_json",
1255+
is_flag=True,
1256+
help="Include the JSON session data in the output directory.",
1257+
)
1258+
def import_session(session_id, output, token, org_uuid, repo, gist, include_json):
12371259
"""Import a session from the Claude API and convert to HTML.
12381260
12391261
If SESSION_ID is not provided, displays an interactive picker to select a session.
@@ -1298,9 +1320,19 @@ def import_session(session_id, output, token, org_uuid, repo, gist):
12981320
elif output is None:
12991321
output = session_id
13001322

1323+
output = Path(output)
13011324
click.echo(f"Generating HTML in {output}/...")
13021325
generate_html_from_session_data(session_data, output, github_repo=repo)
13031326

1327+
# Save JSON session data if requested
1328+
if include_json:
1329+
output.mkdir(exist_ok=True)
1330+
json_dest = output / f"{session_id}.json"
1331+
with open(json_dest, "w") as f:
1332+
json.dump(session_data, f, indent=2)
1333+
json_size_kb = json_dest.stat().st_size / 1024
1334+
click.echo(f"JSON: {json_dest} ({json_size_kb:.1f} KB)")
1335+
13041336
if gist:
13051337
# Inject gist preview JS and create gist
13061338
inject_gist_preview_js(output)

tests/test_generate_html.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,9 @@ def mock_run(*args, **kwargs):
577577
monkeypatch.setattr(subprocess, "run", mock_run)
578578

579579
# Mock tempfile.gettempdir to use our tmp_path
580-
monkeypatch.setattr("claude_code_publish.tempfile.gettempdir", lambda: str(tmp_path))
580+
monkeypatch.setattr(
581+
"claude_code_publish.tempfile.gettempdir", lambda: str(tmp_path)
582+
)
581583

582584
runner = CliRunner()
583585
result = runner.invoke(
@@ -624,6 +626,93 @@ def mock_run(*args, **kwargs):
624626
assert "gistpreview.github.io" in index_content
625627

626628

629+
class TestSessionJsonOption:
630+
"""Tests for the session command --json option."""
631+
632+
def test_session_json_copies_file(self, output_dir):
633+
"""Test that session --json copies the JSON file to output."""
634+
from click.testing import CliRunner
635+
from claude_code_publish import cli
636+
637+
fixture_path = Path(__file__).parent / "sample_session.json"
638+
639+
runner = CliRunner()
640+
result = runner.invoke(
641+
cli,
642+
["session", str(fixture_path), "-o", str(output_dir), "--json"],
643+
)
644+
645+
assert result.exit_code == 0
646+
json_file = output_dir / "sample_session.json"
647+
assert json_file.exists()
648+
assert "JSON:" in result.output
649+
assert "KB" in result.output
650+
651+
def test_session_json_preserves_original_name(self, output_dir):
652+
"""Test that --json preserves the original filename."""
653+
from click.testing import CliRunner
654+
from claude_code_publish import cli
655+
656+
fixture_path = Path(__file__).parent / "sample_session.json"
657+
658+
runner = CliRunner()
659+
result = runner.invoke(
660+
cli,
661+
["session", str(fixture_path), "-o", str(output_dir), "--json"],
662+
)
663+
664+
assert result.exit_code == 0
665+
# Should use original filename, not "session.json"
666+
assert (output_dir / "sample_session.json").exists()
667+
assert not (output_dir / "session.json").exists()
668+
669+
670+
class TestImportJsonOption:
671+
"""Tests for the import command --json option."""
672+
673+
def test_import_json_saves_session_data(self, httpx_mock, output_dir):
674+
"""Test that import --json saves the session JSON."""
675+
from click.testing import CliRunner
676+
from claude_code_publish import cli
677+
678+
# Load sample session to mock API response
679+
fixture_path = Path(__file__).parent / "sample_session.json"
680+
with open(fixture_path) as f:
681+
session_data = json.load(f)
682+
683+
httpx_mock.add_response(
684+
url="https://api.anthropic.com/v1/session_ingress/session/test-session-id",
685+
json=session_data,
686+
)
687+
688+
runner = CliRunner()
689+
result = runner.invoke(
690+
cli,
691+
[
692+
"import",
693+
"test-session-id",
694+
"--token",
695+
"test-token",
696+
"--org-uuid",
697+
"test-org",
698+
"-o",
699+
str(output_dir),
700+
"--json",
701+
],
702+
)
703+
704+
assert result.exit_code == 0
705+
json_file = output_dir / "test-session-id.json"
706+
assert json_file.exists()
707+
assert "JSON:" in result.output
708+
assert "KB" in result.output
709+
710+
# Verify JSON content is valid
711+
with open(json_file) as f:
712+
saved_data = json.load(f)
713+
assert saved_data == session_data
714+
715+
627716
class TestImportGistOption:
628717
"""Tests for the import command --gist option."""
629718

@@ -657,7 +746,9 @@ def mock_run(*args, **kwargs):
657746
monkeypatch.setattr(subprocess, "run", mock_run)
658747

659748
# Mock tempfile.gettempdir
660-
monkeypatch.setattr("claude_code_publish.tempfile.gettempdir", lambda: str(tmp_path))
749+
monkeypatch.setattr(
750+
"claude_code_publish.tempfile.gettempdir", lambda: str(tmp_path)
751+
)
661752

662753
runner = CliRunner()
663754
result = runner.invoke(

0 commit comments

Comments
 (0)