Skip to content

Commit fd2e8ff

Browse files
committed
New option --gist to publish straight to a Gist via gh CLI
Add a --gist feature to both of the commands that can output a session converted to HTML. If --gist is provided without a -o then it uses a tmp directory for the export (still with the session ID as the name) which is still printed out but is expected to be deleted by the OS at some point. The --gist option causes the content of that folder - the index.html and any page_*.html files as multiple files in a single gist. There is just one catch: I intend to serve those files using https://gistpreview.github.io/?3769c0736f45668e9595eb4eb8493f9c/index.html - but any links from that page need to go to https://gistpreview.github.io/?3769c0736f45668e9595eb4eb8493f9c/two.html so relative URLs will not work as they will incorrectly go to https://gistpreview.github.io/two.html. So... if --gist is used then a little bit of javascript needs to be injected into the bottom of the index.html and other pages which checks to see if the browser is accessing the page on gistpreview.github.io and if it is corrects any a href links on the page Do not forget the tests and the README https://gistpreview.github.io/?cbb6a57bc23ba2695bdc4e8d76997789
1 parent 5d66df7 commit fd2e8ff

File tree

3 files changed

+423
-7
lines changed

3 files changed

+423
-7
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,52 @@ This will generate:
3939

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)
42+
- `--gist` - upload the generated HTML files to a GitHub Gist and output a preview URL
43+
44+
### Publishing to GitHub Gist
45+
46+
Use the `--gist` option to automatically upload your transcript to a GitHub Gist and get a shareable preview URL:
47+
48+
```bash
49+
claude-code-publish session.json --gist
50+
```
51+
52+
This will output something like:
53+
```
54+
Gist: https://gist.github.com/username/abc123def456
55+
Preview: https://gistpreview.github.io/?abc123def456/index.html
56+
Files: /var/folders/.../session-id
57+
```
58+
59+
The preview URL uses [gistpreview.github.io](https://gistpreview.github.io/) to render your HTML gist. The tool automatically injects JavaScript to fix relative links when served through gistpreview.
60+
61+
When using `--gist` without `-o`, files are written to a temporary directory (shown in the output). You can combine both options to keep a local copy:
62+
63+
```bash
64+
claude-code-publish session.json -o ./my-transcript --gist
65+
```
66+
67+
**Requirements:** The `--gist` option requires the [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated (`gh auth login`).
68+
69+
## Importing from Claude API
70+
71+
You can import sessions directly from the Claude API without needing to export a `session.json` file:
72+
73+
```bash
74+
# List available sessions
75+
claude-code-publish list-web
76+
77+
# Import a specific session
78+
claude-code-publish import SESSION_ID -o output-directory/
79+
80+
# Import with interactive session picker
81+
claude-code-publish import
82+
83+
# Import and publish to gist
84+
claude-code-publish import SESSION_ID --gist
85+
```
86+
87+
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.
4288

4389
## Development
4490

src/claude_code_publish/__init__.py

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import re
88
import subprocess
9+
import tempfile
910
from pathlib import Path
1011

1112
import click
@@ -609,6 +610,79 @@ def render_message(log_type, message_json, timestamp):
609610
});
610611
"""
611612

613+
# JavaScript to fix relative URLs when served via gistpreview.github.io
614+
GIST_PREVIEW_JS = r"""
615+
(function() {
616+
if (window.location.hostname !== 'gistpreview.github.io') return;
617+
// URL format: https://gistpreview.github.io/?GIST_ID/filename.html
618+
var match = window.location.search.match(/^\?([^/]+)/);
619+
if (!match) return;
620+
var gistId = match[1];
621+
document.querySelectorAll('a[href]').forEach(function(link) {
622+
var href = link.getAttribute('href');
623+
// Skip external links and anchors
624+
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('//')) return;
625+
// Handle anchor in relative URL (e.g., page-001.html#msg-123)
626+
var parts = href.split('#');
627+
var filename = parts[0];
628+
var anchor = parts.length > 1 ? '#' + parts[1] : '';
629+
link.setAttribute('href', '?' + gistId + '/' + filename + anchor);
630+
});
631+
})();
632+
"""
633+
634+
635+
def inject_gist_preview_js(output_dir):
636+
"""Inject gist preview JavaScript into all HTML files in the output directory."""
637+
output_dir = Path(output_dir)
638+
for html_file in output_dir.glob("*.html"):
639+
content = html_file.read_text()
640+
# Insert the gist preview JS before the closing </body> tag
641+
if "</body>" in content:
642+
content = content.replace(
643+
"</body>",
644+
f"<script>{GIST_PREVIEW_JS}</script>\n</body>"
645+
)
646+
html_file.write_text(content)
647+
648+
649+
def create_gist(output_dir, public=False):
650+
"""Create a GitHub gist from the HTML files in output_dir.
651+
652+
Returns the gist ID on success, or raises click.ClickException on failure.
653+
"""
654+
output_dir = Path(output_dir)
655+
html_files = list(output_dir.glob("*.html"))
656+
if not html_files:
657+
raise click.ClickException("No HTML files found to upload to gist.")
658+
659+
# Build the gh gist create command
660+
# gh gist create file1 file2 ... --public/--private
661+
cmd = ["gh", "gist", "create"]
662+
cmd.extend(str(f) for f in sorted(html_files))
663+
if public:
664+
cmd.append("--public")
665+
666+
try:
667+
result = subprocess.run(
668+
cmd,
669+
capture_output=True,
670+
text=True,
671+
check=True,
672+
)
673+
# Output is the gist URL, e.g., https://gist.github.com/username/GIST_ID
674+
gist_url = result.stdout.strip()
675+
# Extract gist ID from URL
676+
gist_id = gist_url.rstrip("/").split("/")[-1]
677+
return gist_id, gist_url
678+
except subprocess.CalledProcessError as e:
679+
error_msg = e.stderr.strip() if e.stderr else str(e)
680+
raise click.ClickException(f"Failed to create gist: {error_msg}")
681+
except FileNotFoundError:
682+
raise click.ClickException(
683+
"gh CLI not found. Install it from https://cli.github.com/ and run 'gh auth login'."
684+
)
685+
612686

613687
def generate_pagination_html(current_page, total_pages):
614688
if total_pages <= 1:
@@ -845,18 +919,42 @@ def cli():
845919
@click.option(
846920
"-o",
847921
"--output",
848-
default=".",
849922
type=click.Path(),
850-
help="Output directory (default: current directory)",
923+
help="Output directory (default: current directory, or temp dir with --gist)",
851924
)
852925
@click.option(
853926
"--repo",
854927
help="GitHub repo (owner/name) for commit links. Auto-detected from git push output if not specified.",
855928
)
856-
def session(json_file, output, repo):
929+
@click.option(
930+
"--gist",
931+
is_flag=True,
932+
help="Upload to GitHub Gist and output a gistpreview.github.io URL.",
933+
)
934+
def session(json_file, output, repo, gist):
857935
"""Convert a Claude Code session JSON file to HTML."""
936+
# Determine output directory
937+
if gist and output is None:
938+
# Extract session ID from JSON file for temp directory name
939+
with open(json_file, "r") as f:
940+
data = json.load(f)
941+
session_id = data.get("sessionId", Path(json_file).stem)
942+
output = Path(tempfile.gettempdir()) / session_id
943+
elif output is None:
944+
output = "."
945+
858946
generate_html(json_file, output, github_repo=repo)
859947

948+
if gist:
949+
# Inject gist preview JS and create gist
950+
inject_gist_preview_js(output)
951+
click.echo("Creating GitHub gist...")
952+
gist_id, gist_url = create_gist(output)
953+
preview_url = f"https://gistpreview.github.io/?{gist_id}/index.html"
954+
click.echo(f"Gist: {gist_url}")
955+
click.echo(f"Preview: {preview_url}")
956+
click.echo(f"Files: {output}")
957+
860958

861959
def resolve_credentials(token, org_uuid):
862960
"""Resolve token and org_uuid from arguments or auto-detect.
@@ -1120,7 +1218,7 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
11201218
"-o",
11211219
"--output",
11221220
type=click.Path(),
1123-
help="Output directory (default: creates folder with session ID)",
1221+
help="Output directory (default: creates folder with session ID, or temp dir with --gist)",
11241222
)
11251223
@click.option("--token", help="API access token (auto-detected from keychain on macOS)")
11261224
@click.option(
@@ -1130,7 +1228,12 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
11301228
"--repo",
11311229
help="GitHub repo (owner/name) for commit links. Auto-detected from git push output if not specified.",
11321230
)
1133-
def import_session(session_id, output, token, org_uuid, repo):
1231+
@click.option(
1232+
"--gist",
1233+
is_flag=True,
1234+
help="Upload to GitHub Gist and output a gistpreview.github.io URL.",
1235+
)
1236+
def import_session(session_id, output, token, org_uuid, repo, gist):
11341237
"""Import a session from the Claude API and convert to HTML.
11351238
11361239
If SESSION_ID is not provided, displays an interactive picker to select a session.
@@ -1190,12 +1293,25 @@ def import_session(session_id, output, token, org_uuid, repo):
11901293
raise click.ClickException(f"Network error: {e}")
11911294

11921295
# Determine output directory
1193-
if output is None:
1296+
if gist and output is None:
1297+
output = Path(tempfile.gettempdir()) / session_id
1298+
elif output is None:
11941299
output = session_id
11951300

11961301
click.echo(f"Generating HTML in {output}/...")
11971302
generate_html_from_session_data(session_data, output, github_repo=repo)
1198-
click.echo(f"Done! Open {output}/index.html to view.")
1303+
1304+
if gist:
1305+
# Inject gist preview JS and create gist
1306+
inject_gist_preview_js(output)
1307+
click.echo("Creating GitHub gist...")
1308+
gist_id, gist_url = create_gist(output)
1309+
preview_url = f"https://gistpreview.github.io/?{gist_id}/index.html"
1310+
click.echo(f"Gist: {gist_url}")
1311+
click.echo(f"Preview: {preview_url}")
1312+
click.echo(f"Files: {output}")
1313+
else:
1314+
click.echo(f"Done! Open {output}/index.html to view.")
11991315

12001316

12011317
def main():

0 commit comments

Comments
 (0)