rails-ai-context 4.2.0 → 4.2.1
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 +18 -0
- data/CLAUDE.md +1 -1
- data/README.md +2 -2
- data/SECURITY.md +8 -2
- data/lib/rails_ai_context/fingerprinter.rb +3 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +4 -4
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +4 -1
- data/lib/rails_ai_context/resources.rb +16 -12
- data/lib/rails_ai_context/server.rb +2 -1
- data/lib/rails_ai_context/tools/dependency_graph.rb +4 -1
- data/lib/rails_ai_context/tools/migration_advisor.rb +9 -0
- data/lib/rails_ai_context/tools/query.rb +7 -3
- data/lib/rails_ai_context/tools/read_logs.rb +6 -2
- data/lib/rails_ai_context/tools/search_docs.rb +12 -3
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4979f73cd9d2deaa6568c81bcc22a42034cc26ead1012e18ccc9afd202cd94b
|
|
4
|
+
data.tar.gz: 90399f07e1d97b4d3ce24a0e203db4febb297810451cea6e3c4a81c2e222b2fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47b89a0f367cc401a28175031f46db972f8a2756aa4757cf73864216e98a9bb6fa09b3f7e30686c24e11ca32bae0388b4d2d190be11fa953a6271c7ae67dab20
|
|
7
|
+
data.tar.gz: 464042799cda86f8ab622c1ac6de5201874749d24e548ba55b2f4d2781ea684c35baf59edeeb8f7c1a8788186b1b8e86784a86987b612043c01d1ff680030ca8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.2.1] — 2026-03-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Security: SQL comment stripping** — `rails_query` now strips MySQL-style `#` comments in addition to `--` and `/* */`
|
|
12
|
+
- **Security: Regex injection** — PerformanceIntrospector now uses `Regexp.escape` on all interpolated model/association names to prevent regex injection
|
|
13
|
+
- **Security: SearchDocs error memoization** — transient index load failures (JSON parse errors, missing file) are no longer cached permanently; subsequent calls retry instead of returning stale errors
|
|
14
|
+
- **Security: ReadLogs file parameter** — null byte sanitization + `File.basename` enforcement prevents path traversal via directory separators in file names
|
|
15
|
+
- **Security: ReadLogs redaction** — added `cookie`, `session_id`, and `_session` patterns to sensitive data redaction
|
|
16
|
+
- **Security: SearchDocs fetch size** — 2MB cap on fetched documentation content prevents memory exhaustion from oversized HTTP responses
|
|
17
|
+
- **Security: MigrationAdvisor input validation** — table and column names now validated as safe identifiers; special characters rejected with clear error messages
|
|
18
|
+
- **Cache: Fingerprinter watched paths** — added `app/components` to WATCHED_DIRS, `package.json` and `tsconfig.json` to WATCHED_FILES; component catalog and frontend stack tools now invalidate on relevant file changes
|
|
19
|
+
- **Schema: static parse skipped tables** — `parse_schema_rb` no longer leaves `current_table` pointing at a skipped table (`schema_migrations`, `ar_internal_metadata`), preventing potential nil access on subsequent column lines
|
|
20
|
+
- **Query: CSV newline escaping** — CSV format output now properly quotes cell values containing newlines and carriage returns
|
|
21
|
+
- **DependencyGraph: Mermaid node IDs** — model names starting with digits now get an `M` prefix to produce valid Mermaid syntax
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Test count: 983 → 998
|
|
25
|
+
|
|
8
26
|
## [4.2.0] — 2026-03-26
|
|
9
27
|
|
|
10
28
|
### Added
|
data/CLAUDE.md
CHANGED
data/README.md
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
[](https://registry.modelcontextprotocol.io)
|
|
9
9
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
10
10
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
11
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
**Works with:** Claude Code • Cursor • GitHub Copilot • OpenCode • Any terminal
|
|
15
15
|
|
|
16
|
-
> Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote
|
|
16
|
+
> Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote 998 tests. This gem exists because I understand Rails deeply enough to know exactly what AI agents get wrong and what context they need to get it right.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
gem "rails-ai-context", group: :development
|
data/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|---------|--------------------|
|
|
7
|
-
| 4.2.x | :white_check_mark: |
|
|
7
|
+
| 4.2.x | :white_check_mark: (4.2.1 includes security hardening) |
|
|
8
8
|
| 4.1.x | :white_check_mark: |
|
|
9
9
|
| 4.0.x | :white_check_mark: |
|
|
10
10
|
| 3.1.x | :white_check_mark: |
|
|
@@ -34,4 +34,10 @@ If you discover a security vulnerability in rails-ai-context, please report it r
|
|
|
34
34
|
- **Credential safety** — `rails_get_env` only reads `.env.example` (never `.env`), shows credential key names only (never values), and redacts secrets. `rails_get_config` exposes adapter/framework names, not connection strings.
|
|
35
35
|
- **Brakeman integration** — optional `rails_security_scan` tool runs static security analysis. Graceful degradation if not installed. Users can exclude it via `config.skip_tools = %w[rails_security_scan]`.
|
|
36
36
|
- **File size limits** — all tools enforce configurable `max_file_size` (default 5MB) to prevent memory exhaustion on large files.
|
|
37
|
-
-
|
|
37
|
+
- **SQL comment stripping** — `rails_query` strips block (`/* */`), line (`--`), and MySQL-style (`#`) comments before validation to prevent keyword hiding.
|
|
38
|
+
- **Regex interpolation safety** — all introspectors use `Regexp.escape` when interpolating model/association names into patterns to prevent regex injection.
|
|
39
|
+
- **Log redaction** — `rails_read_logs` redacts passwords, tokens, secrets, API keys, cookies, session IDs, emails, and environment variables before output.
|
|
40
|
+
- **Migration input validation** — `rails_migration_advisor` validates table and column names as safe identifiers before generating migration code.
|
|
41
|
+
- **Cache invalidation coverage** — Fingerprinter watches `app/components`, `package.json`, and `tsconfig.json` alongside models/controllers/views to prevent stale tool responses.
|
|
42
|
+
- **Fetch size limits** — `rails_search_docs` caps fetched documentation content at 2MB to prevent memory exhaustion.
|
|
43
|
+
- The gem makes outbound HTTPS requests only when `rails_search_docs` is called with `fetch: true` (to fetch Rails documentation from GitHub raw content). All other tools are offline.
|
|
@@ -12,6 +12,8 @@ module RailsAiContext
|
|
|
12
12
|
config/routes.rb
|
|
13
13
|
config/database.yml
|
|
14
14
|
Gemfile.lock
|
|
15
|
+
package.json
|
|
16
|
+
tsconfig.json
|
|
15
17
|
].freeze
|
|
16
18
|
|
|
17
19
|
WATCHED_DIRS = %w[
|
|
@@ -21,6 +23,7 @@ module RailsAiContext
|
|
|
21
23
|
app/jobs
|
|
22
24
|
app/mailers
|
|
23
25
|
app/channels
|
|
26
|
+
app/components
|
|
24
27
|
app/javascript/controllers
|
|
25
28
|
app/middleware
|
|
26
29
|
config/initializers
|
|
@@ -108,12 +108,12 @@ module RailsAiContext
|
|
|
108
108
|
model_data.each do |model|
|
|
109
109
|
model[:has_many].each do |assoc|
|
|
110
110
|
# Check if controller fetches this model's collection without includes
|
|
111
|
-
model_ref = model[:name]
|
|
111
|
+
model_ref = Regexp.escape(model[:name])
|
|
112
112
|
pattern = /#{model_ref}\.(all|where|order|limit|find_each)\b/
|
|
113
113
|
next unless content.match?(pattern)
|
|
114
114
|
|
|
115
115
|
# Check if .includes is used for this association
|
|
116
|
-
includes_pattern = /\.includes\(.*:#{assoc[:name]}/
|
|
116
|
+
includes_pattern = /\.includes\(.*:#{Regexp.escape(assoc[:name])}/
|
|
117
117
|
next if content.match?(includes_pattern)
|
|
118
118
|
|
|
119
119
|
# Check views for accessing association
|
|
@@ -142,7 +142,7 @@ module RailsAiContext
|
|
|
142
142
|
# Check if any view iterates over the association
|
|
143
143
|
Dir.glob(File.join(views_dir, "**/*.{erb,haml,slim}")).any? do |path|
|
|
144
144
|
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
145
|
-
content.match?(/\.#{association_name}\b/)
|
|
145
|
+
content.match?(/\.#{Regexp.escape(association_name)}\b/)
|
|
146
146
|
rescue
|
|
147
147
|
false
|
|
148
148
|
end
|
|
@@ -231,7 +231,7 @@ module RailsAiContext
|
|
|
231
231
|
relative = path.sub("#{root}/", "")
|
|
232
232
|
|
|
233
233
|
model_names.each do |model_name|
|
|
234
|
-
content.scan(/#{model_name}\.all\b/).each do
|
|
234
|
+
content.scan(/#{Regexp.escape(model_name)}\.all\b/).each do
|
|
235
235
|
findings << {
|
|
236
236
|
controller: relative,
|
|
237
237
|
model: model_name,
|
|
@@ -182,7 +182,10 @@ module RailsAiContext
|
|
|
182
182
|
content.each_line do |line|
|
|
183
183
|
if (match = line.match(/create_table\s+"(\w+)"/))
|
|
184
184
|
current_table = match[1]
|
|
185
|
-
|
|
185
|
+
if current_table.start_with?("ar_internal_metadata", "schema_migrations")
|
|
186
|
+
current_table = nil
|
|
187
|
+
next
|
|
188
|
+
end
|
|
186
189
|
tables[current_table] = { columns: [], indexes: [], foreign_keys: [] }
|
|
187
190
|
elsif current_table && (match = line.match(/t\.(\w+)\s+"(\w+)"/))
|
|
188
191
|
col = { name: match[2], type: match[1] }
|
|
@@ -63,11 +63,16 @@ module RailsAiContext
|
|
|
63
63
|
}
|
|
64
64
|
}.freeze
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
MODEL_TEMPLATE = MCP::ResourceTemplate.new(
|
|
67
|
+
uri_template: "rails://models/{name}",
|
|
68
|
+
name: "Model Details",
|
|
69
|
+
description: "Detailed information about a specific ActiveRecord model",
|
|
70
|
+
mime_type: "application/json"
|
|
71
|
+
).freeze
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
class << self
|
|
74
|
+
def static_resources
|
|
75
|
+
STATIC_RESOURCES.map do |uri, meta|
|
|
71
76
|
MCP::Resource.new(
|
|
72
77
|
uri: uri,
|
|
73
78
|
name: meta[:name],
|
|
@@ -75,17 +80,16 @@ module RailsAiContext
|
|
|
75
80
|
mime_type: meta[:mime_type]
|
|
76
81
|
)
|
|
77
82
|
end
|
|
83
|
+
end
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
def resource_templates
|
|
86
|
+
[ MODEL_TEMPLATE ]
|
|
87
|
+
end
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
name: "Model Details",
|
|
84
|
-
description: "Detailed information about a specific ActiveRecord model",
|
|
85
|
-
mime_type: "application/json"
|
|
86
|
-
)
|
|
89
|
+
def register(server)
|
|
90
|
+
require "json"
|
|
87
91
|
|
|
88
|
-
server.
|
|
92
|
+
server.resources = static_resources
|
|
89
93
|
|
|
90
94
|
server.resources_read_handler do |params|
|
|
91
95
|
handle_read(params)
|
|
@@ -56,7 +56,8 @@ module RailsAiContext
|
|
|
56
56
|
server = MCP::Server.new(
|
|
57
57
|
name: config.server_name,
|
|
58
58
|
version: config.server_version,
|
|
59
|
-
tools: active_tools(config) + config.custom_tools
|
|
59
|
+
tools: active_tools(config) + config.custom_tools,
|
|
60
|
+
resource_templates: Resources.resource_templates
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
Resources.register(server)
|
|
@@ -193,7 +193,10 @@ module RailsAiContext
|
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
def sanitize(name)
|
|
196
|
-
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
196
|
+
sanitized = name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
197
|
+
# Mermaid node IDs must start with a letter
|
|
198
|
+
sanitized = "M#{sanitized}" if sanitized.match?(/\A\d/)
|
|
199
|
+
sanitized
|
|
197
200
|
end
|
|
198
201
|
end
|
|
199
202
|
end
|
|
@@ -56,6 +56,15 @@ module RailsAiContext
|
|
|
56
56
|
return text_response("**Error:** `action` is required. Valid actions: #{VALID_ACTIONS.join(', ')}") if action.empty?
|
|
57
57
|
return text_response("**Error:** `table` is required (e.g., 'users', 'posts').") if table.empty?
|
|
58
58
|
|
|
59
|
+
# Validate identifier characters to produce valid migration code
|
|
60
|
+
unless table.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
61
|
+
return text_response("**Error:** Invalid table name `#{table}`. Use lowercase letters, digits, and underscores only.")
|
|
62
|
+
end
|
|
63
|
+
# create_table uses column param as a comma-separated column:type definition string
|
|
64
|
+
if action != "create_table" && column && !column.empty? && !column.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
65
|
+
return text_response("**Error:** Invalid column name `#{column}`. Use lowercase letters, digits, and underscores only.")
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
unless VALID_ACTIONS.include?(action)
|
|
60
69
|
suggestion = VALID_ACTIONS.find { |a| a.start_with?(action) || a.include?(action) }
|
|
61
70
|
hint = suggestion ? " Did you mean `#{suggestion}`?" : ""
|
|
@@ -93,7 +93,11 @@ module RailsAiContext
|
|
|
93
93
|
|
|
94
94
|
# ── SQL comment stripping ───────────────────────────────────────
|
|
95
95
|
def self.strip_sql_comments(sql)
|
|
96
|
-
sql
|
|
96
|
+
sql
|
|
97
|
+
.gsub(/\/\*.*?\*\//m, " ") # Block comments: /* ... */
|
|
98
|
+
.gsub(/--[^\n]*/, " ") # Line comments: -- ...
|
|
99
|
+
.gsub(/#[^\n]*/, " ") # MySQL-style comments: # ...
|
|
100
|
+
.squeeze(" ").strip
|
|
97
101
|
end
|
|
98
102
|
|
|
99
103
|
# ── SQL validation (Layer 1) ────────────────────────────────────
|
|
@@ -263,8 +267,8 @@ module RailsAiContext
|
|
|
263
267
|
rows.each do |row|
|
|
264
268
|
lines << row.map { |val|
|
|
265
269
|
formatted = format_cell(val)
|
|
266
|
-
# Quote values that contain commas or
|
|
267
|
-
if formatted.include?(",") || formatted.include?('"')
|
|
270
|
+
# Quote values that contain commas, quotes, or newlines
|
|
271
|
+
if formatted.include?(",") || formatted.include?('"') || formatted.include?("\n") || formatted.include?("\r")
|
|
268
272
|
"\"#{formatted.gsub('"', '""')}\""
|
|
269
273
|
else
|
|
270
274
|
formatted
|
|
@@ -52,6 +52,9 @@ module RailsAiContext
|
|
|
52
52
|
/(?<=api_key:\s)\S+/i,
|
|
53
53
|
/(?<=authorization:\s)(Bearer\s)?\S+/i,
|
|
54
54
|
/(SECRET|PRIVATE|SIGNING|ENCRYPTION)[_A-Z]*=\S+/i,
|
|
55
|
+
/(?<=cookie:\s)\S+/i,
|
|
56
|
+
/(?<=session_id=)\S+/i,
|
|
57
|
+
/(?<=_session=)\S+/i,
|
|
55
58
|
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i
|
|
56
59
|
].freeze
|
|
57
60
|
|
|
@@ -155,8 +158,9 @@ module RailsAiContext
|
|
|
155
158
|
root = Rails.root.to_s
|
|
156
159
|
|
|
157
160
|
if file_name
|
|
158
|
-
# Strip .log suffix if provided, then re-add
|
|
159
|
-
name = file_name.to_s.strip.delete_suffix(".log")
|
|
161
|
+
# Strip .log suffix if provided, then re-add; sanitize null bytes and path separators
|
|
162
|
+
name = file_name.to_s.strip.delete("\0").delete_suffix(".log")
|
|
163
|
+
name = File.basename(name) # Prevent directory traversal via slashes
|
|
160
164
|
path = File.join(root, "log", "#{name}.log")
|
|
161
165
|
else
|
|
162
166
|
path = File.join(root, "log", "#{Rails.env}.log")
|
|
@@ -105,7 +105,9 @@ module RailsAiContext
|
|
|
105
105
|
private
|
|
106
106
|
|
|
107
107
|
def load_index
|
|
108
|
-
@docs_index
|
|
108
|
+
return @docs_index if @docs_index&.dig(:topics)
|
|
109
|
+
|
|
110
|
+
result = begin
|
|
109
111
|
unless File.exist?(INDEX_PATH)
|
|
110
112
|
return { error: "Documentation index not found at #{INDEX_PATH}. The gem installation may be incomplete — reinstall rails-ai-context." }
|
|
111
113
|
end
|
|
@@ -117,6 +119,10 @@ module RailsAiContext
|
|
|
117
119
|
rescue JSON::ParserError => e
|
|
118
120
|
{ error: "Failed to parse documentation index: #{e.message}" }
|
|
119
121
|
end
|
|
122
|
+
|
|
123
|
+
# Only memoize successful results so transient failures can be retried
|
|
124
|
+
@docs_index = result if result[:topics]
|
|
125
|
+
result
|
|
120
126
|
end
|
|
121
127
|
|
|
122
128
|
def detect_rails_branch
|
|
@@ -222,9 +228,12 @@ module RailsAiContext
|
|
|
222
228
|
request = Net::HTTP::Get.new(uri)
|
|
223
229
|
response = http.request(request)
|
|
224
230
|
|
|
231
|
+
max_fetch_bytes = 2_000_000 # 2MB safety cap
|
|
225
232
|
if response.is_a?(Net::HTTPSuccess)
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
body = response.body
|
|
234
|
+
body = body.byteslice(0, max_fetch_bytes) if body.bytesize > max_fetch_bytes
|
|
235
|
+
File.write(cache_file, body)
|
|
236
|
+
body
|
|
228
237
|
else
|
|
229
238
|
"#{topic['summary']}\n→ #{url}\n_(fetch failed: HTTP #{response.code})_"
|
|
230
239
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.2.
|
|
4
|
+
version: 4.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -13,16 +13,22 @@ dependencies:
|
|
|
13
13
|
name: mcp
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '0.8'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '2.0'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
|
-
- - "
|
|
26
|
+
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '0.8'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '2.0'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: railties
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|