rails-ai-context 0.15.4 → 0.15.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df16cb513ad22c8fca7405fed9a9b111e7018184e1bd16cd08230dace76a9612
4
- data.tar.gz: c4846d86197db0fe697b21cd23ead829a4433abfe444b0a14695433a5ab3d0f0
3
+ metadata.gz: a9e71509ab3fcf2437fdd2616f8cb88140b478677abdd4ebd7cf6818eb8cead3
4
+ data.tar.gz: c6847c4a500d762665ea2493c0ee56943bfe2e601b645bf5e9ec97df81f7e495
5
5
  SHA512:
6
- metadata.gz: dd8e4f99421b6efbc1b589734c03dbf3f167ec6a2779957a357f2924a9ac3bccd3d40c5c8aca442d92e9392cfed40d517b20455f7e9a9dd2f792c543c3c13928
7
- data.tar.gz: f970c8cf22b53531143352ea4a8a90d615dbe7530a2a70eabd04dd7fd9e532119a803260681dbb431278ad6bfdc7103e82b487840514afd0c49b4c792e3ce8df
6
+ metadata.gz: fb13a050a3e8200d1bac5f8d0d59bc1a67469a3fbe904682824541430f9f10755197fa3bd17858c08a2eedcc89f71c9da2dfffea8a8bcd0c177784f85ef511c6
7
+ data.tar.gz: 4e05a8e22c395b4091d336268aa3e3994f416b11d174e63d922b5979b46695a222176a0871253ddd940638ead977e2917d14998618a91c25ad4a6f891eca9694
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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
+ ## [0.15.5] - 2026-03-22
9
+
10
+ ### Fixed
11
+
12
+ - **ERB validation** — now catches missing `<% end %>` by compiling ERB to Ruby then syntax-checking the result (was only checking ERB tag syntax).
13
+ - **Controller namespace format** — accepts both `Bonus::CrisesController` and `bonus/crises` (cross-tool consistency).
14
+ - **Layouts discoverable** — `controller:"layouts"` now works in view tool.
15
+ - **Validate error detail** — Ruby shows up to 5 error lines, JS shows 3 (was truncated to 1).
16
+ - **Invalid/empty regex** — early validation with clear error messages instead of silent fail.
17
+ - **Route count accuracy** — shows filtered count when `app_only:true`, not unfiltered total.
18
+ - **Namespace test lookup** — supports `bonus/crises` format and flat test directories.
19
+ - **Empty inputs** — `near:""` in edit_context and `pattern:""` in search return helpful errors.
20
+
8
21
  ## [0.15.4] - 2026-03-22
9
22
 
10
23
  ### Fixed
