ccexport 0.1.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.
@@ -0,0 +1,268 @@
1
+ class MarkdownCodeBlockParser
2
+ attr_reader :markdown, :matches, :pairs, :blocks
3
+
4
+ def initialize(markdown)
5
+ @markdown = markdown
6
+ @matches = []
7
+ @pairs = []
8
+ @blocks = []
9
+ end
10
+
11
+ def parse
12
+ find_all_code_blocks
13
+ pair_code_blocks
14
+ build_block_structures
15
+ detect_nesting
16
+ self
17
+ end
18
+
19
+ private
20
+
21
+ def find_all_code_blocks
22
+ # Find all backtick sequences with their positions
23
+ pattern = /(^|\n)(```+)([a-zA-Z0-9]*)?/
24
+
25
+ @markdown.scan(pattern) do
26
+ match = Regexp.last_match
27
+ pos = match.begin(0)
28
+ backticks = match[2]
29
+ lang = match[3] unless match[3].nil? || match[3].empty?
30
+
31
+ @matches << {
32
+ pos: pos,
33
+ ticks: backticks.length,
34
+ lang: lang,
35
+ full_match: match[0],
36
+ index: @matches.length
37
+ }
38
+ end
39
+ end
40
+
41
+ def pair_code_blocks
42
+ stack = []
43
+
44
+ @matches.each_with_index do |match, i|
45
+ # Heuristic 1: If it has a language specifier, it's definitely opening
46
+ if match[:lang]
47
+ stack.push(i)
48
+ next
49
+ end
50
+
51
+ # Heuristic 2: Check if this could be a closing block
52
+ if !stack.empty? && i > 0
53
+ last_opener_idx = stack.last
54
+ last_opener = @matches[last_opener_idx]
55
+
56
+ # Get content between them
57
+ content_start = last_opener[:pos] + last_opener[:full_match].length
58
+ content_end = match[:pos]
59
+ content_between = @markdown[content_start...content_end].strip
60
+
61
+ # If there's substantial content and tick counts match, likely a pair
62
+ if content_between.length > 10 && last_opener[:ticks] == match[:ticks]
63
+ @pairs << [last_opener_idx, i]
64
+ stack.pop
65
+ next
66
+ end
67
+ end
68
+
69
+ # Heuristic 3: Check if this looks like an opening based on what follows
70
+ if i < @matches.length - 1
71
+ next_match = @matches[i + 1]
72
+ content_after_end = next_match ? next_match[:pos] : @markdown.length
73
+ content_after = @markdown[(match[:pos] + match[:full_match].length)...content_after_end].strip
74
+
75
+ # If there's content after that looks like code, this might be opening
76
+ if !content_after.empty? && !content_after.start_with?('#')
77
+ stack.push(i)
78
+ next
79
+ end
80
+ end
81
+
82
+ # Default: assume it's closing if we have something to close
83
+ if !stack.empty?
84
+ # Try to match with the most recent opener with same tick count
85
+ matched = false
86
+ stack.reverse_each.with_index do |stack_idx, j|
87
+ if @matches[stack_idx][:ticks] == match[:ticks]
88
+ @pairs << [stack_idx, i]
89
+ stack.delete_at(stack.length - 1 - j)
90
+ matched = true
91
+ break
92
+ end
93
+ end
94
+
95
+ # No matching tick count, close the most recent
96
+ if !matched
97
+ @pairs << [stack.last, i]
98
+ stack.pop
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def build_block_structures
105
+ @pairs.each do |start_idx, end_idx|
106
+ start_match = @matches[start_idx]
107
+ end_match = @matches[end_idx]
108
+
109
+ block = {
110
+ start_pos: start_match[:pos],
111
+ end_pos: end_match[:pos] + end_match[:full_match].length,
112
+ language: start_match[:lang],
113
+ tick_count: start_match[:ticks],
114
+ content_start: start_match[:pos] + start_match[:full_match].length,
115
+ content_end: end_match[:pos],
116
+ start_match_idx: start_idx,
117
+ end_match_idx: end_idx
118
+ }
119
+
120
+ # Extract content
121
+ block[:content] = @markdown[block[:content_start]...block[:content_end]].strip
122
+
123
+ @blocks << block
124
+ end
125
+ end
126
+
127
+ def detect_nesting
128
+ @blocks.each_with_index do |block, i|
129
+ block[:nested_blocks] = []
130
+ block[:depth] = 0
131
+
132
+ @blocks.each_with_index do |other, j|
133
+ next if i == j
134
+
135
+ # Check if other is nested inside block
136
+ if block[:start_pos] < other[:start_pos] && other[:end_pos] < block[:end_pos]
137
+ block[:nested_blocks] << j
138
+ end
139
+
140
+ # Check if block is nested inside other
141
+ if other[:start_pos] < block[:start_pos] && block[:end_pos] < other[:end_pos]
142
+ block[:depth] += 1
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def escape_nested_blocks
149
+ # Calculate maximum nesting depth
150
+ max_depth = @blocks.map { |b| b[:depth] }.max || 0
151
+
152
+ # Create a list of modifications to apply
153
+ modifications = []
154
+
155
+ @blocks.each do |block|
156
+ # Use 3 + (max_depth - depth) backticks
157
+ # So the outermost uses the most backticks
158
+ new_tick_count = 3 + (max_depth - block[:depth])
159
+ old_tick_count = block[:tick_count]
160
+
161
+ if new_tick_count != old_tick_count
162
+ # Record the modification for the opening backticks
163
+ start_match = @matches[block[:start_match_idx]]
164
+ modifications << {
165
+ pos: start_match[:pos],
166
+ old_text: start_match[:full_match],
167
+ new_text: start_match[:full_match].gsub('`' * old_tick_count, '`' * new_tick_count)
168
+ }
169
+
170
+ # Record the modification for the closing backticks
171
+ end_match = @matches[block[:end_match_idx]]
172
+ modifications << {
173
+ pos: end_match[:pos],
174
+ old_text: end_match[:full_match],
175
+ new_text: end_match[:full_match].gsub('`' * old_tick_count, '`' * new_tick_count)
176
+ }
177
+ end
178
+ end
179
+
180
+ # Sort modifications by position (reverse order to maintain positions)
181
+ modifications.sort_by! { |m| -m[:pos] }
182
+
183
+ # Apply modifications
184
+ result = @markdown.dup
185
+ modifications.each do |mod|
186
+ result[mod[:pos], mod[:old_text].length] = mod[:new_text]
187
+ end
188
+
189
+ result
190
+ end
191
+
192
+ public
193
+
194
+ def display_analysis
195
+ puts "Detected #{@matches.length} code block delimiters"
196
+ puts "Formed #{@pairs.length} pairs"
197
+ puts "\nCode blocks found:"
198
+
199
+ @blocks.each_with_index do |block, i|
200
+ puts "\nBlock #{i}:"
201
+ puts " Language: #{block[:language] || 'none'}"
202
+ puts " Position: #{block[:start_pos]}-#{block[:end_pos]}"
203
+ puts " Tick count: #{block[:tick_count]}"
204
+ puts " Nesting depth: #{block[:depth]}"
205
+ puts " Contains blocks: #{block[:nested_blocks].join(', ')}" unless block[:nested_blocks].empty?
206
+
207
+ content_preview = block[:content][0..80].gsub(/\n/, ' ')
208
+ puts " Content preview: #{content_preview}..."
209
+ end
210
+ end
211
+ end
212
+
213
+ # Example usage
214
+ if __FILE__ == $0
215
+ example = <<~MARKDOWN
216
+ Perfect! I've successfully implemented the tool use formatting with collapsed sections as requested. Here's what I implemented:
217
+
218
+ **Key Features:**
219
+
220
+ 1. **Tool Use Detection**: The code now detects `tool_use` entries in assistant message content arrays
221
+ 2. **Tool Pairing**: When a `tool_use` is found, it looks for the immediately following `tool_result` and pairs them together
222
+ 3. **Collapsed Sections**: Uses GitHub's `<details>/<summary>` tags from the cheatsheet
223
+ 4. **Formatted Output**: Each tool use gets:
224
+ - A "## Tool Use" header
225
+ - A collapsed section with the tool name as the summary
226
+ - Input parameters displayed in a JSON code block
227
+ - A nested collapsed section for tool results (if present)
228
+
229
+ **Example Output:**
230
+ ```markdown
231
+ ## Tool Use
232
+ <details>
233
+ <summary>Read</summary>
234
+
235
+ ```json
236
+ {
237
+ "file_path": "/path/to/file.txt"
238
+ }
239
+ ```
240
+
241
+ <details>
242
+ <summary>Tool Result</summary>
243
+
244
+ ```
245
+ File contents here
246
+ ```
247
+ </details>
248
+ </details>
249
+ ```
250
+
251
+ **Test Coverage:**
252
+ - All 20 tests pass
253
+ - Tests verify tool use formatting with and without results
254
+ - Tests ensure text content is still extracted cleanly
255
+ - Tests confirm non-text content is preserved appropriately
256
+
257
+ The tool use sections are now cleanly formatted with collapsible sections that show the tool name prominently and hide the technical details until clicked. Ready to continue customizing the formatting further!
258
+ MARKDOWN
259
+
260
+ parser = MarkdownCodeBlockParser.new(example)
261
+ parser.parse
262
+ parser.display_analysis
263
+
264
+ puts "\n" + "="*50
265
+ puts "Escaped version:"
266
+ puts "="*50
267
+ puts parser.escape_nested_blocks
268
+ end
@@ -0,0 +1,89 @@
1
+ require 'json'
2
+ require 'tempfile'
3
+
4
+ class TruffleHogSecretDetector
5
+ class Finding
6
+ attr_reader :type, :pattern_name, :match, :detector_name, :verified, :line
7
+
8
+ def initialize(detector_name, match, verified, line = nil)
9
+ @type = 'secret'
10
+ @pattern_name = detector_name
11
+ @detector_name = detector_name
12
+ @match = match
13
+ @verified = verified
14
+ @line = line
15
+ end
16
+
17
+ # For backward compatibility with old SecretDetector interface
18
+ alias_method :confidence, :verified
19
+ end
20
+
21
+ def initialize
22
+ # Check if trufflehog is available
23
+ unless system('which trufflehog > /dev/null 2>&1')
24
+ raise "TruffleHog not found. Install it with: brew install trufflehog"
25
+ end
26
+ end
27
+
28
+ def scan(text)
29
+ return [] if text.nil? || text.empty?
30
+
31
+ findings = []
32
+
33
+ # Create a temporary file with the text
34
+ Tempfile.create(['trufflehog_scan', '.txt']) do |temp_file|
35
+ temp_file.write(text)
36
+ temp_file.flush
37
+
38
+ # Run trufflehog on the temporary file
39
+ output = `trufflehog filesystem --json --no-verification #{temp_file.path} 2>/dev/null`
40
+
41
+ # Parse each line of JSON output
42
+ output.each_line do |line|
43
+ line = line.strip
44
+ next if line.empty?
45
+
46
+ begin
47
+ result = JSON.parse(line)
48
+ # Skip non-detection results (like log messages)
49
+ next unless result['DetectorName'] && result['Raw']
50
+
51
+ findings << Finding.new(
52
+ result['DetectorName'],
53
+ result['Raw'],
54
+ result['Verified'] || false,
55
+ result.dig('SourceMetadata', 'Data', 'Filesystem', 'line')
56
+ )
57
+ rescue JSON::ParserError
58
+ # Skip malformed JSON lines
59
+ next
60
+ end
61
+ end
62
+ end
63
+
64
+ findings
65
+ end
66
+
67
+ def redact(text, replacement = '[REDACTED]')
68
+ return text if text.nil? || text.empty?
69
+
70
+ findings = scan(text)
71
+ return text if findings.empty?
72
+
73
+ redacted_text = text.dup
74
+
75
+ # Sort findings by match length (longest first) to avoid partial replacements
76
+ findings.sort_by { |f| -f.match.length }.each do |finding|
77
+ redacted_text = redacted_text.gsub(finding.match, replacement)
78
+ end
79
+
80
+ redacted_text
81
+ end
82
+
83
+ def has_secrets?(text)
84
+ !scan(text).empty?
85
+ end
86
+ end
87
+
88
+ # Alias for backward compatibility
89
+ SecretDetector = TruffleHogSecretDetector
@@ -0,0 +1,241 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %></title>
7
+ <%= include_prism %>
8
+ <style>
9
+ :root {
10
+ --dark-brown: #592d1c;
11
+ --medium-brown: #cc7c5e;
12
+ --light-brown: #f3dfd8;
13
+ --lightest-brown: #f9efec;
14
+ --light-blue: #a6d2e3;
15
+ --dark-blue: #1c4859;
16
+ }
17
+
18
+ body {
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
20
+ line-height: 1.6;
21
+ color: var(--dark-brown);
22
+ background-color: var(--lightest-brown);
23
+ margin: 0;
24
+ padding: 20px;
25
+ }
26
+
27
+ .conversation-content {
28
+ max-width: 900px;
29
+ margin: 0 auto;
30
+ background: white;
31
+ padding: 32px;
32
+ border-radius: 16px;
33
+ box-shadow: 0 4px 6px rgba(89, 45, 28, 0.1);
34
+ }
35
+
36
+ /* Headers */
37
+ h1 {
38
+ color: var(--dark-blue);
39
+ border-bottom: 2px solid var(--light-blue);
40
+ padding-bottom: 12px;
41
+ margin-bottom: 24px;
42
+ border-radius: 0 0 8px 8px;
43
+ }
44
+
45
+ h2 {
46
+ color: var(--dark-brown);
47
+ margin-top: 32px;
48
+ margin-bottom: 16px;
49
+ border-bottom: 1px solid var(--medium-brown);
50
+ }
51
+
52
+ /* Code blocks - integrate with Prism.js */
53
+ pre[class*="language-"],
54
+ pre:not([class*="language-"]) {
55
+ background: var(--dark-blue) !important;
56
+ border-radius: 12px;
57
+ overflow-x: auto;
58
+ margin: 16px 0;
59
+ padding: 16px;
60
+ }
61
+
62
+ code[class*="language-"],
63
+ pre:not([class*="language-"]) > code {
64
+ color: var(--light-blue) !important;
65
+ text-shadow: none !important;
66
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
67
+ }
68
+
69
+ /* Inline code (no language class) */
70
+ :not(pre) > code:not([class*="language-"]) {
71
+ background: var(--light-brown);
72
+ color: var(--dark-brown);
73
+ padding: 2px 6px;
74
+ border-radius: 6px;
75
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
76
+ }
77
+
78
+ /* Override Prism.js colors for better contrast on dark background */
79
+ .token.comment,
80
+ .token.prolog,
81
+ .token.doctype,
82
+ .token.cdata { color: #a0a0a0 !important; }
83
+
84
+ .token.punctuation { color: #f8f8f2 !important; }
85
+
86
+ .token.property,
87
+ .token.tag,
88
+ .token.boolean,
89
+ .token.number,
90
+ .token.constant,
91
+ .token.symbol,
92
+ .token.deleted { color: #f92672 !important; }
93
+
94
+ .token.selector,
95
+ .token.attr-name,
96
+ .token.string,
97
+ .token.char,
98
+ .token.builtin,
99
+ .token.inserted { color: #a6e22e !important; }
100
+
101
+ .token.operator,
102
+ .token.entity,
103
+ .token.url { color: #f8f8f2 !important; }
104
+
105
+ .token.atrule,
106
+ .token.attr-value,
107
+ .token.keyword { color: #66d9ef !important; }
108
+
109
+ .token.function,
110
+ .token.class-name { color: #e6db74 !important; }
111
+
112
+ .token.regex,
113
+ .token.important,
114
+ .token.variable { color: #fd971f !important; }
115
+
116
+ /* Blockquotes (thinking content) */
117
+ blockquote {
118
+ border-left: 4px solid var(--light-blue);
119
+ margin: 16px 0;
120
+ padding: 12px 20px;
121
+ background: var(--lightest-brown);
122
+ border-radius: 0 12px 12px 0;
123
+ font-style: italic;
124
+ color: var(--dark-blue);
125
+ }
126
+
127
+ /* Details/Summary (collapsible sections) */
128
+ details {
129
+ margin: 16px 0;
130
+ border: 1px solid var(--light-brown);
131
+ border-radius: 12px;
132
+ overflow: hidden;
133
+ }
134
+
135
+ summary {
136
+ background: var(--light-brown);
137
+ color: black;
138
+ padding: 12px 16px;
139
+ cursor: pointer;
140
+ font-weight: 600;
141
+ border-radius: 12px 12px 0 0;
142
+ transition: background-color 0.2s ease;
143
+ }
144
+
145
+ summary:hover {
146
+ background: var(--medium-brown);
147
+ color: white;
148
+ }
149
+
150
+ details[open] summary {
151
+ border-radius: 12px 12px 0 0;
152
+ }
153
+
154
+ details > *:not(summary) {
155
+ padding: 16px;
156
+ margin: 0;
157
+ }
158
+
159
+ details > pre {
160
+ margin: 0;
161
+ border-radius: 0;
162
+ }
163
+
164
+ /* Tables */
165
+ table {
166
+ border-collapse: collapse;
167
+ width: 100%;
168
+ margin: 16px 0;
169
+ border-radius: 12px;
170
+ overflow: hidden;
171
+ box-shadow: 0 2px 4px rgba(89, 45, 28, 0.1);
172
+ }
173
+
174
+ th, td {
175
+ padding: 12px 16px;
176
+ text-align: left;
177
+ border-bottom: 1px solid var(--light-brown);
178
+ }
179
+
180
+ th {
181
+ background: var(--medium-brown);
182
+ color: white;
183
+ font-weight: 600;
184
+ }
185
+
186
+ tr:nth-child(even) {
187
+ background: var(--lightest-brown);
188
+ }
189
+
190
+ /* Links */
191
+ a {
192
+ color: var(--dark-blue);
193
+ text-decoration: none;
194
+ border-bottom: 1px solid var(--light-blue);
195
+ transition: border-color 0.2s ease;
196
+ }
197
+
198
+ a:hover {
199
+ border-bottom-color: var(--dark-blue);
200
+ }
201
+
202
+ /* Horizontal rules */
203
+ hr {
204
+ border: none;
205
+ height: 2px;
206
+ background: linear-gradient(to right, var(--light-blue), var(--medium-brown), var(--light-blue));
207
+ margin: 32px 0;
208
+ border-radius: 2px;
209
+ }
210
+
211
+ /* Lists */
212
+ ul, ol {
213
+ padding-left: 24px;
214
+ }
215
+
216
+ li {
217
+ margin: 8px 0;
218
+ }
219
+
220
+ /* Strong/Bold text */
221
+ strong, b {
222
+ color: var(--dark-brown);
223
+ font-weight: 600;
224
+ }
225
+
226
+ p {
227
+ overflow: scroll;
228
+ }
229
+
230
+ /* Session metadata styling */
231
+ p strong {
232
+ color: var(--dark-blue);
233
+ }
234
+ </style>
235
+ </head>
236
+ <body>
237
+ <main class="conversation-content">
238
+ <%= content %>
239
+ </main>
240
+ </body>
241
+ </html>