session-slides 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/session-slides.js +171 -0
- package/package.json +39 -0
- package/scripts/__init__.py +54 -0
- package/scripts/generate_slides.py +315 -0
- package/scripts/html_generator.py +1808 -0
- package/scripts/parser.py +703 -0
- package/scripts/titles.py +1057 -0
- package/scripts/truncation.py +606 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Brandon J.P. Lambert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# session-slides
|
|
2
|
+
|
|
3
|
+
Convert Claude Code session transcripts into navigable HTML slide presentations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g session-slides
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8 or later.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Generate slides from the current project's most recent session
|
|
17
|
+
session-slides
|
|
18
|
+
|
|
19
|
+
# Open in browser immediately
|
|
20
|
+
session-slides --open
|
|
21
|
+
|
|
22
|
+
# Generate from a specific session file
|
|
23
|
+
session-slides --from ~/.claude/projects/.../session.jsonl
|
|
24
|
+
|
|
25
|
+
# Custom title and output path
|
|
26
|
+
session-slides --title "Building the Auth System" --output slides.html
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Output defaults to `./session-slides/{timestamp}.html` in your current directory. Each run is preserved.
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Option | Description |
|
|
34
|
+
|--------|-------------|
|
|
35
|
+
| `--from PATH` | Path to session JSONL file (auto-detects if omitted) |
|
|
36
|
+
| `--output PATH` | Output HTML file path (default: `./session-slides/{timestamp}.html`) |
|
|
37
|
+
| `--title TEXT` | Custom presentation title |
|
|
38
|
+
| `--open` | Open in browser after generation |
|
|
39
|
+
| `--ai-titles` | Use Ollama for slide titles (requires local Ollama) |
|
|
40
|
+
| `--clean` | Remove previous timestamped output files |
|
|
41
|
+
| `--verbose` | Enable verbose output |
|
|
42
|
+
|
|
43
|
+
## Output
|
|
44
|
+
|
|
45
|
+
Generates a self-contained HTML file with:
|
|
46
|
+
|
|
47
|
+
- Title slide with session metadata
|
|
48
|
+
- One slide per conversation turn
|
|
49
|
+
- Intelligent titles extracted from your prompts
|
|
50
|
+
- Tool usage indicators (Read, Write, Bash, etc.)
|
|
51
|
+
- Code blocks with syntax highlighting
|
|
52
|
+
- Summary slide with statistics
|
|
53
|
+
|
|
54
|
+
The HTML has no external dependencies and works offline.
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
1. Parses Claude Code's JSONL session format
|
|
59
|
+
2. Extracts conversation turns and tool usage
|
|
60
|
+
3. Generates titles using pattern matching (390+ action verbs)
|
|
61
|
+
4. Builds a responsive HTML presentation with keyboard navigation
|
|
62
|
+
|
|
63
|
+
## Session Files
|
|
64
|
+
|
|
65
|
+
Claude Code stores sessions in `~/.claude/projects/`. Each project directory contains JSONL files with your conversation history. The tool automatically finds the most recent session for your current working directory.
|
|
66
|
+
|
|
67
|
+
## Requirements
|
|
68
|
+
|
|
69
|
+
- Node.js 14+
|
|
70
|
+
- Python 3.8+
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* session-slides CLI
|
|
5
|
+
*
|
|
6
|
+
* Node.js wrapper that invokes the Python session-slides tool.
|
|
7
|
+
* Handles cross-platform Python detection and argument passthrough.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const spawn = require('cross-spawn');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
|
|
16
|
+
const SCRIPT_NAME = 'generate_slides.py';
|
|
17
|
+
const MIN_PYTHON_VERSION = [3, 8];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse Python version string into [major, minor] array.
|
|
21
|
+
* @param {string} versionOutput - Output from `python --version`
|
|
22
|
+
* @returns {number[]|null} - [major, minor] or null if parsing fails
|
|
23
|
+
*/
|
|
24
|
+
function parsePythonVersion(versionOutput) {
|
|
25
|
+
const match = versionOutput.match(/Python\s+(\d+)\.(\d+)/i);
|
|
26
|
+
if (match) {
|
|
27
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10)];
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a Python version meets minimum requirements.
|
|
34
|
+
* @param {number[]} version - [major, minor]
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function meetsMinVersion(version) {
|
|
38
|
+
if (!version) return false;
|
|
39
|
+
if (version[0] > MIN_PYTHON_VERSION[0]) return true;
|
|
40
|
+
if (version[0] < MIN_PYTHON_VERSION[0]) return false;
|
|
41
|
+
return version[1] >= MIN_PYTHON_VERSION[1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attempt to detect a working Python 3 executable.
|
|
46
|
+
* @returns {{cmd: string, args: string[]}|null} - Command and args, or null if not found
|
|
47
|
+
*/
|
|
48
|
+
function detectPython() {
|
|
49
|
+
// Allow environment variable override
|
|
50
|
+
const envPython = process.env.SESSION_SLIDES_PYTHON || process.env.PYTHON3;
|
|
51
|
+
if (envPython) {
|
|
52
|
+
return { cmd: envPython, args: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Platform-specific candidate order
|
|
56
|
+
const isWindows = process.platform === 'win32';
|
|
57
|
+
const candidates = isWindows
|
|
58
|
+
? [
|
|
59
|
+
{ cmd: 'python', args: [] },
|
|
60
|
+
{ cmd: 'python3', args: [] },
|
|
61
|
+
{ cmd: 'py', args: ['-3'] }
|
|
62
|
+
]
|
|
63
|
+
: [
|
|
64
|
+
{ cmd: 'python3', args: [] },
|
|
65
|
+
{ cmd: 'python', args: [] }
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
try {
|
|
70
|
+
const result = spawn.sync(candidate.cmd, [...candidate.args, '--version'], {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
stdio: 'pipe',
|
|
73
|
+
timeout: 5000
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.status === 0) {
|
|
77
|
+
const output = (result.stdout || result.stderr || '').toString();
|
|
78
|
+
const version = parsePythonVersion(output);
|
|
79
|
+
|
|
80
|
+
if (meetsMinVersion(version)) {
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Command not found, continue to next candidate
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Print error message and exit.
|
|
94
|
+
* @param {string} message
|
|
95
|
+
* @param {number} code
|
|
96
|
+
*/
|
|
97
|
+
function exitWithError(message, code = 1) {
|
|
98
|
+
console.error(`\x1b[31mError:\x1b[0m ${message}`);
|
|
99
|
+
process.exit(code);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Print help for Python installation.
|
|
104
|
+
*/
|
|
105
|
+
function printPythonHelp() {
|
|
106
|
+
console.error(`
|
|
107
|
+
\x1b[33mPython ${MIN_PYTHON_VERSION.join('.')}+ is required but was not found.\x1b[0m
|
|
108
|
+
|
|
109
|
+
Install Python:
|
|
110
|
+
macOS: brew install python3
|
|
111
|
+
Ubuntu: sudo apt install python3
|
|
112
|
+
Windows: https://www.python.org/downloads/
|
|
113
|
+
|
|
114
|
+
Or set SESSION_SLIDES_PYTHON environment variable to your Python path.
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Main entry point.
|
|
120
|
+
*/
|
|
121
|
+
function main() {
|
|
122
|
+
// Detect Python
|
|
123
|
+
const python = detectPython();
|
|
124
|
+
|
|
125
|
+
if (!python) {
|
|
126
|
+
printPythonHelp();
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Locate Python script
|
|
131
|
+
const scriptPath = path.join(__dirname, '..', 'scripts', SCRIPT_NAME);
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(scriptPath)) {
|
|
134
|
+
exitWithError(`Python script not found: ${scriptPath}\n\nThis may indicate a corrupt installation. Try reinstalling:\n npm uninstall -g session-slides && npm install -g session-slides`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build arguments: python args + script path + user args
|
|
138
|
+
const args = [...python.args, scriptPath, ...process.argv.slice(2)];
|
|
139
|
+
|
|
140
|
+
// Spawn Python process with inherited stdio for proper terminal handling
|
|
141
|
+
const child = spawn(python.cmd, args, {
|
|
142
|
+
stdio: 'inherit',
|
|
143
|
+
windowsHide: false
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Handle spawn errors
|
|
147
|
+
child.on('error', (err) => {
|
|
148
|
+
if (err.code === 'ENOENT') {
|
|
149
|
+
exitWithError(`Could not execute Python: ${python.cmd}`);
|
|
150
|
+
} else {
|
|
151
|
+
exitWithError(`Failed to start Python: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Propagate exit code
|
|
156
|
+
child.on('close', (code) => {
|
|
157
|
+
process.exit(code || 0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Forward termination signals to child process
|
|
161
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
162
|
+
for (const signal of signals) {
|
|
163
|
+
process.on(signal, () => {
|
|
164
|
+
if (!child.killed) {
|
|
165
|
+
child.kill(signal);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "session-slides",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Convert Claude Code session transcripts into navigable HTML slide presentations",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"slides",
|
|
9
|
+
"presentation",
|
|
10
|
+
"session",
|
|
11
|
+
"jsonl",
|
|
12
|
+
"html",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/bjpl/session-slides",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/bjpl/session-slides.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/bjpl/session-slides/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Brandon J.P. Lambert",
|
|
25
|
+
"bin": {
|
|
26
|
+
"session-slides": "./bin/session-slides.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.0.0"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux",
|
|
34
|
+
"win32"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"cross-spawn": "^7.0.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Slides Scripts Package
|
|
3
|
+
|
|
4
|
+
This package contains the core modules for parsing Claude Code sessions
|
|
5
|
+
and generating presentation slides.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .parser import (
|
|
9
|
+
# Data classes
|
|
10
|
+
ToolUse,
|
|
11
|
+
ContentBlock,
|
|
12
|
+
Turn,
|
|
13
|
+
Session,
|
|
14
|
+
# Core parsing functions
|
|
15
|
+
parse_jsonl,
|
|
16
|
+
extract_turns,
|
|
17
|
+
load_session,
|
|
18
|
+
# Session finding
|
|
19
|
+
find_current_session,
|
|
20
|
+
find_all_sessions,
|
|
21
|
+
# Path utilities
|
|
22
|
+
encode_path,
|
|
23
|
+
decode_path,
|
|
24
|
+
get_project_path_from_session,
|
|
25
|
+
# Summary
|
|
26
|
+
get_session_summary,
|
|
27
|
+
# Constants
|
|
28
|
+
CLAUDE_PROJECTS_DIR,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Data classes
|
|
33
|
+
"ToolUse",
|
|
34
|
+
"ContentBlock",
|
|
35
|
+
"Turn",
|
|
36
|
+
"Session",
|
|
37
|
+
# Core parsing functions
|
|
38
|
+
"parse_jsonl",
|
|
39
|
+
"extract_turns",
|
|
40
|
+
"load_session",
|
|
41
|
+
# Session finding
|
|
42
|
+
"find_current_session",
|
|
43
|
+
"find_all_sessions",
|
|
44
|
+
# Path utilities
|
|
45
|
+
"encode_path",
|
|
46
|
+
"decode_path",
|
|
47
|
+
"get_project_path_from_session",
|
|
48
|
+
# Summary
|
|
49
|
+
"get_session_summary",
|
|
50
|
+
# Constants
|
|
51
|
+
"CLAUDE_PROJECTS_DIR",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session Slides Generator CLI
|
|
4
|
+
|
|
5
|
+
Converts Claude Code session JSONL files into navigable HTML slide presentations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python generate_slides.py [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--from PATH Session JSONL file (auto-detects if omitted)
|
|
12
|
+
--output PATH Output HTML file (default: ./session-slides/{timestamp}.html)
|
|
13
|
+
--title TEXT Custom presentation title
|
|
14
|
+
--open Open in browser after generation
|
|
15
|
+
--ai-titles Use Ollama for title generation (requires ollama)
|
|
16
|
+
--clean Remove previous output files before generating
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
import webbrowser
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
# Import from sibling modules
|
|
27
|
+
from parser import Session, Turn, extract_turns, find_current_session, load_session
|
|
28
|
+
from titles import generate_turn_title, generate_continued_title, generate_title_ollama
|
|
29
|
+
from truncation import (
|
|
30
|
+
TruncationConfig,
|
|
31
|
+
truncate_user_prompt,
|
|
32
|
+
truncate_prose,
|
|
33
|
+
truncate_code_block,
|
|
34
|
+
format_tool_use,
|
|
35
|
+
)
|
|
36
|
+
from html_generator import generate_html
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_progress(message: str, end: str = "\n") -> None:
|
|
40
|
+
"""Print progress message to terminal."""
|
|
41
|
+
print(f"[*] {message}", end=end, flush=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_success(message: str) -> None:
|
|
45
|
+
"""Print success message to terminal."""
|
|
46
|
+
print(f"[✓] {message}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_error(message: str) -> None:
|
|
50
|
+
"""Print error message to terminal."""
|
|
51
|
+
print(f"[✗] {message}", file=sys.stderr)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_titles_for_session(session: Session, use_ai: bool = False) -> dict[int, str]:
|
|
55
|
+
"""Generate titles for all turns in a session."""
|
|
56
|
+
titles = {}
|
|
57
|
+
|
|
58
|
+
for turn in session.turns:
|
|
59
|
+
if turn.is_user_message():
|
|
60
|
+
prompt = turn.get_text_content()
|
|
61
|
+
if use_ai:
|
|
62
|
+
# Try AI first, fall back to heuristic
|
|
63
|
+
ai_title = generate_title_ollama(prompt)
|
|
64
|
+
if ai_title:
|
|
65
|
+
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = ai_title
|
|
66
|
+
else:
|
|
67
|
+
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
|
|
68
|
+
else:
|
|
69
|
+
titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
|
|
70
|
+
|
|
71
|
+
return titles
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def session_to_dict(session: Session) -> dict:
|
|
75
|
+
"""
|
|
76
|
+
Convert Session object to dict format expected by html_generator.
|
|
77
|
+
|
|
78
|
+
Uses get_conversation_pairs() to combine user prompts with assistant responses
|
|
79
|
+
into the format expected by generate_turn_slide():
|
|
80
|
+
{number, prompt, response, tools_used, files_modified, title, timestamp}
|
|
81
|
+
|
|
82
|
+
Applies truncation to keep slide content concise.
|
|
83
|
+
"""
|
|
84
|
+
turns_data = []
|
|
85
|
+
config = TruncationConfig()
|
|
86
|
+
|
|
87
|
+
# Use get_conversation_pairs() to pair user turns with assistant responses
|
|
88
|
+
conversation_pairs = session.get_conversation_pairs()
|
|
89
|
+
|
|
90
|
+
for turn_num, (user_turn, assistant_responses) in enumerate(conversation_pairs, 1):
|
|
91
|
+
# Get and truncate user prompt
|
|
92
|
+
raw_prompt = user_turn.get_text_content()
|
|
93
|
+
prompt = truncate_user_prompt(raw_prompt, config)
|
|
94
|
+
|
|
95
|
+
# Combine all assistant responses
|
|
96
|
+
response_parts = []
|
|
97
|
+
tools_used = []
|
|
98
|
+
files_modified = []
|
|
99
|
+
|
|
100
|
+
for assistant_turn in assistant_responses:
|
|
101
|
+
# Get text content
|
|
102
|
+
raw_response = assistant_turn.get_text_content()
|
|
103
|
+
if raw_response:
|
|
104
|
+
# Truncate prose content
|
|
105
|
+
truncated_response = truncate_prose(raw_response, config)
|
|
106
|
+
response_parts.append(truncated_response)
|
|
107
|
+
|
|
108
|
+
# Collect tool uses
|
|
109
|
+
for tool_use in assistant_turn.get_tool_uses():
|
|
110
|
+
# Format tool use as a concise description
|
|
111
|
+
tool_desc = format_tool_use(tool_use.name, tool_use.input)
|
|
112
|
+
tools_used.append(tool_desc)
|
|
113
|
+
|
|
114
|
+
# Track file modifications from tool uses
|
|
115
|
+
if tool_use.name in ('Write', 'Edit', 'NotebookEdit'):
|
|
116
|
+
file_path = tool_use.input.get('file_path', '')
|
|
117
|
+
if file_path:
|
|
118
|
+
action = 'created' if tool_use.name == 'Write' else 'modified'
|
|
119
|
+
files_modified.append({
|
|
120
|
+
'path': file_path,
|
|
121
|
+
'action': action,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# Combine response parts
|
|
125
|
+
response = '\n\n'.join(response_parts) if response_parts else ''
|
|
126
|
+
|
|
127
|
+
# Generate title for this turn
|
|
128
|
+
title = generate_turn_title(raw_prompt, turn_num)
|
|
129
|
+
|
|
130
|
+
turns_data.append({
|
|
131
|
+
'number': turn_num,
|
|
132
|
+
'prompt': prompt,
|
|
133
|
+
'response': response,
|
|
134
|
+
'tools_used': tools_used,
|
|
135
|
+
'files_modified': files_modified,
|
|
136
|
+
'title': title,
|
|
137
|
+
'timestamp': user_turn.timestamp.isoformat() if user_turn.timestamp else None,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
'session_id': session.session_id,
|
|
142
|
+
'project_path': session.project_path,
|
|
143
|
+
'turns': turns_data,
|
|
144
|
+
'metadata': {
|
|
145
|
+
'timestamp': session.start_time.isoformat() if session.start_time else None,
|
|
146
|
+
},
|
|
147
|
+
'created_at': session.turns[0].timestamp.isoformat() if session.turns and session.turns[0].timestamp else None,
|
|
148
|
+
'total_turns': len(turns_data),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main() -> int:
|
|
153
|
+
"""Main entry point for the CLI."""
|
|
154
|
+
parser = argparse.ArgumentParser(
|
|
155
|
+
prog="generate_slides",
|
|
156
|
+
description="Convert Claude Code session files into HTML slide presentations",
|
|
157
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
158
|
+
epilog="""
|
|
159
|
+
Examples:
|
|
160
|
+
python generate_slides.py --from session.jsonl
|
|
161
|
+
python generate_slides.py --from session.jsonl --output slides.html --title "My Session"
|
|
162
|
+
python generate_slides.py --from session.jsonl --ai-titles --open
|
|
163
|
+
python generate_slides.py # Auto-detect latest session file
|
|
164
|
+
""",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--from",
|
|
169
|
+
dest="input_file",
|
|
170
|
+
type=str,
|
|
171
|
+
metavar="PATH",
|
|
172
|
+
help="Path to session JSONL file (auto-detects if not specified)",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--output",
|
|
177
|
+
"-o",
|
|
178
|
+
type=str,
|
|
179
|
+
metavar="PATH",
|
|
180
|
+
help="Output HTML file path (default: ./session-slides/{timestamp}.html)",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--title",
|
|
185
|
+
"-t",
|
|
186
|
+
type=str,
|
|
187
|
+
metavar="TEXT",
|
|
188
|
+
help="Custom title for the presentation",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--open",
|
|
193
|
+
action="store_true",
|
|
194
|
+
help="Open the generated HTML in default browser",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"--ai-titles",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="Use Ollama AI to generate slide titles (requires local Ollama)",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
"--verbose",
|
|
205
|
+
"-v",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Enable verbose output",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--clean",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="Remove previous output files before generating",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
args = parser.parse_args()
|
|
217
|
+
|
|
218
|
+
# Step 1: Find or load session file
|
|
219
|
+
if args.input_file:
|
|
220
|
+
input_path = Path(args.input_file)
|
|
221
|
+
if not input_path.exists():
|
|
222
|
+
print_error(f"Session file not found: {input_path}")
|
|
223
|
+
return 1
|
|
224
|
+
print_progress(f"Loading session: {input_path}")
|
|
225
|
+
else:
|
|
226
|
+
print_progress("Searching for latest session...")
|
|
227
|
+
input_path = find_current_session()
|
|
228
|
+
if input_path is None:
|
|
229
|
+
print_error("No session file found for current directory.")
|
|
230
|
+
print_error("Use --from to specify a session file path.")
|
|
231
|
+
return 1
|
|
232
|
+
print_success(f"Found session: {input_path}")
|
|
233
|
+
|
|
234
|
+
# Step 2: Parse session file
|
|
235
|
+
try:
|
|
236
|
+
session = extract_turns(input_path)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print_error(f"Failed to parse session: {e}")
|
|
239
|
+
if args.verbose:
|
|
240
|
+
import traceback
|
|
241
|
+
traceback.print_exc()
|
|
242
|
+
return 1
|
|
243
|
+
|
|
244
|
+
if not session.turns:
|
|
245
|
+
print_error("Session contains no conversation turns")
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
user_turns = len([t for t in session.turns if t.is_user_message()])
|
|
249
|
+
print_success(f"Parsed {len(session.turns)} messages ({user_turns} user turns)")
|
|
250
|
+
|
|
251
|
+
# Step 3: Convert to dict format and generate titles
|
|
252
|
+
if args.ai_titles:
|
|
253
|
+
print_progress("Generating AI titles with Ollama...")
|
|
254
|
+
else:
|
|
255
|
+
print_progress("Generating heuristic titles...")
|
|
256
|
+
|
|
257
|
+
session_dict = session_to_dict(session)
|
|
258
|
+
print_success(f"Generated {session_dict['total_turns']} slide titles")
|
|
259
|
+
|
|
260
|
+
# Step 4: Generate HTML
|
|
261
|
+
presentation_title = args.title or f"Session: {session.session_id[:8] if session.session_id else 'Claude Code'}"
|
|
262
|
+
print_progress("Building HTML presentation...")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
html_content = generate_html(session_dict, title=presentation_title)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print_error(f"Failed to generate HTML: {e}")
|
|
268
|
+
if args.verbose:
|
|
269
|
+
import traceback
|
|
270
|
+
traceback.print_exc()
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
# Step 5: Write output
|
|
274
|
+
if args.output:
|
|
275
|
+
output_path = Path(args.output)
|
|
276
|
+
else:
|
|
277
|
+
# Default: write to ./session-slides/ in the current working directory
|
|
278
|
+
# Each run gets a timestamped filename to preserve history
|
|
279
|
+
output_dir = Path.cwd() / "session-slides"
|
|
280
|
+
output_dir.mkdir(exist_ok=True)
|
|
281
|
+
|
|
282
|
+
# Clean previous output files if requested
|
|
283
|
+
if args.clean:
|
|
284
|
+
old_files = list(output_dir.glob("*.html"))
|
|
285
|
+
if old_files:
|
|
286
|
+
for old_file in old_files:
|
|
287
|
+
old_file.unlink()
|
|
288
|
+
print_progress(f"Cleaned {len(old_files)} previous output file(s)")
|
|
289
|
+
|
|
290
|
+
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
|
|
291
|
+
output_path = output_dir / f"{timestamp}.html"
|
|
292
|
+
|
|
293
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
output_path.write_text(html_content, encoding="utf-8")
|
|
297
|
+
except IOError as e:
|
|
298
|
+
print_error(f"Failed to write output: {e}")
|
|
299
|
+
return 1
|
|
300
|
+
|
|
301
|
+
print_success(f"Generated: {output_path.absolute()}")
|
|
302
|
+
|
|
303
|
+
# Step 6: Optionally open in browser
|
|
304
|
+
if args.open:
|
|
305
|
+
print_progress("Opening in browser...")
|
|
306
|
+
try:
|
|
307
|
+
webbrowser.open(f"file://{output_path.absolute()}")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print_error(f"Could not open browser: {e}")
|
|
310
|
+
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
sys.exit(main())
|