ccexport 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -1
- data/CLAUDE.md +41 -1
- data/README.md +76 -4
- data/TODO.md +5 -0
- data/bin/ccexport +2 -92
- data/exe/ccexport +1 -152
- data/lib/ccexport/cli.rb +158 -0
- data/lib/ccexport/version.rb +2 -2
- data/lib/ccexport.rb +1 -0
- data/lib/claude_conversation_exporter.rb +115 -59
- metadata +3 -2
- data/ccexport.gemspec +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d1615cbaeacf17cc0f054e7d997e0e1a53b739511f2086c410340cfb2816d2d
|
|
4
|
+
data.tar.gz: e4b856098953850b6857395c3ca9a5054c91033161f1d1bb74bf8c7bf85fe0f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bf47b9c630fcb1f06efcfd4cd4ab3a23f6881812fdbe733a1272193af23f47f231ca0751b60586766cdd99a8e3649630639fd667ebc2cf81252b4719dffc85a
|
|
7
|
+
data.tar.gz: d9694653205b5b27818f0721dd43df868375d3c4a878beed201d234427600df1e6dda751626e4f07d84f1660a1f58ccc5a5ec49e245a5c76f21355667d2c14ef
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-05-06
|
|
11
|
+
|
|
12
|
+
- Add `--session ID` option to process a specific session. Thanks to @kopyl
|
|
13
|
+
- Add `--clean` option to remove thinking and tool use from output. Thanks to @kopyl
|
|
14
|
+
- Add `--stdout` option to write markdown to stdout instead of a file (implies `--silent`), enabling piping to other tools such as `gh gist create`. Inspired by @kopyl
|
|
15
|
+
|
|
10
16
|
## [0.1.0] - 2024-08-17
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -20,4 +26,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
26
|
- Command-line interface with extensive options
|
|
21
27
|
- Support for both single session and multi-session exports
|
|
22
28
|
- Path relativization for project files
|
|
23
|
-
- Comprehensive test suite with RSpec
|
|
29
|
+
- Comprehensive test suite with RSpec
|
data/CLAUDE.md
CHANGED
|
@@ -153,4 +153,44 @@ brew install cmark-gfm
|
|
|
153
153
|
- 96 RSpec tests covering all major functionality
|
|
154
154
|
- Test fixtures in `spec/fixtures/` with real JSONL data samples
|
|
155
155
|
- Mocking of external dependencies (TruffleHog, cmark-gfm, system commands)
|
|
156
|
-
- Comprehensive secret detection testing with realistic secret formats
|
|
156
|
+
- Comprehensive secret detection testing with realistic secret formats
|
|
157
|
+
|
|
158
|
+
## Gem Distribution
|
|
159
|
+
|
|
160
|
+
### RubyGem Structure
|
|
161
|
+
- **Gem Name**: `ccexport`
|
|
162
|
+
- **Version**: Managed in `lib/ccexport/version.rb`
|
|
163
|
+
- **Executable**: `exe/ccexport` (installed globally as `ccexport` command)
|
|
164
|
+
- **Entry Point**: `lib/ccexport.rb` (requires all necessary components)
|
|
165
|
+
|
|
166
|
+
### Dependency Management
|
|
167
|
+
**Automatic Installation**: The `exe/ccexport` executable includes automatic dependency checking and installation:
|
|
168
|
+
|
|
169
|
+
1. **Dependency Detection**: Checks for `trufflehog` and `cmark-gfm` using `which` command
|
|
170
|
+
2. **Auto-Install**: If Homebrew is available, automatically runs `brew install` for missing dependencies
|
|
171
|
+
3. **Graceful Fallback**: Provides installation instructions and exits if dependencies can't be auto-installed
|
|
172
|
+
4. **Skip Option**: `--skip-dependency-check` flag for advanced users or CI environments
|
|
173
|
+
5. **Silent Mode**: Respects `--silent` flag for dependency operations
|
|
174
|
+
|
|
175
|
+
### Build and Release Process
|
|
176
|
+
```bash
|
|
177
|
+
# Build gem locally
|
|
178
|
+
gem build ccexport.gemspec
|
|
179
|
+
|
|
180
|
+
# Install locally for testing
|
|
181
|
+
gem install ./ccexport-0.1.0.gem
|
|
182
|
+
|
|
183
|
+
# Test functionality
|
|
184
|
+
ccexport --help
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Files Excluded from Gem**:
|
|
188
|
+
- Test files (`spec/`)
|
|
189
|
+
- Development scripts (`debug_compacted.rb`, `generate_vibe_samples`)
|
|
190
|
+
- Example outputs (`VIBE*`, `claude-conversations/`)
|
|
191
|
+
- Build artifacts (`*.gem`)
|
|
192
|
+
|
|
193
|
+
### Installation Paths
|
|
194
|
+
- **Quick Install**: `gem install ccexport` (dependencies auto-installed with Homebrew)
|
|
195
|
+
- **Development**: Clone repo, `bundle install`, manual dependency installation
|
|
196
|
+
- **Manual Setup**: Detailed Ruby + Homebrew installation instructions for non-developers
|
data/README.md
CHANGED
|
@@ -45,6 +45,8 @@ I created this tool because, as I'm exploring using agentic coding tools, I've f
|
|
|
45
45
|
gem install ccexport
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
**Note**: If you have Homebrew installed, ccexport will automatically install missing dependencies (TruffleHog and cmark-gfm) when you first run it. If you don't have Homebrew, see the full setup instructions below.
|
|
49
|
+
|
|
48
50
|
### Full Setup (For non-Ruby developers)
|
|
49
51
|
|
|
50
52
|
If you don't have Ruby installed or aren't familiar with Ruby development, follow these steps:
|
|
@@ -173,18 +175,72 @@ rbenv global 3.4.3
|
|
|
173
175
|
ruby --version # Should show Ruby 3.4.3
|
|
174
176
|
```
|
|
175
177
|
|
|
176
|
-
#### 4. Install ccexport
|
|
178
|
+
#### 4. Install ccexport
|
|
177
179
|
|
|
178
180
|
```bash
|
|
179
181
|
# Install the gem
|
|
180
182
|
gem install ccexport
|
|
181
183
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
+
# Run ccexport - it will automatically install dependencies via Homebrew if needed
|
|
185
|
+
ccexport --help
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**If the `ccexport` command is not found after installation:**
|
|
189
|
+
|
|
190
|
+
<details>
|
|
191
|
+
<summary>Refresh your Ruby version manager (click to expand)</summary>
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# rbenv users
|
|
195
|
+
rbenv rehash
|
|
196
|
+
|
|
197
|
+
# RVM users
|
|
198
|
+
rvm reload
|
|
199
|
+
|
|
200
|
+
# asdf users
|
|
201
|
+
asdf reshim ruby
|
|
202
|
+
|
|
203
|
+
# mise users
|
|
204
|
+
mise reshim
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Then try running `ccexport --help` again.
|
|
208
|
+
</details>
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
<summary>Add the gem bin directory to your PATH (plain Homebrew Ruby, no version manager)</summary>
|
|
212
|
+
|
|
213
|
+
If you installed Ruby directly via `brew install ruby` and aren't using a version manager, the gem executables may not be on your PATH. Add this line to your shell profile:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
export PATH="$(gem environment gemdir)/bin:$PATH"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Which file to add it to depends on your shell:
|
|
220
|
+
|
|
221
|
+
- **zsh** (default on macOS Catalina and later):
|
|
222
|
+
```bash
|
|
223
|
+
echo 'export PATH="$(gem environment gemdir)/bin:$PATH"' >> ~/.zprofile
|
|
224
|
+
```
|
|
225
|
+
- **bash** (default on older macOS or if you've switched):
|
|
226
|
+
```bash
|
|
227
|
+
echo 'export PATH="$(gem environment gemdir)/bin:$PATH"' >> ~/.bash_profile
|
|
228
|
+
```
|
|
229
|
+
- **fish**: `~/.config/fish/config.fish`, using `fish_add_path (gem environment gemdir)/bin`
|
|
230
|
+
|
|
231
|
+
After running the command, restart your terminal or run `source ~/.zprofile` (adjust for your shell), then try `ccexport --help` again.
|
|
232
|
+
</details>
|
|
184
233
|
|
|
234
|
+
**That's it!** When you first run ccexport, it will automatically detect and install any missing dependencies (TruffleHog and cmark-gfm) if you have Homebrew installed.
|
|
235
|
+
|
|
236
|
+
**Manual dependency installation** (only needed if you don't have Homebrew):
|
|
237
|
+
```bash
|
|
185
238
|
# If you're using system package managers instead of Homebrew:
|
|
186
239
|
# - TruffleHog: https://github.com/trufflesecurity/trufflehog#installation
|
|
187
240
|
# - cmark-gfm: May require building from source on some Linux distributions
|
|
241
|
+
|
|
242
|
+
# To skip dependency checking entirely:
|
|
243
|
+
ccexport --skip-dependency-check
|
|
188
244
|
```
|
|
189
245
|
|
|
190
246
|
### From Source (Development)
|
|
@@ -196,8 +252,12 @@ brew install trufflehog cmark-gfm
|
|
|
196
252
|
### Prerequisites Summary
|
|
197
253
|
|
|
198
254
|
- **Ruby**: 3.0.0 or higher (3.4.3 recommended)
|
|
255
|
+
- **Homebrew**: Recommended for automatic dependency installation ([installation guide](https://brew.sh))
|
|
256
|
+
- **TruffleHog** and **cmark-gfm**: Automatically installed via Homebrew when you first run ccexport
|
|
257
|
+
|
|
258
|
+
**Manual installation only needed if you don't have Homebrew:**
|
|
199
259
|
- **TruffleHog**: For secret detection ([installation guide](https://github.com/trufflesecurity/trufflehog#installation))
|
|
200
|
-
- **cmark-gfm**: For HTML preview generation
|
|
260
|
+
- **cmark-gfm**: For HTML preview generation
|
|
201
261
|
|
|
202
262
|
## Usage
|
|
203
263
|
|
|
@@ -260,6 +320,15 @@ ccexport --preview --template github
|
|
|
260
320
|
# Process specific JSONL file
|
|
261
321
|
ccexport --jsonl /path/to/conversation.jsonl --out specific-conversation.md
|
|
262
322
|
|
|
323
|
+
# Process specific session
|
|
324
|
+
ccexport --session aee5189e-cdc7-42fd-9aa1-96ae42c12826
|
|
325
|
+
|
|
326
|
+
# Write markdown to stdout (implies --silent)
|
|
327
|
+
ccexport --today --stdout
|
|
328
|
+
|
|
329
|
+
# Create a GitHub Gist from today's conversations
|
|
330
|
+
ccexport --today --stdout | gh gist create -f claude.md
|
|
331
|
+
|
|
263
332
|
# Silent mode (suppress all output)
|
|
264
333
|
ccexport --silent
|
|
265
334
|
|
|
@@ -273,12 +342,15 @@ ccexport --in /path/to/project --from 2024-01-15 --out ./my-exports --timestamps
|
|
|
273
342
|
- `--from DATE`: Filter messages from this date (YYYY-MM-DD or timestamp format from --timestamps output)
|
|
274
343
|
- `--to DATE`: Filter messages to this date (YYYY-MM-DD or timestamp format from --timestamps output)
|
|
275
344
|
- `--today`: Filter messages from today only (in your local timezone)
|
|
345
|
+
- `--session ID`: Export a specific session
|
|
276
346
|
- `--out PATH`: Custom output directory or specific file path (supports relative paths, use .md extension for specific file)
|
|
277
347
|
- `--timestamps`: Show precise timestamps with each message for easy reference
|
|
348
|
+
- `--clean`: Suppress the output of thinking and tool use
|
|
278
349
|
- `--preview`: Generate HTML preview and open in browser automatically
|
|
279
350
|
- `--no-open`: Generate HTML preview without opening in browser (requires --preview)
|
|
280
351
|
- `--template NAME_OR_PATH`: HTML template name (from templates dir) or file path (default: default)
|
|
281
352
|
- `--jsonl FILE`: Process a specific JSONL file instead of scanning directories
|
|
353
|
+
- `--stdout`: Write markdown to stdout instead of a file (implies `--silent`); useful for piping to other tools
|
|
282
354
|
- `-s`, `--silent`: Silent mode - suppress all output except errors
|
|
283
355
|
- `--help`: Show usage information
|
|
284
356
|
|
data/TODO.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Investigate JSONL entries without timestamps
|
|
4
|
+
|
|
5
|
+
Some JSONL entries have no `timestamp` key. Currently `message_in_date_range?` treats them as in-range (includes them). It may be more correct to inherit the timestamp of the preceding entry — investigate whether that's the pattern in practice and update accordingly.
|
data/bin/ccexport
CHANGED
|
@@ -1,95 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require_relative '../lib/
|
|
4
|
-
require 'optparse'
|
|
5
|
-
require 'time'
|
|
3
|
+
require_relative '../lib/ccexport'
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
OptionParser.new do |parser|
|
|
10
|
-
parser.banner = "Usage: ccexport [options]"
|
|
11
|
-
|
|
12
|
-
parser.on("--from DATE", "Filter messages from this date (YYYY-MM-DD)") do |date|
|
|
13
|
-
options[:from] = date
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
parser.on("--to DATE", "Filter messages to this date (YYYY-MM-DD)") do |date|
|
|
17
|
-
options[:to] = date
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
parser.on("--today", "Filter messages from today only") do
|
|
21
|
-
options[:today] = true
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
parser.on("--in PATH", "Project path to export conversations from (default: current directory)") do |path|
|
|
25
|
-
options[:in] = path
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
parser.on("--out PATH", "Output directory or file path (default: claude-conversations)") do |path|
|
|
29
|
-
options[:out] = path
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
parser.on("--timestamps", "Show timestamps with each message") do
|
|
33
|
-
options[:timestamps] = true
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
parser.on("--preview", "Generate HTML preview and open in browser") do
|
|
37
|
-
options[:preview] = true
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
parser.on("--no-open", "Generate HTML preview without opening in browser (requires --preview)") do
|
|
41
|
-
options[:no_open] = true
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
parser.on("--template NAME_OR_PATH", "HTML template name (from templates dir) or file path (default: default)") do |template|
|
|
45
|
-
options[:template] = template
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
parser.on("--jsonl FILE", "Process a specific JSONL file instead of scanning directories") do |file|
|
|
49
|
-
options[:jsonl] = file
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
parser.on("-s", "--silent", "Silent mode - suppress all output") do
|
|
53
|
-
options[:silent] = true
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
parser.on("-h", "--help", "Show this help message") do
|
|
57
|
-
puts parser
|
|
58
|
-
exit
|
|
59
|
-
end
|
|
60
|
-
end.parse!
|
|
61
|
-
|
|
62
|
-
begin
|
|
63
|
-
# Validate options
|
|
64
|
-
if options[:no_open] && !options[:preview]
|
|
65
|
-
puts "Error: --no-open requires --preview" unless options[:silent]
|
|
66
|
-
exit 1
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
if options[:jsonl] && !File.exist?(options[:jsonl])
|
|
70
|
-
puts "Error: JSONL file not found: #{options[:jsonl]}" unless options[:silent]
|
|
71
|
-
exit 1
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
if options[:jsonl] && !options[:jsonl].end_with?('.jsonl')
|
|
75
|
-
puts "Error: File must have .jsonl extension: #{options[:jsonl]}" unless options[:silent]
|
|
76
|
-
exit 1
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
project_path = options[:in] || Dir.pwd
|
|
80
|
-
output_dir = options[:out] || 'claude-conversations'
|
|
81
|
-
result = ClaudeConversationExporter.export(project_path, output_dir, options)
|
|
82
|
-
|
|
83
|
-
# Generate preview if requested
|
|
84
|
-
if options[:preview]
|
|
85
|
-
# Use the actual output file if it was specified, otherwise use the directory
|
|
86
|
-
preview_target = result[:output_file] || output_dir
|
|
87
|
-
template_name = options[:template] || 'default'
|
|
88
|
-
ClaudeConversationExporter.generate_preview(preview_target, !options[:no_open], result[:leaf_summaries] || [], template_name, options[:silent])
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
exit 0
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
puts "Error: #{e.message}" unless options[:silent]
|
|
94
|
-
exit 1
|
|
95
|
-
end
|
|
5
|
+
CcExport::CLI.run
|
data/exe/ccexport
CHANGED
|
@@ -1,156 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require 'ccexport'
|
|
4
|
-
require 'optparse'
|
|
5
|
-
require 'time'
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
def check_dependencies(silent = false)
|
|
9
|
-
missing_deps = []
|
|
10
|
-
|
|
11
|
-
# Check for TruffleHog
|
|
12
|
-
unless system('which trufflehog > /dev/null 2>&1')
|
|
13
|
-
missing_deps << 'trufflehog'
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Check for cmark-gfm
|
|
17
|
-
unless system('which cmark-gfm > /dev/null 2>&1')
|
|
18
|
-
missing_deps << 'cmark-gfm'
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
return if missing_deps.empty?
|
|
22
|
-
|
|
23
|
-
puts "Missing required dependencies: #{missing_deps.join(', ')}" unless silent
|
|
24
|
-
|
|
25
|
-
# Check if Homebrew is available
|
|
26
|
-
if system('which brew > /dev/null 2>&1')
|
|
27
|
-
puts "\nAttempting to install missing dependencies via Homebrew..." unless silent
|
|
28
|
-
|
|
29
|
-
success = true
|
|
30
|
-
missing_deps.each do |dep|
|
|
31
|
-
puts "Installing #{dep}..." unless silent
|
|
32
|
-
unless system(silent ? "brew install #{dep} > /dev/null 2>&1" : "brew install #{dep}")
|
|
33
|
-
puts "Failed to install #{dep}" unless silent
|
|
34
|
-
success = false
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
if success
|
|
39
|
-
puts "✅ All dependencies installed successfully!" unless silent
|
|
40
|
-
return
|
|
41
|
-
else
|
|
42
|
-
puts "❌ Some dependencies failed to install" unless silent
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
puts "\nHomebrew not found." unless silent
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
unless silent
|
|
49
|
-
puts "\nPlease install the missing dependencies manually:"
|
|
50
|
-
puts "- TruffleHog: https://github.com/trufflesecurity/trufflehog#installation"
|
|
51
|
-
puts "- cmark-gfm: Install via your system package manager or from source"
|
|
52
|
-
puts "\nFor detailed installation instructions, see:"
|
|
53
|
-
puts "https://github.com/marcheiligers/ccexport#installation"
|
|
54
|
-
puts "\nAlternatively, install Homebrew and run ccexport again for automatic installation:"
|
|
55
|
-
puts "https://brew.sh"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
exit 1
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
options = {}
|
|
62
|
-
|
|
63
|
-
OptionParser.new do |parser|
|
|
64
|
-
parser.banner = "Usage: ccexport [options]"
|
|
65
|
-
|
|
66
|
-
parser.on("--from DATE", "Filter messages from this date (YYYY-MM-DD)") do |date|
|
|
67
|
-
options[:from] = date
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
parser.on("--to DATE", "Filter messages to this date (YYYY-MM-DD)") do |date|
|
|
71
|
-
options[:to] = date
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
parser.on("--today", "Filter messages from today only") do
|
|
75
|
-
options[:today] = true
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
parser.on("--in PATH", "Project path to export conversations from (default: current directory)") do |path|
|
|
79
|
-
options[:in] = path
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
parser.on("--out PATH", "Output directory or file path (default: claude-conversations)") do |path|
|
|
83
|
-
options[:out] = path
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
parser.on("--timestamps", "Show timestamps with each message") do
|
|
87
|
-
options[:timestamps] = true
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
parser.on("--preview", "Generate HTML preview and open in browser") do
|
|
91
|
-
options[:preview] = true
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
parser.on("--no-open", "Generate HTML preview without opening in browser (requires --preview)") do
|
|
95
|
-
options[:no_open] = true
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
parser.on("--template NAME_OR_PATH", "HTML template name (from templates dir) or file path (default: default)") do |template|
|
|
99
|
-
options[:template] = template
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
parser.on("--jsonl FILE", "Process a specific JSONL file instead of scanning directories") do |file|
|
|
103
|
-
options[:jsonl] = file
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
parser.on("-s", "--silent", "Silent mode - suppress all output") do
|
|
107
|
-
options[:silent] = true
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
parser.on("--skip-dependency-check", "Skip checking for external dependencies") do
|
|
111
|
-
options[:skip_dependency_check] = true
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
parser.on("-h", "--help", "Show this help message") do
|
|
115
|
-
puts parser
|
|
116
|
-
exit
|
|
117
|
-
end
|
|
118
|
-
end.parse!
|
|
119
|
-
|
|
120
|
-
# Check dependencies before proceeding (unless skipped)
|
|
121
|
-
check_dependencies(options[:silent]) unless options[:skip_dependency_check]
|
|
122
|
-
|
|
123
|
-
begin
|
|
124
|
-
# Validate options
|
|
125
|
-
if options[:no_open] && !options[:preview]
|
|
126
|
-
puts "Error: --no-open requires --preview" unless options[:silent]
|
|
127
|
-
exit 1
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
if options[:jsonl] && !File.exist?(options[:jsonl])
|
|
131
|
-
puts "Error: JSONL file not found: #{options[:jsonl]}" unless options[:silent]
|
|
132
|
-
exit 1
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
if options[:jsonl] && !options[:jsonl].end_with?('.jsonl')
|
|
136
|
-
puts "Error: File must have .jsonl extension: #{options[:jsonl]}" unless options[:silent]
|
|
137
|
-
exit 1
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
project_path = options[:in] || Dir.pwd
|
|
141
|
-
output_dir = options[:out] || 'claude-conversations'
|
|
142
|
-
result = ClaudeConversationExporter.export(project_path, output_dir, options)
|
|
143
|
-
|
|
144
|
-
# Generate preview if requested
|
|
145
|
-
if options[:preview]
|
|
146
|
-
# Use the actual output file if it was specified, otherwise use the directory
|
|
147
|
-
preview_target = result[:output_file] || output_dir
|
|
148
|
-
template_name = options[:template] || 'default'
|
|
149
|
-
ClaudeConversationExporter.generate_preview(preview_target, !options[:no_open], result[:leaf_summaries] || [], template_name, options[:silent])
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
exit 0
|
|
153
|
-
rescue StandardError => e
|
|
154
|
-
puts "Error: #{e.message}" unless options[:silent]
|
|
155
|
-
exit 1
|
|
156
|
-
end
|
|
5
|
+
CcExport::CLI.run
|
data/lib/ccexport/cli.rb
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module CcExport
|
|
7
|
+
module CLI
|
|
8
|
+
def self.run(argv = ARGV)
|
|
9
|
+
options = {}
|
|
10
|
+
|
|
11
|
+
OptionParser.new do |parser|
|
|
12
|
+
parser.banner = "Usage: ccexport [options]"
|
|
13
|
+
|
|
14
|
+
parser.on("--from DATE", "Filter messages from this date (YYYY-MM-DD)") do |date|
|
|
15
|
+
options[:from] = date
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
parser.on("--to DATE", "Filter messages to this date (YYYY-MM-DD)") do |date|
|
|
19
|
+
options[:to] = date
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
parser.on("--today", "Filter messages from today only") do
|
|
23
|
+
options[:today] = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
parser.on("--in PATH", "Project path to export conversations from (default: current directory)") do |path|
|
|
27
|
+
options[:in] = path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
parser.on("--out PATH", "Output directory or file path (default: claude-conversations)") do |path|
|
|
31
|
+
options[:out] = path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
parser.on("--timestamps", "Show timestamps with each message") do
|
|
35
|
+
options[:timestamps] = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
parser.on("--preview", "Generate HTML preview and open in browser") do
|
|
39
|
+
options[:preview] = true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
parser.on("--no-open", "Generate HTML preview without opening in browser (requires --preview)") do
|
|
43
|
+
options[:no_open] = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
parser.on("--template NAME_OR_PATH", "HTML template name (from templates dir) or file path (default: default)") do |template|
|
|
47
|
+
options[:template] = template
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
parser.on("--jsonl FILE", "Process a specific JSONL file instead of scanning directories") do |file|
|
|
51
|
+
options[:jsonl] = file
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
parser.on("--session ID", "Export a specific session by its UUID") do |id|
|
|
55
|
+
options[:session] = id
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
parser.on("--clean", "Export only visible conversation (no tool calls, thinking, or metadata)") do
|
|
59
|
+
options[:clean] = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parser.on("--stdout", "Write markdown to stdout instead of a file (implies --silent)") do
|
|
63
|
+
options[:stdout] = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
parser.on("-s", "--silent", "Silent mode - suppress all output") do
|
|
67
|
+
options[:silent] = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parser.on("--skip-dependency-check", "Skip checking for external dependencies") do
|
|
71
|
+
options[:skip_dependency_check] = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
parser.on("-h", "--help", "Show this help message") do
|
|
75
|
+
puts parser
|
|
76
|
+
exit
|
|
77
|
+
end
|
|
78
|
+
end.parse!(argv)
|
|
79
|
+
|
|
80
|
+
check_dependencies(options[:silent]) unless options[:skip_dependency_check]
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
if options[:no_open] && !options[:preview]
|
|
84
|
+
puts "Error: --no-open requires --preview" unless options[:silent]
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if options[:jsonl] && !File.exist?(options[:jsonl])
|
|
89
|
+
puts "Error: JSONL file not found: #{options[:jsonl]}" unless options[:silent]
|
|
90
|
+
exit 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if options[:jsonl] && !options[:jsonl].end_with?('.jsonl')
|
|
94
|
+
puts "Error: File must have .jsonl extension: #{options[:jsonl]}" unless options[:silent]
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
project_path = options[:in] || Dir.pwd
|
|
99
|
+
output_dir = options[:out] || 'claude-conversations'
|
|
100
|
+
result = ClaudeConversationExporter.export(project_path, output_dir, options)
|
|
101
|
+
|
|
102
|
+
if options[:preview]
|
|
103
|
+
preview_target = result[:output_file] || output_dir
|
|
104
|
+
template_name = options[:template] || 'default'
|
|
105
|
+
ClaudeConversationExporter.generate_preview(preview_target, !options[:no_open], result[:leaf_summaries] || [], template_name, options[:silent])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
exit 0
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
puts "Error: #{e.message}" unless options[:silent]
|
|
111
|
+
exit 1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.check_dependencies(silent = false)
|
|
116
|
+
missing_deps = []
|
|
117
|
+
missing_deps << 'trufflehog' unless system('which trufflehog > /dev/null 2>&1')
|
|
118
|
+
missing_deps << 'cmark-gfm' unless system('which cmark-gfm > /dev/null 2>&1')
|
|
119
|
+
return if missing_deps.empty?
|
|
120
|
+
|
|
121
|
+
puts "Missing required dependencies: #{missing_deps.join(', ')}" unless silent
|
|
122
|
+
|
|
123
|
+
if system('which brew > /dev/null 2>&1')
|
|
124
|
+
puts "\nAttempting to install missing dependencies via Homebrew..." unless silent
|
|
125
|
+
|
|
126
|
+
success = true
|
|
127
|
+
missing_deps.each do |dep|
|
|
128
|
+
puts "Installing #{dep}..." unless silent
|
|
129
|
+
unless system(silent ? "brew install #{dep} > /dev/null 2>&1" : "brew install #{dep}")
|
|
130
|
+
puts "Failed to install #{dep}" unless silent
|
|
131
|
+
success = false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if success
|
|
136
|
+
puts "✅ All dependencies installed successfully!" unless silent
|
|
137
|
+
return
|
|
138
|
+
else
|
|
139
|
+
puts "❌ Some dependencies failed to install" unless silent
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
puts "\nHomebrew not found." unless silent
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
unless silent
|
|
146
|
+
puts "\nPlease install the missing dependencies manually:"
|
|
147
|
+
puts "- TruffleHog: https://github.com/trufflesecurity/trufflehog#installation"
|
|
148
|
+
puts "- cmark-gfm: Install via your system package manager or from source"
|
|
149
|
+
puts "\nFor detailed installation instructions, see:"
|
|
150
|
+
puts "https://github.com/marcheiligers/ccexport#installation"
|
|
151
|
+
puts "\nAlternatively, install Homebrew and run ccexport again for automatic installation:"
|
|
152
|
+
puts "https://brew.sh"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
exit 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/ccexport/version.rb
CHANGED
data/lib/ccexport.rb
CHANGED
|
@@ -167,7 +167,9 @@ class ClaudeConversationExporter
|
|
|
167
167
|
@compacted_conversation_processed = false
|
|
168
168
|
@options = options
|
|
169
169
|
@show_timestamps = options[:timestamps] || false
|
|
170
|
-
@
|
|
170
|
+
@clean = options[:clean] || false
|
|
171
|
+
@stdout = options[:stdout] || false
|
|
172
|
+
@silent = @stdout || options[:silent] || false
|
|
171
173
|
@leaf_summaries = []
|
|
172
174
|
@skipped_messages = []
|
|
173
175
|
@secrets_detected = []
|
|
@@ -176,7 +178,11 @@ class ClaudeConversationExporter
|
|
|
176
178
|
end
|
|
177
179
|
|
|
178
180
|
def export
|
|
179
|
-
if @options[:
|
|
181
|
+
if @options[:session]
|
|
182
|
+
session_file = find_session_file(@options[:session])
|
|
183
|
+
session_files = [session_file]
|
|
184
|
+
session_dir = File.dirname(session_file)
|
|
185
|
+
elsif @options[:jsonl]
|
|
180
186
|
# Process specific JSONL file
|
|
181
187
|
session_files = [File.expand_path(@options[:jsonl])]
|
|
182
188
|
session_dir = File.dirname(session_files.first)
|
|
@@ -226,13 +232,20 @@ class ClaudeConversationExporter
|
|
|
226
232
|
return { sessions_exported: 0, total_messages: 0 }
|
|
227
233
|
end
|
|
228
234
|
|
|
235
|
+
markdown = format_combined_markdown(sessions)
|
|
236
|
+
|
|
237
|
+
if @stdout
|
|
238
|
+
$stdout.print(markdown)
|
|
239
|
+
return { sessions_exported: sessions.length, total_messages: total_messages, leaf_summaries: @leaf_summaries, output_file: nil }
|
|
240
|
+
end
|
|
241
|
+
|
|
229
242
|
# Generate output path if not already specified
|
|
230
243
|
if output_path.nil?
|
|
231
244
|
filename = generate_combined_filename(sessions)
|
|
232
245
|
output_path = File.join(output_dir, filename)
|
|
233
246
|
end
|
|
234
247
|
|
|
235
|
-
File.write(output_path,
|
|
248
|
+
File.write(output_path, markdown)
|
|
236
249
|
|
|
237
250
|
# Write skip log if there were any skipped messages
|
|
238
251
|
write_skip_log(output_path)
|
|
@@ -378,6 +391,7 @@ class ClaudeConversationExporter
|
|
|
378
391
|
|
|
379
392
|
def message_in_date_range?(timestamp)
|
|
380
393
|
return true unless @from_time || @to_time
|
|
394
|
+
return true if timestamp.nil?
|
|
381
395
|
|
|
382
396
|
begin
|
|
383
397
|
message_time = Time.parse(timestamp)
|
|
@@ -428,6 +442,17 @@ class ClaudeConversationExporter
|
|
|
428
442
|
candidates.first
|
|
429
443
|
end
|
|
430
444
|
|
|
445
|
+
def find_session_file(session_id)
|
|
446
|
+
projects_dir = File.join(@claude_home, 'projects')
|
|
447
|
+
filename = "#{session_id}.jsonl"
|
|
448
|
+
|
|
449
|
+
Dir.glob(File.join(projects_dir, '*', filename)).each do |path|
|
|
450
|
+
return path
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
raise "No session found with ID: #{session_id}"
|
|
454
|
+
end
|
|
455
|
+
|
|
431
456
|
def encode_path(path)
|
|
432
457
|
path.gsub('/', '-').gsub('_', '-')
|
|
433
458
|
end
|
|
@@ -479,15 +504,26 @@ class ClaudeConversationExporter
|
|
|
479
504
|
@compacted_conversation_processed = true
|
|
480
505
|
end
|
|
481
506
|
elsif data.key?('toolUseResult')
|
|
507
|
+
if @clean
|
|
508
|
+
# In clean mode, skip tool results entirely
|
|
509
|
+
next
|
|
510
|
+
end
|
|
482
511
|
# Pair with previous tool_use
|
|
483
512
|
if pending_tool_use
|
|
484
|
-
|
|
513
|
+
message = format_combined_tool_use(pending_tool_use, data)
|
|
514
|
+
messages << message if message
|
|
485
515
|
pending_tool_use = nil
|
|
486
516
|
end
|
|
487
517
|
elsif data.key?('requestId') || regular_message?(data)
|
|
488
518
|
# Check if this assistant message contains tool_use
|
|
489
519
|
if tool_use_message?(data)
|
|
490
|
-
|
|
520
|
+
if @clean
|
|
521
|
+
# In clean mode, extract only text content from tool_use messages
|
|
522
|
+
message = format_regular_message(data)
|
|
523
|
+
messages << message if message
|
|
524
|
+
else
|
|
525
|
+
pending_tool_use = data # Hold for pairing with next toolUseResult
|
|
526
|
+
end
|
|
491
527
|
else
|
|
492
528
|
message = format_regular_message(data)
|
|
493
529
|
if message
|
|
@@ -577,6 +613,8 @@ class ClaudeConversationExporter
|
|
|
577
613
|
|
|
578
614
|
message_id = data.dig('message', 'id') || 'unknown'
|
|
579
615
|
|
|
616
|
+
return nil if content.nil?
|
|
617
|
+
|
|
580
618
|
if content.is_a?(Array)
|
|
581
619
|
result = extract_text_content(content, "message_#{message_id}")
|
|
582
620
|
processed_content = result[:content]
|
|
@@ -590,7 +628,7 @@ class ClaudeConversationExporter
|
|
|
590
628
|
processed_content = JSON.pretty_generate(content)
|
|
591
629
|
end
|
|
592
630
|
|
|
593
|
-
return nil if processed_content.strip.empty?
|
|
631
|
+
return nil if processed_content.nil? || processed_content.strip.empty?
|
|
594
632
|
|
|
595
633
|
# Fix nested backticks in regular content
|
|
596
634
|
processed_content = fix_nested_backticks_in_content(processed_content)
|
|
@@ -680,14 +718,17 @@ class ClaudeConversationExporter
|
|
|
680
718
|
if item.is_a?(Hash) && item['type'] == 'text' && item['text']
|
|
681
719
|
parts << item['text']
|
|
682
720
|
elsif item.is_a?(Hash) && item['type'] == 'thinking' && item['thinking']
|
|
721
|
+
next if @clean
|
|
683
722
|
# Format thinking content as blockquote
|
|
684
723
|
thinking_lines = item['thinking'].split("\n").map { |line| "> #{line}" }
|
|
685
724
|
parts << thinking_lines.join("\n")
|
|
686
725
|
has_thinking = true
|
|
687
726
|
elsif item.is_a?(Hash) && item['type'] == 'tool_use'
|
|
727
|
+
next if @clean
|
|
688
728
|
# Format tool_use without tool_result (will be paired later at message level)
|
|
689
729
|
parts << format_tool_use(item, nil)
|
|
690
730
|
else
|
|
731
|
+
next if @clean
|
|
691
732
|
# Preserve other content types as JSON for now
|
|
692
733
|
parts << JSON.pretty_generate(item)
|
|
693
734
|
end
|
|
@@ -994,38 +1035,41 @@ class ClaudeConversationExporter
|
|
|
994
1035
|
|
|
995
1036
|
def format_combined_markdown(sessions)
|
|
996
1037
|
md = []
|
|
997
|
-
title = get_markdown_title
|
|
998
|
-
md << "# #{title}"
|
|
999
|
-
md << ""
|
|
1000
1038
|
|
|
1001
1039
|
if sessions.length == 1
|
|
1002
1040
|
# Single session - use original format
|
|
1003
1041
|
return format_markdown(sessions.first)
|
|
1004
1042
|
end
|
|
1005
1043
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1044
|
+
unless @clean
|
|
1045
|
+
title = get_markdown_title
|
|
1046
|
+
md << "# #{title}"
|
|
1047
|
+
md << ""
|
|
1010
1048
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1049
|
+
# Multiple sessions - combined format
|
|
1050
|
+
total_messages = sessions.sum { |s| s[:messages].length }
|
|
1051
|
+
total_user = sessions.sum { |s| s[:messages].count { |m| m[:role] == 'user' } }
|
|
1052
|
+
total_assistant = sessions.sum { |s| s[:messages].count { |m| m[:role] == 'assistant' } }
|
|
1014
1053
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1054
|
+
md << "**Sessions:** #{sessions.length}"
|
|
1055
|
+
md << "**Total Messages:** #{total_messages} (#{total_user} user, #{total_assistant} assistant)"
|
|
1056
|
+
md << ""
|
|
1017
1057
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
end
|
|
1058
|
+
first_timestamp = sessions.map { |s| s[:first_timestamp] }.compact.min
|
|
1059
|
+
last_timestamp = sessions.map { |s| s[:last_timestamp] }.compact.max
|
|
1021
1060
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1061
|
+
if first_timestamp
|
|
1062
|
+
md << "**Started:** #{Time.parse(first_timestamp).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1063
|
+
end
|
|
1025
1064
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1065
|
+
if last_timestamp
|
|
1066
|
+
md << "**Last activity:** #{Time.parse(last_timestamp).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
md << ""
|
|
1070
|
+
md << "---"
|
|
1071
|
+
md << ""
|
|
1072
|
+
end
|
|
1029
1073
|
|
|
1030
1074
|
# Process each session with separators
|
|
1031
1075
|
sessions.each_with_index do |session, session_index|
|
|
@@ -1033,19 +1077,22 @@ class ClaudeConversationExporter
|
|
|
1033
1077
|
md << ""
|
|
1034
1078
|
md << "---"
|
|
1035
1079
|
md << ""
|
|
1036
|
-
md << "# Session #{session_index + 1}"
|
|
1037
|
-
md << ""
|
|
1038
|
-
md << "**Session ID:** `#{session[:session_id]}`"
|
|
1039
1080
|
|
|
1040
|
-
|
|
1041
|
-
md << "
|
|
1042
|
-
|
|
1081
|
+
unless @clean
|
|
1082
|
+
md << "# Session #{session_index + 1}"
|
|
1083
|
+
md << ""
|
|
1084
|
+
md << "**Session ID:** `#{session[:session_id]}`"
|
|
1043
1085
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1086
|
+
if session[:first_timestamp]
|
|
1087
|
+
md << "**Started:** #{Time.parse(session[:first_timestamp]).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1088
|
+
end
|
|
1046
1089
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1090
|
+
user_count = session[:messages].count { |m| m[:role] == 'user' }
|
|
1091
|
+
assistant_count = session[:messages].count { |m| m[:role] == 'assistant' }
|
|
1092
|
+
|
|
1093
|
+
md << "**Messages:** #{session[:messages].length} (#{user_count} user, #{assistant_count} assistant)"
|
|
1094
|
+
md << ""
|
|
1095
|
+
end
|
|
1049
1096
|
end
|
|
1050
1097
|
|
|
1051
1098
|
# Add messages for this session
|
|
@@ -1064,27 +1111,30 @@ class ClaudeConversationExporter
|
|
|
1064
1111
|
|
|
1065
1112
|
def format_markdown(session)
|
|
1066
1113
|
md = []
|
|
1067
|
-
title = get_markdown_title
|
|
1068
|
-
md << "# #{title}"
|
|
1069
|
-
md << ""
|
|
1070
|
-
md << "**Session:** `#{session[:session_id]}`"
|
|
1071
|
-
md << ""
|
|
1072
|
-
|
|
1073
|
-
if session[:first_timestamp]
|
|
1074
|
-
md << "**Started:** #{Time.parse(session[:first_timestamp]).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1075
|
-
end
|
|
1076
1114
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1115
|
+
unless @clean
|
|
1116
|
+
title = get_markdown_title
|
|
1117
|
+
md << "# #{title}"
|
|
1118
|
+
md << ""
|
|
1119
|
+
md << "**Session:** `#{session[:session_id]}`"
|
|
1120
|
+
md << ""
|
|
1121
|
+
|
|
1122
|
+
if session[:first_timestamp]
|
|
1123
|
+
md << "**Started:** #{Time.parse(session[:first_timestamp]).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1124
|
+
end
|
|
1080
1125
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1126
|
+
if session[:last_timestamp]
|
|
1127
|
+
md << "**Last activity:** #{Time.parse(session[:last_timestamp]).getlocal.strftime('%B %d, %Y at %I:%M %p')}"
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
user_count = session[:messages].count { |m| m[:role] == 'user' }
|
|
1131
|
+
assistant_count = session[:messages].count { |m| m[:role] == 'assistant' }
|
|
1083
1132
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1133
|
+
md << "**Messages:** #{session[:messages].length} (#{user_count} user, #{assistant_count} assistant)"
|
|
1134
|
+
md << ""
|
|
1135
|
+
md << "---"
|
|
1136
|
+
md << ""
|
|
1137
|
+
end
|
|
1088
1138
|
|
|
1089
1139
|
# Process messages linearly - they're already processed and paired
|
|
1090
1140
|
session[:messages].each_with_index do |message, index|
|
|
@@ -1115,8 +1165,10 @@ class ClaudeConversationExporter
|
|
|
1115
1165
|
when 'assistant'
|
|
1116
1166
|
"## 🤖 Assistant"
|
|
1117
1167
|
when 'assistant_thinking'
|
|
1168
|
+
return [] if @clean
|
|
1118
1169
|
"## 🤖💭 Assistant"
|
|
1119
1170
|
when 'system'
|
|
1171
|
+
return [] if @clean
|
|
1120
1172
|
"## ⚙️ System"
|
|
1121
1173
|
end
|
|
1122
1174
|
|
|
@@ -1133,15 +1185,19 @@ class ClaudeConversationExporter
|
|
|
1133
1185
|
|
|
1134
1186
|
lines << role_header
|
|
1135
1187
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1188
|
+
unless @clean
|
|
1189
|
+
# Add message ID as HTML comment if available
|
|
1190
|
+
if message[:message_id]
|
|
1191
|
+
lines << "<!-- #{message[:message_id]} -->"
|
|
1192
|
+
end
|
|
1139
1193
|
end
|
|
1140
1194
|
|
|
1141
1195
|
lines << ""
|
|
1142
1196
|
end
|
|
1143
1197
|
|
|
1144
|
-
|
|
1198
|
+
content = message[:content]
|
|
1199
|
+
content = content.gsub(/\A[[:space:] ]+/, '') if @clean
|
|
1200
|
+
lines << content
|
|
1145
1201
|
lines << ""
|
|
1146
1202
|
|
|
1147
1203
|
lines
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ccexport
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marc Heiligers
|
|
@@ -55,8 +55,8 @@ files:
|
|
|
55
55
|
- LICENSE
|
|
56
56
|
- README.md
|
|
57
57
|
- Rakefile
|
|
58
|
+
- TODO.md
|
|
58
59
|
- bin/ccexport
|
|
59
|
-
- ccexport.gemspec
|
|
60
60
|
- exe/ccexport
|
|
61
61
|
- lib/assets/prism-bash.js
|
|
62
62
|
- lib/assets/prism-json.js
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/assets/prism.css
|
|
68
68
|
- lib/assets/prism.js
|
|
69
69
|
- lib/ccexport.rb
|
|
70
|
+
- lib/ccexport/cli.rb
|
|
70
71
|
- lib/ccexport/version.rb
|
|
71
72
|
- lib/claude_conversation_exporter.rb
|
|
72
73
|
- lib/markdown_code_block_parser.rb
|
data/ccexport.gemspec
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/ccexport/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "ccexport"
|
|
7
|
-
spec.version = CcExport::VERSION
|
|
8
|
-
spec.authors = ["Marc Heiligers"]
|
|
9
|
-
spec.email = ["marc@silvermerc.net"]
|
|
10
|
-
|
|
11
|
-
spec.summary = "Export and preview Claude Code conversations with syntax highlighting"
|
|
12
|
-
spec.description = <<~DESC
|
|
13
|
-
A Ruby tool to export Claude Code conversations from JSONL session files
|
|
14
|
-
into beautifully formatted Markdown and HTML files with syntax highlighting,
|
|
15
|
-
secret detection, and multiple template options.
|
|
16
|
-
DESC
|
|
17
|
-
spec.homepage = "https://github.com/marcheiligers/ccexport"
|
|
18
|
-
spec.license = "MIT"
|
|
19
|
-
spec.required_ruby_version = ">= 3.0.0"
|
|
20
|
-
|
|
21
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
22
|
-
spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
|
|
23
|
-
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
24
|
-
|
|
25
|
-
# Specify which files should be added to the gem when it is released.
|
|
26
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
27
|
-
spec.files = Dir.chdir(__dir__) do
|
|
28
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
29
|
-
(File.expand_path(f) == __FILE__) ||
|
|
30
|
-
f.start_with?(*%w[bin/fetch_secrets_patterns spec/ VIBE claude-conversations/ debug_compacted.rb discover_structure.out discover_structure.rb generate_vibe_samples]) ||
|
|
31
|
-
f.end_with?('.gem')
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
spec.bindir = "exe"
|
|
35
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
36
|
-
spec.require_paths = ["lib"]
|
|
37
|
-
|
|
38
|
-
# Runtime dependencies
|
|
39
|
-
spec.add_dependency "json", "~> 2.0"
|
|
40
|
-
|
|
41
|
-
# Development dependencies
|
|
42
|
-
spec.add_development_dependency "rspec", "~> 3.12"
|
|
43
|
-
|
|
44
|
-
# Specify the minimum version of RubyGems required
|
|
45
|
-
spec.required_rubygems_version = ">= 1.8.11"
|
|
46
|
-
|
|
47
|
-
# For more information and examples about making a new gem, check out our
|
|
48
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
|
49
|
-
end
|