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