copy_for_ai 0.1.4
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 +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/lib/copy_for_ai/content_injector.rb +366 -0
- data/lib/copy_for_ai/middleware.rb +63 -0
- data/lib/copy_for_ai/railtie.rb +21 -0
- data/lib/copy_for_ai/version.rb +5 -0
- data/lib/copy_for_ai.rb +8 -0
- metadata +84 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e41927310321363d9badd434462197ae4fd62a28d705bbc851bcdd5e4be9f521
|
|
4
|
+
data.tar.gz: 623374e6bb274dd721ac42b6d3ed129ffe17024afc68eb8560d784744b3c23c0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a551348845665088b0d2f4ecd758f38b83b74be9eb5e04021ca35ddc84632018a95eb70f3181d82bd1f418a794dd0953cb115acd597724b398609fc71f4566f
|
|
7
|
+
data.tar.gz: 810b4cf6c62442ff27c0d9a959ce41876102de321b7d695a866a3f3bd485098cdc6fe228a85df0f90c2c29e9cef9b4a6ae0a653234859d7f8e80f01d2a5c9ff8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.4] - 2024-12-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- "Copy for AI" button on Rails development error pages
|
|
15
|
+
- Copies error details as formatted Markdown
|
|
16
|
+
- Includes error type, message, source code, stack traces, and request info
|
|
17
|
+
- Compatible with Rails 7.0+ and 8.0+
|
|
18
|
+
- Styled to match Rails error page aesthetics
|
|
19
|
+
- Clipboard API with fallback for older browsers
|
|
20
|
+
|
|
21
|
+
### Technical Details
|
|
22
|
+
- Middleware-based injection (inserted before ActionDispatch::DebugExceptions)
|
|
23
|
+
- Automatic detection of Rails debug error pages
|
|
24
|
+
- Multiple fallback strategies for button placement
|
|
25
|
+
- Development-only activation
|
|
26
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Andrew Miller
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copy for AI
|
|
2
|
+
|
|
3
|
+
A Rails gem that adds a **"Copy for AI"** button to development error pages, allowing you to copy error details as formatted Markdown optimized for AI agent consumption.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Adds a styled button to Rails development error pages
|
|
8
|
+
- Extracts comprehensive error information:
|
|
9
|
+
- Error type and message
|
|
10
|
+
- Source code with file and line number
|
|
11
|
+
- Application stack trace
|
|
12
|
+
- Full stack trace
|
|
13
|
+
- Request details and parameters
|
|
14
|
+
- Formats output as clean Markdown for easy LLM parsing
|
|
15
|
+
- Zero configuration required
|
|
16
|
+
- Only active in development environment
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add this line to your application's Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
group :development do
|
|
24
|
+
gem 'copy_for_ai'
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
And then execute:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it! The gem automatically hooks into Rails and adds the button to error pages.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
1. When your Rails app encounters an error in development, you'll see the standard Rails error page
|
|
39
|
+
2. Look for the **"Copy for AI"** button in the header area (near the trace toggle buttons)
|
|
40
|
+
3. Click the button to copy all error details to your clipboard
|
|
41
|
+
4. Paste the Markdown into your AI assistant of choice
|
|
42
|
+
|
|
43
|
+
### Example Output
|
|
44
|
+
|
|
45
|
+
When you click "Copy for AI", you'll get formatted Markdown like this:
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
# Rails Error: NoMethodError
|
|
49
|
+
|
|
50
|
+
## Error Message
|
|
51
|
+
|
|
52
|
+
undefined method `foo' for nil:NilClass
|
|
53
|
+
|
|
54
|
+
## Source Code
|
|
55
|
+
|
|
56
|
+
**File:** `app/controllers/users_controller.rb`
|
|
57
|
+
**Line:** 15
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
def show
|
|
61
|
+
@user = User.find(params[:id])
|
|
62
|
+
@user.foo # Error occurs here
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Application Trace
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
app/controllers/users_controller.rb:15:in `show'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Full Trace
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
app/controllers/users_controller.rb:15:in `show'
|
|
76
|
+
actionpack (7.0.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Request Details
|
|
81
|
+
|
|
82
|
+
- **URL:** /users/1
|
|
83
|
+
- **Method:** GET
|
|
84
|
+
- **Parameters:**
|
|
85
|
+
```
|
|
86
|
+
{"id"=>"1"}
|
|
87
|
+
```
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
Currently, the gem works out of the box with no configuration needed. It automatically:
|
|
93
|
+
|
|
94
|
+
- Detects Rails development environment
|
|
95
|
+
- Identifies Rails error pages
|
|
96
|
+
- Injects the copy button with appropriate styling
|
|
97
|
+
|
|
98
|
+
## Requirements
|
|
99
|
+
|
|
100
|
+
- Ruby >= 2.7.0
|
|
101
|
+
- Rails 7.0+ or Rails 8.0+
|
|
102
|
+
|
|
103
|
+
**Tested with:**
|
|
104
|
+
- Rails 7.0, 7.1, 7.2
|
|
105
|
+
- Rails 8.0
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
110
|
+
|
|
111
|
+
To install this gem onto your local machine, run:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
bundle exec rake install
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Contributing
|
|
118
|
+
|
|
119
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
124
|
+
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CopyForAi
|
|
4
|
+
class ContentInjector
|
|
5
|
+
BUTTON_AND_SCRIPT = <<~HTML
|
|
6
|
+
<style>
|
|
7
|
+
.copy-for-ai-btn {
|
|
8
|
+
background: none;
|
|
9
|
+
color: #CC0000;
|
|
10
|
+
padding: 0;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
border: none;
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
font-size: inherit;
|
|
15
|
+
text-decoration: underline;
|
|
16
|
+
display: inline;
|
|
17
|
+
transition: color 0.15s ease;
|
|
18
|
+
}
|
|
19
|
+
.copy-for-ai-btn:hover {
|
|
20
|
+
color: #990000;
|
|
21
|
+
}
|
|
22
|
+
.copy-for-ai-btn.copied {
|
|
23
|
+
color: #00AA00;
|
|
24
|
+
}
|
|
25
|
+
.copy-for-ai-floating {
|
|
26
|
+
position: fixed;
|
|
27
|
+
top: 10px;
|
|
28
|
+
right: 10px;
|
|
29
|
+
z-index: 99999;
|
|
30
|
+
background: #CC0000;
|
|
31
|
+
color: #fff;
|
|
32
|
+
padding: 8px 16px;
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
}
|
|
38
|
+
.copy-for-ai-floating:hover {
|
|
39
|
+
background: #990000;
|
|
40
|
+
}
|
|
41
|
+
.copy-for-ai-floating.copied {
|
|
42
|
+
background: #00AA00;
|
|
43
|
+
color: #fff;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
<script>
|
|
47
|
+
(function() {
|
|
48
|
+
function initCopyForAi() {
|
|
49
|
+
// Create the button
|
|
50
|
+
var btn = document.createElement('button');
|
|
51
|
+
btn.className = 'copy-for-ai-btn';
|
|
52
|
+
btn.textContent = 'Copy for AI';
|
|
53
|
+
btn.title = 'Copy error details as Markdown for AI agents';
|
|
54
|
+
|
|
55
|
+
var inserted = false;
|
|
56
|
+
|
|
57
|
+
// Strategy 1: Find the trace links and insert after the last one
|
|
58
|
+
var traceLinks = document.querySelectorAll('a');
|
|
59
|
+
var lastTraceLink = null;
|
|
60
|
+
|
|
61
|
+
for (var i = 0; i < traceLinks.length; i++) {
|
|
62
|
+
var link = traceLinks[i];
|
|
63
|
+
var text = link.textContent.toLowerCase();
|
|
64
|
+
if (text.includes('application trace') || text.includes('framework trace') || text.includes('full trace')) {
|
|
65
|
+
lastTraceLink = link;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (lastTraceLink) {
|
|
70
|
+
// Insert right after the last trace link as a sibling
|
|
71
|
+
var separator = document.createTextNode(' | ');
|
|
72
|
+
lastTraceLink.parentNode.insertBefore(separator, lastTraceLink.nextSibling);
|
|
73
|
+
lastTraceLink.parentNode.insertBefore(btn, separator.nextSibling);
|
|
74
|
+
inserted = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Strategy 2: Look for header with Rails.root info
|
|
78
|
+
if (!inserted) {
|
|
79
|
+
var allText = document.body.innerText;
|
|
80
|
+
if (allText.includes('Rails.root:')) {
|
|
81
|
+
// Find the element containing Rails.root
|
|
82
|
+
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
|
|
83
|
+
while (walker.nextNode()) {
|
|
84
|
+
if (walker.currentNode.textContent.includes('Rails.root:')) {
|
|
85
|
+
var parent = walker.currentNode.parentElement;
|
|
86
|
+
if (parent) {
|
|
87
|
+
parent.parentElement.insertBefore(btn, parent.nextSibling);
|
|
88
|
+
inserted = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Strategy 3: Insert after h1 or h2
|
|
97
|
+
if (!inserted) {
|
|
98
|
+
var h2 = document.querySelector('h2');
|
|
99
|
+
if (h2 && h2.parentElement) {
|
|
100
|
+
h2.parentElement.insertBefore(btn, h2.nextSibling);
|
|
101
|
+
inserted = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Strategy 4: Floating button as fallback
|
|
106
|
+
if (!inserted) {
|
|
107
|
+
btn.classList.add('copy-for-ai-floating');
|
|
108
|
+
document.body.appendChild(btn);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
btn.addEventListener('click', function(e) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
var markdown = extractErrorAsMarkdown();
|
|
114
|
+
copyToClipboard(markdown, btn);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractErrorAsMarkdown() {
|
|
119
|
+
var lines = [];
|
|
120
|
+
|
|
121
|
+
// Extract error type from h1
|
|
122
|
+
var h1 = document.querySelector('h1');
|
|
123
|
+
var errorType = h1 ? h1.textContent.trim() : 'Unknown Error';
|
|
124
|
+
|
|
125
|
+
// Extract error message from h2
|
|
126
|
+
var h2 = document.querySelector('h2');
|
|
127
|
+
var errorMessage = h2 ? h2.textContent.trim() : '';
|
|
128
|
+
|
|
129
|
+
lines.push('# Rails Error: ' + errorType);
|
|
130
|
+
lines.push('');
|
|
131
|
+
if (errorMessage) {
|
|
132
|
+
lines.push('## Error Message');
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push(errorMessage);
|
|
135
|
+
lines.push('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Extract Rails.root info
|
|
139
|
+
var bodyText = document.body.innerText;
|
|
140
|
+
var railsRootMatch = bodyText.match(/Rails\\.root:\\s*([^\\n]+)/);
|
|
141
|
+
if (railsRootMatch) {
|
|
142
|
+
lines.push('**Rails.root:** `' + railsRootMatch[1].trim() + '`');
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Extract source code sections
|
|
147
|
+
var sourceInfo = extractSourceCode();
|
|
148
|
+
if (sourceInfo) {
|
|
149
|
+
lines.push('## Source Code');
|
|
150
|
+
lines.push('');
|
|
151
|
+
if (sourceInfo.file) {
|
|
152
|
+
lines.push('**File:** `' + sourceInfo.file + '`');
|
|
153
|
+
}
|
|
154
|
+
if (sourceInfo.line) {
|
|
155
|
+
lines.push('**Line:** ' + sourceInfo.line);
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
if (sourceInfo.code) {
|
|
159
|
+
lines.push('```ruby');
|
|
160
|
+
lines.push(sourceInfo.code);
|
|
161
|
+
lines.push('```');
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract traces
|
|
167
|
+
var traces = extractAllTraces();
|
|
168
|
+
if (traces.application) {
|
|
169
|
+
lines.push('## Application Trace');
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('```');
|
|
172
|
+
lines.push(traces.application);
|
|
173
|
+
lines.push('```');
|
|
174
|
+
lines.push('');
|
|
175
|
+
}
|
|
176
|
+
if (traces.framework) {
|
|
177
|
+
lines.push('## Framework Trace');
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push('```');
|
|
180
|
+
lines.push(traces.framework);
|
|
181
|
+
lines.push('```');
|
|
182
|
+
lines.push('');
|
|
183
|
+
}
|
|
184
|
+
if (traces.full) {
|
|
185
|
+
lines.push('## Full Trace');
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push('```');
|
|
188
|
+
lines.push(traces.full);
|
|
189
|
+
lines.push('```');
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract Routes section if present (for routing errors)
|
|
194
|
+
var routesSection = extractRoutes();
|
|
195
|
+
if (routesSection) {
|
|
196
|
+
lines.push('## Available Routes');
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('```');
|
|
199
|
+
lines.push(routesSection);
|
|
200
|
+
lines.push('```');
|
|
201
|
+
lines.push('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return lines.join('\\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function extractSourceCode() {
|
|
208
|
+
var result = {};
|
|
209
|
+
|
|
210
|
+
// Look for source extract divs
|
|
211
|
+
var sourceExtracts = document.querySelectorAll('[id*="source"], [class*="source"], .extract');
|
|
212
|
+
for (var i = 0; i < sourceExtracts.length; i++) {
|
|
213
|
+
var section = sourceExtracts[i];
|
|
214
|
+
|
|
215
|
+
// Look for file info
|
|
216
|
+
var headerText = '';
|
|
217
|
+
var headers = section.querySelectorAll('h4, h5, .info, [class*="file"]');
|
|
218
|
+
if (headers.length > 0) {
|
|
219
|
+
headerText = headers[0].textContent.trim();
|
|
220
|
+
var match = headerText.match(/([^:]+\\.rb):?(\\d+)?/);
|
|
221
|
+
if (match) {
|
|
222
|
+
result.file = match[1].trim();
|
|
223
|
+
if (match[2]) result.line = match[2];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Look for code
|
|
228
|
+
var code = section.querySelector('pre, code, .code');
|
|
229
|
+
if (code) {
|
|
230
|
+
result.code = code.textContent.trim();
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (result.code || result.file) ? result : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractAllTraces() {
|
|
239
|
+
var traces = {
|
|
240
|
+
application: null,
|
|
241
|
+
framework: null,
|
|
242
|
+
full: null
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Look for trace divs by ID
|
|
246
|
+
var appTrace = document.querySelector('#Application-Trace, #application-trace, [id*="Application"][id*="Trace"]');
|
|
247
|
+
var frameworkTrace = document.querySelector('#Framework-Trace, #framework-trace, [id*="Framework"][id*="Trace"]');
|
|
248
|
+
var fullTrace = document.querySelector('#Full-Trace, #full-trace, [id*="Full"][id*="Trace"]');
|
|
249
|
+
|
|
250
|
+
if (appTrace) {
|
|
251
|
+
var pre = appTrace.querySelector('pre, code');
|
|
252
|
+
if (pre) traces.application = pre.textContent.trim();
|
|
253
|
+
}
|
|
254
|
+
if (frameworkTrace) {
|
|
255
|
+
var pre = frameworkTrace.querySelector('pre, code');
|
|
256
|
+
if (pre) traces.framework = pre.textContent.trim();
|
|
257
|
+
}
|
|
258
|
+
if (fullTrace) {
|
|
259
|
+
var pre = fullTrace.querySelector('pre, code');
|
|
260
|
+
if (pre) traces.full = pre.textContent.trim();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fallback: look for any trace-related content
|
|
264
|
+
if (!traces.application && !traces.framework && !traces.full) {
|
|
265
|
+
var allPres = document.querySelectorAll('pre');
|
|
266
|
+
for (var i = 0; i < allPres.length; i++) {
|
|
267
|
+
var pre = allPres[i];
|
|
268
|
+
var text = pre.textContent;
|
|
269
|
+
// Check if it looks like a stack trace
|
|
270
|
+
if (text.includes(':in `') || text.includes('.rb:')) {
|
|
271
|
+
if (!traces.full) {
|
|
272
|
+
traces.full = text.trim();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return traces;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractRoutes() {
|
|
282
|
+
// For routing errors, extract available routes
|
|
283
|
+
var routesTable = document.querySelector('table');
|
|
284
|
+
if (routesTable) {
|
|
285
|
+
var rows = routesTable.querySelectorAll('tr');
|
|
286
|
+
if (rows.length > 0) {
|
|
287
|
+
var routeLines = [];
|
|
288
|
+
rows.forEach(function(row) {
|
|
289
|
+
var cells = row.querySelectorAll('th, td');
|
|
290
|
+
if (cells.length >= 3) {
|
|
291
|
+
routeLines.push(
|
|
292
|
+
(cells[0].textContent.trim().padEnd(10) + ' ') +
|
|
293
|
+
(cells[1].textContent.trim().padEnd(40) + ' ') +
|
|
294
|
+
cells[2].textContent.trim()
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (routeLines.length > 1) { // More than just header
|
|
299
|
+
return routeLines.slice(0, 20).join('\\n') + (rows.length > 20 ? '\\n... and more' : '');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function copyToClipboard(text, btn) {
|
|
307
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
308
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
309
|
+
showCopiedFeedback(btn);
|
|
310
|
+
}).catch(function(err) {
|
|
311
|
+
fallbackCopy(text, btn);
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
fallbackCopy(text, btn);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function fallbackCopy(text, btn) {
|
|
319
|
+
var textarea = document.createElement('textarea');
|
|
320
|
+
textarea.value = text;
|
|
321
|
+
textarea.style.position = 'fixed';
|
|
322
|
+
textarea.style.left = '-9999px';
|
|
323
|
+
document.body.appendChild(textarea);
|
|
324
|
+
textarea.select();
|
|
325
|
+
try {
|
|
326
|
+
document.execCommand('copy');
|
|
327
|
+
showCopiedFeedback(btn);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
alert('Failed to copy. Error details:\\n\\n' + text.substring(0, 1000));
|
|
330
|
+
}
|
|
331
|
+
document.body.removeChild(textarea);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function showCopiedFeedback(btn) {
|
|
335
|
+
var originalText = btn.textContent;
|
|
336
|
+
btn.textContent = 'Copied!';
|
|
337
|
+
btn.classList.add('copied');
|
|
338
|
+
setTimeout(function() {
|
|
339
|
+
btn.textContent = originalText;
|
|
340
|
+
btn.classList.remove('copied');
|
|
341
|
+
}, 2000);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Initialize when DOM is ready
|
|
345
|
+
if (document.readyState === 'loading') {
|
|
346
|
+
document.addEventListener('DOMContentLoaded', initCopyForAi);
|
|
347
|
+
} else {
|
|
348
|
+
initCopyForAi();
|
|
349
|
+
}
|
|
350
|
+
})();
|
|
351
|
+
</script>
|
|
352
|
+
HTML
|
|
353
|
+
|
|
354
|
+
class << self
|
|
355
|
+
def inject(html)
|
|
356
|
+
# Inject before closing </body> tag
|
|
357
|
+
if html.include?("</body>")
|
|
358
|
+
html.sub("</body>", "#{BUTTON_AND_SCRIPT}</body>")
|
|
359
|
+
else
|
|
360
|
+
# If no body tag, append to end
|
|
361
|
+
html + BUTTON_AND_SCRIPT
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "content_injector"
|
|
4
|
+
|
|
5
|
+
module CopyForAi
|
|
6
|
+
class Middleware
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
status, headers, response = @app.call(env)
|
|
13
|
+
|
|
14
|
+
# Only modify error responses (4xx and 5xx) with HTML content
|
|
15
|
+
if error_response?(status) && html_response?(headers)
|
|
16
|
+
body = extract_body(response)
|
|
17
|
+
|
|
18
|
+
# Check if this is a Rails debug error page
|
|
19
|
+
if rails_error_page?(body)
|
|
20
|
+
modified_body = ContentInjector.inject(body)
|
|
21
|
+
headers["Content-Length"] = modified_body.bytesize.to_s
|
|
22
|
+
response = [modified_body]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
[status, headers, response]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def error_response?(status)
|
|
32
|
+
status >= 400 && status < 600
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def html_response?(headers)
|
|
36
|
+
content_type = headers["Content-Type"] || headers["content-type"] || ""
|
|
37
|
+
content_type.include?("text/html")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_body(response)
|
|
41
|
+
body = +""
|
|
42
|
+
response.each { |part| body << part }
|
|
43
|
+
response.close if response.respond_to?(:close)
|
|
44
|
+
body
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rails_error_page?(body)
|
|
48
|
+
# Check for Rails debug error page markers - be more flexible
|
|
49
|
+
# Rails 7+ uses different markers than older versions
|
|
50
|
+
has_trace_links = body.include?("Application Trace") ||
|
|
51
|
+
body.include?("Framework Trace") ||
|
|
52
|
+
body.include?("Full Trace") ||
|
|
53
|
+
body.include?("Full trace")
|
|
54
|
+
|
|
55
|
+
has_error_markers = body.include?("Rails.root:") ||
|
|
56
|
+
body.include?("ActionController::RoutingError") ||
|
|
57
|
+
body.include?("id=\"container\"") ||
|
|
58
|
+
body.include?("id=\"traces\"")
|
|
59
|
+
|
|
60
|
+
has_trace_links && has_error_markers
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "middleware"
|
|
4
|
+
|
|
5
|
+
module CopyForAi
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "copy_for_ai.add_middleware", after: :load_config_initializers do |app|
|
|
8
|
+
# Only add middleware in development environment
|
|
9
|
+
if Rails.env.development?
|
|
10
|
+
# Insert BEFORE DebugExceptions so we can see the error page response
|
|
11
|
+
# that DebugExceptions generates
|
|
12
|
+
begin
|
|
13
|
+
app.middleware.insert_before ActionDispatch::DebugExceptions, CopyForAi::Middleware
|
|
14
|
+
rescue
|
|
15
|
+
# Fallback: insert at the beginning
|
|
16
|
+
app.middleware.insert 0, CopyForAi::Middleware
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/copy_for_ai.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: copy_for_ai
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Miller
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: actionpack
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
description: A Rails gem that adds a 'Copy for AI' button to development error pages,
|
|
42
|
+
allowing you to copy error details as formatted Markdown for easy consumption by
|
|
43
|
+
AI agents.
|
|
44
|
+
email:
|
|
45
|
+
- andrew@example.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/copy_for_ai.rb
|
|
54
|
+
- lib/copy_for_ai/content_injector.rb
|
|
55
|
+
- lib/copy_for_ai/middleware.rb
|
|
56
|
+
- lib/copy_for_ai/railtie.rb
|
|
57
|
+
- lib/copy_for_ai/version.rb
|
|
58
|
+
homepage: https://github.com/andrewmiller/copy_for_ai
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
homepage_uri: https://github.com/andrewmiller/copy_for_ai
|
|
63
|
+
source_code_uri: https://github.com/andrewmiller/copy_for_ai
|
|
64
|
+
changelog_uri: https://github.com/andrewmiller/copy_for_ai/blob/main/CHANGELOG.md
|
|
65
|
+
post_install_message:
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 2.7.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.5.9
|
|
81
|
+
signing_key:
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: Copy Rails error messages in AI-friendly format
|
|
84
|
+
test_files: []
|