data/README.md CHANGED
@@ -302,7 +302,7 @@ end
302
302
  | `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
303
303
  | `excluded_models` | internal Rails models | Models to skip during introspection |
304
304
  | `excluded_paths` | `node_modules tmp log vendor .git` | Paths excluded from code search |
305
- | `sensitive_patterns` | `.env .env.* *.key *.pem config/master.key config/credentials.yml.enc` | File patterns blocked from search and read tools |
305
+ | `sensitive_patterns` | `.env .env.* config/master.key config/credentials.yml.enc config/credentials/*.yml.enc *.pem *.key` | File patterns blocked from search and read tools |
306
306
  | `auto_mount` | `false` | Auto-mount HTTP MCP endpoint |
307
307
  | `http_path` | `"/mcp"` | HTTP endpoint path |
308
308
  | `http_port` | `6029` | HTTP server port |
data/SECURITY.md CHANGED
@@ -5,7 +5,7 @@
5
5
  | Version | Supported |
6
6
  |---------|--------------------|
7
7
  | 0.15.x | :white_check_mark: |
8
- | < 0.14 | :x: |
8
+ | < 0.15 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
data/docs/GUIDE.md CHANGED
@@ -146,22 +146,23 @@ end
146
146
  | `app/models/AGENTS.md` | Model reference | Auto-loaded by OpenCode when reading files in `app/models/`. |
147
147
  | `app/controllers/AGENTS.md` | Controller reference | Auto-loaded by OpenCode when reading files in `app/controllers/`. |
148
148
 
149
- ### Cursor (6 files)
149
+ ### Cursor (5 files)
150
150
 
151
151
  | File | Purpose | Notes |
152
152
  |------|---------|-------|
153
153
  | `.cursor/rules/rails-project.mdc` | Project overview | `alwaysApply: true` — loaded in every conversation. |
154
154
  | `.cursor/rules/rails-models.mdc` | Model reference | `globs: app/models/**/*.rb` — auto-attaches when editing models. |
155
155
  | `.cursor/rules/rails-controllers.mdc` | Controller reference | `globs: app/controllers/**/*.rb` — auto-attaches when editing controllers. |
156
- | `.cursor/rules/rails-ui-patterns.mdc` | UI patterns and design tokens | `alwaysApply: true` — loaded in every conversation. |
156
+ | `.cursor/rules/rails-ui-patterns.mdc` | UI patterns and design tokens | `globs: app/views/**/*.erb` — loaded when editing views. |
157
157
  | `.cursor/rules/rails-mcp-tools.mdc` | MCP tool reference | `alwaysApply: true` — always available. |
158
158
 
159
- ### Windsurf (3 files)
159
+ ### Windsurf (4 files)
160
160
 
161
161
  | File | Purpose | Notes |
162
162
  |------|---------|-------|
163
163
  | `.windsurfrules` | Main context file | Hard-capped at 5,800 chars (Windsurf's 6K limit). Truncated silently if exceeded. |
164
164
  | `.windsurf/rules/rails-context.md` | Project overview | New Windsurf rules format. |
165
+ | `.windsurf/rules/rails-ui-patterns.md` | UI patterns and design tokens | Loaded when editing views. |
165
166
  | `.windsurf/rules/rails-mcp-tools.md` | MCP tool reference | Compact — respects 6K per-file limit. |
166
167
 
167
168
  ### GitHub Copilot (6 files)
@@ -172,7 +173,7 @@ end
172
173
  | `.github/instructions/rails-models.instructions.md` | Model context | `applyTo: app/models/**/*.rb` — loaded when editing models. |
173
174
  | `.github/instructions/rails-controllers.instructions.md` | Controller context | `applyTo: app/controllers/**/*.rb` — loaded when editing controllers. |
174
175
  | `.github/instructions/rails-context.instructions.md` | Project context and conventions | `applyTo: **/*` — loaded everywhere. |
175
- | `.github/instructions/rails-ui-patterns.instructions.md` | UI patterns and design tokens | `applyTo: **/*` — loaded everywhere. |
176
+ | `.github/instructions/rails-ui-patterns.instructions.md` | UI patterns and design tokens | `applyTo: app/views/**/*.erb` — loaded when editing views. |
176
177
  | `.github/instructions/rails-mcp-tools.instructions.md` | MCP tool reference | `applyTo: **/*` — loaded everywhere. |
177
178
 
178
179
  ### Generic (1 file)
@@ -359,8 +360,11 @@ Returns controller details: actions, filters, strong params, concerns.
359
360
 
360
361
  | Param | Type | Description |
361
362
  |-------|------|-------------|
362
- | `controller` | string | Specific controller name (e.g. `UsersController`). Case-insensitive. |
363
+ | `controller` | string | Specific controller name (e.g. `UsersController`, `cooks`, `bonus/crises`). Case-insensitive, flexible format. |
364
+ | `action` | string | Specific action (e.g. `index`). Requires controller. Returns source code with applicable filters. |
363
365
  | `detail` | string | `summary` / `standard` (default) / `full`. Ignored when controller is specified. |
366
+ | `limit` | integer | Max controllers to return when listing. Default: 50. |
367
+ | `offset` | integer | Skip this many controllers for pagination. Default: 0. |
364
368
 
365
369
  **Examples:**
366
370
 
@@ -382,7 +386,7 @@ rails_get_controllers(detail: "full")
382
386
 
383
387
  Returns application configuration. No parameters.
384
388
 
385
- **Returns:** cache store, session store, timezone, middleware stack, initializers, credentials keys, current attributes.
389
+ **Returns:** cache store, session store, timezone, queue adapter, mailer settings, custom middleware, initializers, current attributes.
386
390
 
387
391
  ```
388
392
  rails_get_config()
@@ -391,20 +395,35 @@ rails_get_config()
391
395
 
392
396
  ### rails_get_test_info
393
397
 
394
- Returns test infrastructure details. No parameters.
398
+ Returns test infrastructure details. Optionally filter by model or controller to find existing tests.
395
399
 
396
- **Returns:** test framework (rspec/minitest), factories/fixtures with locations and counts, system tests, CI config, coverage tool, test helpers.
400
+ **Parameters:**
401
+
402
+ | Param | Type | Description |
403
+ |-------|------|-------------|
404
+ | `model` | string | Show tests for a specific model (e.g. `User`). |
405
+ | `controller` | string | Show tests for a specific controller (e.g. `Cooks`). |
406
+ | `detail` | string | `summary` / `standard` (default) / `full`. |
397
407
 
398
408
  ```
399
409
  rails_get_test_info()
400
- → Framework: rspec, Factories: spec/factories (12 files), CI: .github/workflows/ci.yml, ...
410
+ → Framework: rspec, Factories: spec/factories (12 files), CI: .github/workflows/ci.yml
411
+
412
+ rails_get_test_info(model: "User")
413
+ → Shows spec/models/user_spec.rb test names (summary/standard) or full source (full)
401
414
  ```
402
415
 
403
416
  ### rails_get_gems
404
417
 
405
- Returns notable gems categorized by function. No parameters.
418
+ Returns notable gems categorized by function.
419
+
420
+ **Parameters:**
421
+
422
+ | Param | Type | Description |
423
+ |-------|------|-------------|
424
+ | `category` | string | Filter by category: `auth`, `jobs`, `frontend`, `api`, `database`, `files`, `testing`, `deploy`, `all` (default). |
406
425
 
407
- **Returns:** 70+ recognized gems grouped by category (auth, background_jobs, admin, monitoring, search, pagination, etc.) with versions and descriptions.
426
+ **Returns:** Notable gems grouped by category with descriptions.
408
427
 
409
428
  ```
410
429
  rails_get_gems()
@@ -48,13 +48,19 @@ module RailsAiContext
48
48
  # Specific controller — always full detail (searches ALL controllers including framework)
49
49
  # Flexible matching: "cooks", "CooksController", "cookscontroller" all work
50
50
  if controller
51
- normalized = controller.downcase.delete_suffix("controller")
51
+ # Accept multiple formats: "CooksController", "cooks", "bonus/crises", "Bonus::CrisesController"
52
+ normalized = controller.downcase.delete_suffix("controller").tr("/", "::")
52
53
  key = controllers.keys.find { |k|
53
54
  kd = k.downcase
54
- kd == controller.downcase || kd.delete_suffix("controller") == normalized
55
+ kd == controller.downcase ||
56
+ kd.delete_suffix("controller") == normalized ||
57
+ kd.delete_suffix("controller").tr("::", "/") == controller.downcase.delete_suffix("controller")
55
58
  } || controller
56
59
  info = controllers[key]
57
- return text_response("Controller '#{controller}' not found. Available: #{app_controller_names.join(', ')}") unless info
60
+ unless info
61
+ available = app_controller_names.any? ? "Available: #{app_controller_names.join(', ')}" : "No controllers discovered."
62
+ return text_response("Controller '#{controller}' not found. #{available}")
63
+ end
58
64
  return text_response("Error inspecting #{key}: #{info[:error]}") if info[:error]
59
65
 
60
66
  # Specific action — return source code
@@ -32,6 +32,11 @@ module RailsAiContext
32
32
  SENSITIVE_PATTERNS = nil # uses configuration.sensitive_patterns
33
33
 
34
34
  def self.call(file:, near:, context_lines: 5, server_context: nil)
35
+ # Reject empty search term
36
+ if near.nil? || near.strip.empty?
37
+ return text_response("The `near` parameter is required. Provide a method name, keyword, or string to find.")
38
+ end
39
+
35
40
  full_path = Rails.root.join(file)
36
41
 
37
42
  # Block access to sensitive files (secrets, keys, credentials)
@@ -65,6 +65,7 @@ module RailsAiContext
65
65
 
66
66
  # Combine PUT/PATCH duplicates (Rails generates both for update routes)
67
67
  by_controller = by_controller.transform_values { |actions| dedupe_put_patch(actions) }
68
+ filtered_total = by_controller.values.sum(&:size)
68
69
 
69
70
  case detail
70
71
  when "summary"
@@ -72,7 +73,7 @@ module RailsAiContext
72
73
  app_routes = controller ? by_controller : by_controller.reject { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
73
74
  framework_routes = controller ? {} : by_controller.select { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
74
75
 
75
- lines = [ "# Routes Summary (#{routes[:total_routes]} total)", "" ]
76
+ lines = [ "# Routes Summary (#{filtered_total} routes)", "" ]
76
77
 
77
78
  # Group sibling routes with identical verb patterns (e.g., bonus/*)
78
79
  grouped = app_routes.keys.sort.group_by do |ctrl|
@@ -118,7 +119,7 @@ module RailsAiContext
118
119
  app_routes = controller ? by_controller : by_controller.reject { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
119
120
  framework_routes = controller ? {} : by_controller.select { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
120
121
 
121
- lines = [ "# Routes (#{routes[:total_routes]} total)", "" ]
122
+ lines = [ "# Routes (#{filtered_total} routes)", "" ]
122
123
  count = 0
123
124
 
124
125
  # Group identical sibling route sets (e.g. bonus/*)
@@ -181,7 +182,7 @@ module RailsAiContext
181
182
  when "full"
182
183
  # Existing full table behavior
183
184
  limit ||= 200
184
- lines = [ "# Routes Full Detail (#{routes[:total_routes]} total)", "" ]
185
+ lines = [ "# Routes Full Detail (#{filtered_total} routes)", "" ]
185
186
  lines << "| Verb | Path | Controller#Action | Name |"
186
187
  lines << "|------|------|-------------------|------|"
187
188
  count = 0
@@ -122,7 +122,8 @@ module RailsAiContext
122
122
  MAX_TEST_FILE_SIZE = 500_000 # 500KB safety limit
123
123
 
124
124
  private_class_method def self.find_test_file(name, type, detail = "full")
125
- snake = name.to_s.underscore.sub(/_controller$/, "")
125
+ # Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises"
126
+ snake = name.to_s.tr("/", "::").underscore.sub(/_controller$/, "")
126
127
  candidates = case type
127
128
  when :model
128
129
  [
@@ -133,7 +134,9 @@ module RailsAiContext
133
134
  [
134
135
  "spec/controllers/#{snake}_controller_spec.rb",
135
136
  "spec/requests/#{snake}_spec.rb",
136
- "test/controllers/#{snake}_controller_test.rb"
137
+ "test/controllers/#{snake}_controller_test.rb",
138
+ # Also try without namespace prefix for flat test dirs
139
+ "spec/requests/#{snake.split('/').last}_spec.rb"
137
140
  ]
138
141
  end
139
142
 
@@ -43,6 +43,11 @@ module RailsAiContext
43
43
  end
44
44
 
45
45
  # Filter by controller (also checks partials for directories like "shared/")
46
+ # Special case: "layouts" reads from app/views/layouts/ (excluded from normal listing)
47
+ if controller&.downcase == "layouts"
48
+ return list_layouts(detail)
49
+ end
50
+
46
51
  if controller
47
52
  ctrl_lower = controller.downcase
48
53
  filtered_templates = templates.select { |k, _| k.downcase.start_with?(ctrl_lower + "/") }
@@ -135,6 +140,27 @@ module RailsAiContext
135
140
  end
136
141
  end
137
142
 
143
+ private_class_method def self.list_layouts(detail)
144
+ layouts_dir = Rails.root.join("app", "views", "layouts")
145
+ return text_response("No app/views/layouts/ directory found.") unless Dir.exist?(layouts_dir)
146
+
147
+ files = Dir.glob(File.join(layouts_dir, "*")).reject { |f| File.directory?(f) }.sort
148
+ return text_response("No layout files found.") if files.empty?
149
+
150
+ lines = [ "# Layouts (#{files.size} files)", "" ]
151
+ files.each do |path|
152
+ relative = "layouts/#{File.basename(path)}"
153
+ if detail == "full"
154
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue "(error reading)"
155
+ lines << "## #{relative}" << "```erb" << strip_svg(content) << "```" << ""
156
+ else
157
+ line_count = (File.readlines(path).size rescue 0)
158
+ lines << "- #{relative} (#{line_count} lines)"
159
+ end
160
+ end
161
+ text_response(lines.join("\n"))
162
+ end
163
+
138
164
  MAX_FILE_SIZE = 2_000_000 # 2MB safety limit
139
165
 
140
166
  private_class_method def self.read_view_file(path)
@@ -41,6 +41,18 @@ module RailsAiContext
41
41
  def self.call(pattern:, path: nil, file_type: nil, max_results: 30, context_lines: 0, server_context: nil)
42
42
  root = Rails.root.to_s
43
43
 
44
+ # Reject empty or whitespace-only patterns
45
+ if pattern.nil? || pattern.strip.empty?
46
+ return text_response("Pattern is required. Provide a search term or regex.")
47
+ end
48
+
49
+ # Validate regex syntax early
50
+ begin
51
+ Regexp.new(pattern, timeout: 1)
52
+ rescue RegexpError => e
53
+ return text_response("Invalid regex pattern: #{e.message}")
54
+ end
55
+
44
56
  # Validate file_type to prevent injection
45
57
  if file_type && !file_type.match?(/\A[a-zA-Z0-9]+\z/)
46
58
  return text_response("Invalid file_type: must contain only alphanumeric characters.")
@@ -93,20 +93,37 @@ module RailsAiContext
93
93
  if status.success?
94
94
  [ true, nil ]
95
95
  else
96
- error = result.lines.reject { |l| l.strip.empty? }.first&.strip || "syntax error"
97
- error = error.sub(full_path.to_s, File.basename(full_path.to_s))
96
+ # Show up to 5 non-empty error lines for full context (ruby -c gives multi-line errors)
97
+ error_lines = result.lines
98
+ .reject { |l| l.strip.empty? || l.include?("Syntax OK") }
99
+ .first(5)
100
+ .map { |l| l.strip.sub(full_path.to_s, File.basename(full_path.to_s)) }
101
+ error = error_lines.any? ? error_lines.join("\n") : "syntax error"
98
102
  [ false, error ]
99
103
  end
100
104
  end
101
105
 
102
- # Validate ERB syntax by compiling the template (no shell uses Open3 array form)
106
+ # Validate ERB by compiling to Ruby source then syntax-checking the result.
107
+ # ERB.new(...).src only validates ERB tag syntax — it does NOT catch missing <% end %>.
108
+ # So we compile to Ruby, then run ruby -c on the compiled output to catch block mismatches.
103
109
  private_class_method def self.validate_erb(full_path)
104
- script = "require 'erb'; ERB.new(File.read(ARGV[0])).src"
105
- result, status = Open3.capture2e("ruby", "-e", script, full_path.to_s)
106
- if status.success?
110
+ # Step 1: Compile ERB to Ruby source
111
+ compile_script = "require 'erb'; print ERB.new(File.read(ARGV[0])).src"
112
+ compiled, compile_status = Open3.capture2e("ruby", "-e", compile_script, full_path.to_s)
113
+
114
+ unless compile_status.success?
115
+ error = compiled.lines.reject { |l| l.strip.empty? }.first&.strip || "ERB syntax error"
116
+ return [ false, error ]
117
+ end
118
+
119
+ # Step 2: Syntax-check the compiled Ruby to catch missing end, unclosed blocks, etc.
120
+ check_result, check_status = Open3.capture2e("ruby", "-c", "-", stdin_data: compiled)
121
+ if check_status.success?
107
122
  [ true, nil ]
108
123
  else
109
- error = result.lines.reject { |l| l.strip.empty? }.first&.strip || "ERB syntax error"
124
+ error = check_result.lines.reject { |l| l.strip.empty? || l.include?("Syntax OK") }.first&.strip || "ERB error"
125
+ # Make the error more helpful — it's from compiled source, translate back
126
+ error = error.sub(/^-:/, "compiled ERB line ")
110
127
  [ false, error ]
111
128
  end
112
129
  end
@@ -120,8 +137,12 @@ module RailsAiContext
120
137
  if status.success?
121
138
  [ true, nil ]
122
139
  else
123
- error = result.lines.reject { |l| l.strip.empty? }.first&.strip || "syntax error"
124
- error = error.sub(full_path.to_s, File.basename(full_path.to_s))
140
+ # Show up to 3 non-empty error lines for context
141
+ error_lines = result.lines
142
+ .reject { |l| l.strip.empty? }
143
+ .first(3)
144
+ .map { |l| l.strip.sub(full_path.to_s, File.basename(full_path.to_s)) }
145
+ error = error_lines.any? ? error_lines.join("\n") : "syntax error"
125
146
  [ false, error ]
126
147
  end
127
148
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "0.15.4"
4
+ VERSION = "0.15.5"
5
5
  end
data/server.json CHANGED
@@ -7,11 +7,11 @@
7
7
  "url": "https://github.com/crisnahine/rails-ai-context",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.15.2",
10
+ "version": "0.15.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
14
- "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.2/rails-ai-context-mcp.mcpb",
14
+ "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.4/rails-ai-context-mcp.mcpb",
15
15
  "fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
16
16
  "transport": {
17
17
  "type": "stdio"
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: 0.15.4
4
+ version: 0.15.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine