rails-ai-context 3.0.0 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29cd675e5f63893551b3ca87869bc6bca2335a5a2fe15ea38e54ec11dab6329a
4
- data.tar.gz: 4c2a0fe1003abd939f1ea005cf1aefac1055672dbe80c9fb771937acec1bde24
3
+ metadata.gz: a6bdb0dcbde3c7abfab69bee14e661afc8a260fe3d00faa99ecc4becbc58a19b
4
+ data.tar.gz: deacfa10c6c188d0482fc82199f085b4f13cbf24b0cf06e0977074ecf67864d8
5
5
  SHA512:
6
- metadata.gz: 31a7796c31b70f9e3d667d4b961d00cd316a6bd365720fff32b9d922d7faa1febdbcfad00adcba0ff7d625af510e80ff8a1b596dba504543e0f1e72ce65022af
7
- data.tar.gz: 552c6388e3ea2648f400d2b948eefd25e325e4ec67a896b7917dee61d6bd37032df0a5d1c5e03c1dd6dbe66fbf6b21e2e38d725344b4438b98d23c0d5141d6f4
6
+ metadata.gz: bf049844c52cd8b88371a352907b08c9863668424ae37f9393d72c7210501c21fcc2ea3df797e59184b603b98e723f0bd07ebb4e36503852344c27d96970a308
7
+ data.tar.gz: ab5cf1f7f4031d605db8f076da5544ede9c1aa48d426dc57482a45551d46741c687caaaf0e8722932d8d982c6a9af5c86f425e117a80bcb88b36d2d1d761040f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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
+ ## [3.1.0] - 2026-03-26
9
+
10
+ ### Fixed
11
+
12
+ - **Consistent input normalization across all tools** — AI agents and humans can now use any casing or format and tools resolve correctly:
13
+ - `model=brand_profile` (snake_case) now resolves to `BrandProfile` via `.underscore` comparison in `get_model_details`.
14
+ - `table=Cook` (model name) now resolves to `cooks` table via `.underscore.pluralize` normalization in `get_schema`.
15
+ - `controller=CooksController` now works in `get_view` and `get_routes` — both strip `Controller`/`_controller` suffix consistently, matching `get_controllers` behavior.
16
+ - `controller=cooks_controller` no longer leaves a trailing underscore in route matching.
17
+ - `stimulus=CookStatus` (PascalCase) now resolves to `cook_status` via `.underscore` conversion in `get_stimulus`.
18
+ - `partial=_status_badge` (underscore-prefixed, no directory) now searches recursively across all view directories in `get_partial_interface`.
19
+ - `model=cooks` (plural) now tries `.singularize` for test file lookup in `get_test_info`.
20
+ - **Smarter fuzzy matching** — `BaseTool.find_closest_match` now prefers shortest substring match (so `Cook` suggests `cooks`, not `cook_comments`) and supports underscore/classify variant matching.
21
+ - **File path suggestions in validate** — `files=["cook.rb"]` now suggests `app/models/cook.rb` when the file isn't found at the given path.
22
+ - **Empty parameter validation** — `edit_context` now returns friendly messages for empty `file` or `near` parameters instead of hard errors.
23
+
8
24
  ## [3.0.0] - 2026-03-26
9
25
 
10
26
  ### Removed
@@ -13,17 +29,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
29
 
14
30
  ### Added
15
31
 
16
- - **CLI tool support** — all 25 MCP tools can now be run from the terminal. Rake: `rails 'ai:tool[schema]' table=users detail=full`. Thor CLI: `rails-ai-context tool schema --table users --detail full`. `rails ai:tool` (no args) lists all tools. `--help` shows per-tool help auto-generated from input_schema. `--json` / `JSON=1` for JSON envelope output. Tool name resolution: `schema` → `get_schema` → `rails_get_schema`.
17
- - **`tool_mode` config** — new `config.tool_mode` option: `:mcp` (default, MCP primary + CLI fallback) or `:cli` (CLI only, no MCP server needed). Selected during `rails generate rails_ai_context:install`.
18
- - **ToolGuideHelper** — shared serializer module for tool reference sections, rendering MCP or CLI syntax based on `tool_mode`. All 5 serializers updated to use it.
19
- - **ToolRunner** — `lib/rails_ai_context/cli/tool_runner.rb` handles CLI tool execution with argument parsing, tool name resolution, help generation, and JSON output formatting.
32
+ - **CLI tool support** — all 25 MCP tools can now be run from the terminal: `rails 'ai:tool[schema]' table=users detail=full`. Also via Thor CLI: `rails-ai-context tool schema --table users`. `rails ai:tool` lists all tools. `--help` shows per-tool help auto-generated from input_schema. `--json` / `JSON=1` for JSON envelope. Tool name resolution: `schema` → `get_schema` → `rails_get_schema`.
33
+ - **`tool_mode` config** — `:mcp` (default, MCP primary + CLI fallback) or `:cli` (CLI only, no MCP server needed). Selected during install and first `rails ai:context` run.
34
+ - **ToolRunner** — `lib/rails_ai_context/cli/tool_runner.rb` handles CLI tool execution: arg parsing, type coercion from input_schema, required param validation, enum checking, fuzzy tool name suggestions on typos.
35
+ - **ToolGuideHelper** — shared serializer module renders tool reference sections with MCP or CLI syntax based on `tool_mode`, with MANDATORY enforcement + CLI escape hatch. 3-column tool table (MCP | CLI | description).
36
+ - **Copilot `excludeAgent`** — MCP tools instruction file uses `excludeAgent: "code-review"` (code review can't invoke MCP tools, saves 4K char budget).
37
+ - **`.mcp.json` auto-create** — `rails ai:context` automatically creates `.mcp.json` when `tool_mode` is `:mcp` and the file doesn't exist. Existing apps upgrading to v3.0.0 get it without re-running the install generator.
38
+ - **Full config initializer** — generated initializer documents every configuration option organized by section (AI Tools, Introspection, Models & Filtering, MCP Server, File Size Limits, Extensibility, Security, Search).
39
+ - **Cursor MDC compliance spec** — 26 tests validating MDC format: frontmatter fields, rule types, glob syntax, line limits.
40
+ - **Copilot compliance spec** — 25 tests validating instruction format: applyTo, excludeAgent, file naming, content quality.
20
41
 
21
42
  ### Changed
22
43
 
23
44
  - Serializer count reduced from 6 to 5 (Claude, Cursor, Copilot, OpenCode, JSON).
24
- - Install generator AI tool selection renumbered (4 options instead of 5).
25
- - Install generator now includes MCP opt-in step for `tool_mode` selection.
26
- - All documentation, rake tasks, CLI, and configuration updated to reflect Windsurf removal.
45
+ - Install generator renumbered (4 AI tool options instead of 5) + MCP opt-in step.
46
+ - Cursor glob-based rules no longer combine `globs` + `description` (pure Type 2 auto-attach per Cursor best practices).
47
+ - MCP tool instructions use MANDATORY enforcement with CLI escape hatch — AI agents use tools when available, fall back to CLI or file reading when not.
48
+ - All CLI examples use zsh-safe quoting: `rails 'ai:tool[X]'` (brackets are glob patterns in zsh).
49
+ - README rewritten with real-world workflow examples, categorized tool table, MCP vs CLI showcase.
27
50
 
28
51
  ## [2.0.5] - 2026-03-25
29
52
 
data/CLAUDE.md CHANGED
@@ -11,7 +11,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
11
11
  - `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
12
12
  - `lib/rails_ai_context/tools/` — 25 MCP tools using the official mcp SDK
13
13
  - `lib/rails_ai_context/cli/` — CLI tool runner (`tool_runner.rb`) — executes MCP tools from rake/Thor
14
- - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper)
14
+ - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper, design_system_helper, stack_overview_helper)
15
15
  - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
16
16
  - `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
17
17
  - `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
@@ -51,7 +51,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
51
51
  ## Testing
52
52
 
53
53
  ```bash
54
- bundle exec rspec # Run specs (653 examples)
54
+ bundle exec rspec # Run specs (681 examples)
55
55
  bundle exec rubocop # Lint
56
56
  ```
57
57
 
data/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
 
10
10
  **Works with:** Claude Code • Cursor • GitHub Copilot • OpenCode • Any terminal
11
11
 
12
+ > 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 681 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.
13
+
12
14
  ```bash
13
15
  gem "rails-ai-context", group: :development
14
16
  rails generate rails_ai_context:install
@@ -16,6 +18,8 @@ rails generate rails_ai_context:install
16
18
 
17
19
  That's it. Your AI now has 25 tools that understand your entire Rails app — via MCP server or CLI. Zero config.
18
20
 
21
+ > **[Full Guide →](docs/GUIDE.md)** — every command, every parameter, every configuration option.
22
+
19
23
  ---
20
24
 
21
25
  ## Two ways to use it
@@ -336,7 +340,7 @@ end
336
340
  ```bash
337
341
  git clone https://github.com/crisnahine/rails-ai-context.git
338
342
  cd rails-ai-context && bundle install
339
- bundle exec rspec # 653 examples
343
+ bundle exec rspec # 681 examples
340
344
  bundle exec rubocop # Lint
341
345
  ```
342
346
 
@@ -219,27 +219,37 @@ module RailsAiContext
219
219
  end
220
220
 
221
221
  # Validate kwargs against the tool's input_schema.
222
+ # For missing required params: strip empty values so the tool's own guards
223
+ # can return a friendly response (matching MCP behavior).
224
+ # For invalid enums: strip the bad value and let the tool use its default.
222
225
  def validate_kwargs!(kwargs, schema)
223
226
  properties = schema[:properties] || {}
224
227
  required = (schema[:required] || []).map(&:to_s)
225
228
 
226
- # Check required params
229
+ # For required params with empty-string values, keep the key but set to nil
230
+ # so the tool's own guards can return friendly "parameter is required" messages
231
+ # (matching MCP behavior). We keep the key to avoid Ruby ArgumentError on
232
+ # required keyword arguments.
227
233
  required.each do |param|
228
- unless kwargs.key?(param.to_sym) && !kwargs[param.to_sym].nil? && kwargs[param.to_sym].to_s != ""
229
- raise InvalidArgumentError,
230
- "Missing required parameter '#{param}' for #{tool_class.tool_name}.\n" \
231
- "Run: rails-ai-context tool #{self.class.short_name(tool_class.tool_name)} --help"
234
+ if kwargs.key?(param.to_sym) && kwargs[param.to_sym].to_s.strip == ""
235
+ kwargs[param.to_sym] = nil
236
+ elsif !kwargs.key?(param.to_sym)
237
+ kwargs[param.to_sym] = nil
232
238
  end
233
239
  end
234
240
 
235
- # Check enum constraints
241
+ # Check enum constraints — downcase before comparing for case-insensitive match.
242
+ # If invalid, strip the param so the tool uses its default behavior.
236
243
  kwargs.each do |key, value|
237
244
  prop = properties[key]
238
245
  next unless prop&.dig(:enum)
239
- unless prop[:enum].include?(value.to_s)
240
- raise InvalidArgumentError,
241
- "Invalid value '#{value}' for --#{key.to_s.tr('_', '-')}. " \
242
- "Must be one of: #{prop[:enum].join(', ')}"
246
+
247
+ # Try case-insensitive match first
248
+ matched = prop[:enum].find { |e| e.to_s.downcase == value.to_s.downcase }
249
+ if matched
250
+ kwargs[key] = matched
251
+ else
252
+ kwargs.delete(key)
243
253
  end
244
254
  end
245
255
  end
@@ -65,13 +65,24 @@ module RailsAiContext
65
65
  text_response(lines.join("\n"))
66
66
  end
67
67
 
68
- # Simple fuzzy match: find the closest available name by substring or edit distance
68
+ # Fuzzy match: find the closest available name by exact, underscore, substring, or prefix
69
69
  def find_closest_match(input, available)
70
70
  return nil if available.empty?
71
71
  downcased = input.downcase
72
- # Exact substring match first
73
- exact = available.find { |a| a.downcase.include?(downcased) || downcased.include?(a.downcase) }
72
+ underscored = input.underscore.downcase
73
+
74
+ # Exact case-insensitive match (including underscore/classify variants)
75
+ exact = available.find do |a|
76
+ a_down = a.downcase
77
+ a_under = a.underscore.downcase
78
+ a_down == downcased || a_under == underscored || a_down == underscored || a_under == downcased
79
+ end
74
80
  return exact if exact
81
+
82
+ # Substring match — prefer shortest (most specific) to avoid cook → cook_comments
83
+ substring_matches = available.select { |a| a.downcase.include?(downcased) || downcased.include?(a.downcase) }
84
+ return substring_matches.min_by(&:length) if substring_matches.any?
85
+
75
86
  # Prefix match
76
87
  available.find { |a| a.downcase.start_with?(downcased[0..2]) }
77
88
  end
@@ -36,7 +36,11 @@ module RailsAiContext
36
36
  SENSITIVE_PATTERNS = nil # uses configuration.sensitive_patterns
37
37
 
38
38
  def self.call(file:, near:, context_lines: 5, server_context: nil)
39
- # Reject empty search term
39
+ # Reject empty parameters
40
+ if file.nil? || file.strip.empty?
41
+ return text_response("The `file` parameter is required. Provide a path relative to Rails root (e.g. 'app/models/cook.rb').")
42
+ end
43
+
40
44
  if near.nil? || near.strip.empty?
41
45
  return text_response("The `near` parameter is required. Provide a method name, keyword, or string to find.")
42
46
  end
@@ -40,7 +40,8 @@ module RailsAiContext
40
40
  # Specific model — always full detail (strip whitespace for fuzzy input)
41
41
  if model
42
42
  model = model.strip
43
- key = models.keys.find { |k| k.downcase == model.downcase } || model
43
+ model_under = model.underscore
44
+ key = models.keys.find { |k| k.downcase == model.downcase || k.underscore == model_under } || model
44
45
  data = models[key]
45
46
  unless data
46
47
  return not_found_response("Model", model, models.keys.sort,
@@ -201,6 +201,7 @@ module RailsAiContext
201
201
 
202
202
  # Resolve a partial reference to an actual file path on disk.
203
203
  # Handles both underscore-prefixed filenames and non-prefixed input.
204
+ # Falls back to recursive search when no directory is specified.
204
205
  private_class_method def self.resolve_partial_path(views_dir, partial)
205
206
  # Normalize: strip leading underscore from basename if provided
206
207
  parts = partial.split("/")
@@ -211,10 +212,11 @@ module RailsAiContext
211
212
  prefixed_basename = basename.start_with?("_") ? basename : "_#{basename}"
212
213
  unprefixed_basename = basename.delete_prefix("_")
213
214
 
215
+ extensions = %w[.html.erb .erb .html.haml .haml .html.slim .slim]
214
216
  candidates = []
215
217
 
216
218
  # Try prefixed name with various extensions
217
- %w[.html.erb .erb .html.haml .haml .html.slim .slim].each do |ext|
219
+ extensions.each do |ext|
218
220
  candidates << File.join(views_dir, *dir_parts, "#{prefixed_basename}#{ext}")
219
221
  candidates << File.join(views_dir, *dir_parts, "#{unprefixed_basename}#{ext}")
220
222
  end
@@ -223,6 +225,19 @@ module RailsAiContext
223
225
  candidates << File.join(views_dir, partial)
224
226
 
225
227
  found = candidates.find { |c| File.exist?(c) }
228
+
229
+ # Fallback: if no directory was specified and direct lookup failed,
230
+ # search recursively for the partial across all view directories
231
+ if found.nil? && dir_parts.empty?
232
+ extensions.each do |ext|
233
+ matches = Dir.glob(File.join(views_dir, "**", "#{prefixed_basename}#{ext}"))
234
+ if matches.any?
235
+ found = matches.first
236
+ break
237
+ end
238
+ end
239
+ end
240
+
226
241
  return nil unless found
227
242
 
228
243
  # Path traversal protection
@@ -53,10 +53,11 @@ module RailsAiContext
53
53
  by_controller = by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
54
54
  end
55
55
 
56
- # Filter by controller — accepts both slash and :: notation
56
+ # Filter by controller — accepts "cooks", "CooksController", "cooks_controller", "Api::V1::Posts"
57
57
  if controller
58
- normalized = controller.downcase.tr("::", "/").delete_suffix("controller")
59
- filtered = by_controller.select { |k, _| k.downcase.include?(normalized) }
58
+ normalized = controller.underscore.delete_suffix("_controller")
59
+ normalized_alt = controller.downcase.delete_suffix("_controller").delete_suffix("controller")
60
+ filtered = by_controller.select { |k, _| k.downcase.include?(normalized) || k.downcase.include?(normalized_alt) }
60
61
  return text_response("No routes for '#{controller}'. Controllers: #{by_controller.keys.sort.join(', ')}") if filtered.empty?
61
62
  by_controller = filtered
62
63
  end
@@ -56,9 +56,14 @@ module RailsAiContext
56
56
  offset = [ offset.to_i, 0 ].max
57
57
  limit = [ limit.to_i, 0 ].max if limit && limit.to_i < 0
58
58
 
59
- # Single table — case-insensitive lookup
59
+ # Single table — case-insensitive lookup with model name normalization
60
+ # Accepts: "users", "Users", "User" (model name → pluralized+underscored table)
60
61
  if table
61
- table_key = tables.keys.find { |k| k.downcase == table.downcase } || table
62
+ table_down = table.downcase
63
+ table_as_table = table.underscore.pluralize # Cook → cooks, BrandProfile → brand_profiles
64
+ table_key = tables.keys.find { |k|
65
+ k.downcase == table_down || k == table_as_table || k == table.underscore
66
+ } || table
62
67
  table_data = tables[table_key]
63
68
  unless table_data
64
69
  return not_found_response("Table", table, tables.keys.sort,
@@ -44,7 +44,12 @@ module RailsAiContext
44
44
  # (HTML uses data-controller="weekly-chart", file is weekly_chart_controller.js)
45
45
  if controller
46
46
  normalized = controller.downcase.tr("-", "_")
47
- ctrl = all_controllers.find { |c| c[:name]&.downcase&.tr("-", "_") == normalized }
47
+ # Also handle PascalCase: CookStatus cook_status
48
+ underscored = controller.underscore.downcase.tr("-", "_")
49
+ ctrl = all_controllers.find { |c|
50
+ name_norm = c[:name]&.downcase&.tr("-", "_")
51
+ name_norm == normalized || name_norm == underscored
52
+ }
48
53
  unless ctrl
49
54
  names = all_controllers.map { |c| c[:name] }.sort
50
55
  return not_found_response("Stimulus controller", controller, names,
@@ -157,16 +157,27 @@ module RailsAiContext
157
157
  end
158
158
 
159
159
  private_class_method def self.find_test_file(name, type, detail = "full")
160
- # Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises"
160
+ # Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises", "cooks" (plural)
161
161
  snake = name.to_s.tr("/", "::").underscore.sub(/_controller$/, "")
162
+ # For models, also try singular form (cooks → cook)
163
+ snake_singular = snake.singularize
162
164
  candidates = case type
163
165
  when :model
164
- [
166
+ base = [
165
167
  "spec/models/#{snake}_spec.rb",
166
168
  "test/models/#{snake}_test.rb",
167
169
  "spec/models/concerns/#{snake}_spec.rb",
168
170
  "test/models/concerns/#{snake}_test.rb"
169
171
  ]
172
+ if snake != snake_singular
173
+ base += [
174
+ "spec/models/#{snake_singular}_spec.rb",
175
+ "test/models/#{snake_singular}_test.rb",
176
+ "spec/models/concerns/#{snake_singular}_spec.rb",
177
+ "test/models/concerns/#{snake_singular}_test.rb"
178
+ ]
179
+ end
180
+ base
170
181
  when :controller
171
182
  [
172
183
  "spec/controllers/#{snake}_controller_spec.rb",
@@ -51,9 +51,17 @@ module RailsAiContext
51
51
  end
52
52
 
53
53
  if controller
54
- ctrl_lower = controller.downcase
55
- filtered_templates = templates.select { |k, _| k.downcase.start_with?(ctrl_lower + "/") }
56
- filtered_partials = partials.select { |k, _| k.downcase.start_with?(ctrl_lower + "/") }
54
+ # Normalize: accept "CooksController", "cooks", "cooks_controller", "Bonus::CooksController"
55
+ ctrl_lower = controller.underscore.delete_suffix("_controller")
56
+ ctrl_lower_alt = controller.downcase.delete_suffix("controller")
57
+ filtered_templates = templates.select { |k, _|
58
+ k_down = k.downcase
59
+ k_down.start_with?(ctrl_lower + "/") || k_down.start_with?(ctrl_lower_alt + "/")
60
+ }
61
+ filtered_partials = partials.select { |k, _|
62
+ k_down = k.downcase
63
+ k_down.start_with?(ctrl_lower + "/") || k_down.start_with?(ctrl_lower_alt + "/")
64
+ }
57
65
 
58
66
  if filtered_templates.empty? && filtered_partials.empty?
59
67
  all_dirs = (templates.keys + partials.keys).map { |k| k.split("/").first }.uniq.sort
@@ -303,7 +311,12 @@ module RailsAiContext
303
311
  .sort
304
312
 
305
313
  if controller
306
- templates = templates.select { |t| t.downcase.start_with?(controller.downcase + "/") }
314
+ ctrl_lower = controller.underscore.delete_suffix("_controller")
315
+ ctrl_lower_alt = controller.downcase.delete_suffix("controller")
316
+ templates = templates.select { |t|
317
+ t_down = t.downcase
318
+ t_down.start_with?(ctrl_lower + "/") || t_down.start_with?(ctrl_lower_alt + "/")
319
+ }
307
320
  end
308
321
 
309
322
  lines = [ "# Views (#{templates.size} templates)", "" ]
@@ -37,7 +37,7 @@ module RailsAiContext
37
37
  # ── Main entry point ─────────────────────────────────────────────
38
38
 
39
39
  def self.call(files:, level: "syntax", server_context: nil)
40
- return text_response("No files provided.") if files.empty?
40
+ return text_response("No files provided. Pass file paths relative to Rails root (e.g. files:[\"app/models/cook.rb\"]).") if files.nil? || files.empty?
41
41
  return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.") if files.size > max_files
42
42
 
43
43
  results = []
@@ -48,7 +48,9 @@ module RailsAiContext
48
48
  full_path = Rails.root.join(file)
49
49
 
50
50
  unless File.exist?(full_path)
51
- results << "\u2717 #{file} \u2014 file not found"
51
+ suggestion = find_file_suggestion(file)
52
+ hint = suggestion ? " Did you mean '#{suggestion}'?" : ""
53
+ results << "\u2717 #{file} \u2014 file not found.#{hint}"
52
54
  total += 1
53
55
  next
54
56
  end
@@ -122,6 +124,24 @@ module RailsAiContext
122
124
 
123
125
  # ── Ruby validation ──────────────────────────────────────────────
124
126
 
127
+ # Search common Rails directories for a file by basename and suggest the full path
128
+ private_class_method def self.find_file_suggestion(file)
129
+ basename = File.basename(file)
130
+ %w[app/models app/controllers app/views app/helpers app/jobs app/mailers
131
+ app/services app/channels lib config].each do |dir|
132
+ candidate = File.join(dir, basename)
133
+ return candidate if File.exist?(Rails.root.join(candidate))
134
+ end
135
+
136
+ # Broader recursive search
137
+ matches = Dir.glob(File.join(Rails.root, "app", "**", basename)).first(1)
138
+ return matches.first.sub("#{Rails.root}/", "") if matches.any?
139
+
140
+ nil
141
+ rescue
142
+ nil
143
+ end
144
+
125
145
  private_class_method def self.validate_ruby(full_path)
126
146
  prism_available? ? validate_ruby_prism(full_path) : validate_ruby_subprocess(full_path)
127
147
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.0"
5
5
  end
data/server.json CHANGED
@@ -2,16 +2,16 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.crisnahine/rails-ai-context",
4
4
  "title": "Rails AI Context",
5
- "description": "Auto-expose Rails app structure to AI via MCP. Zero config, 25 read-only tools.",
5
+ "description": "Auto-expose Rails app structure to AI via MCP or CLI. Zero config, 25 read-only tools.",
6
6
  "repository": {
7
7
  "url": "https://github.com/crisnahine/rails-ai-context",
8
8
  "source": "github"
9
9
  },
10
- "version": "3.0.0",
10
+ "version": "3.1.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
14
- "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v3.0.0/rails-ai-context-mcp.mcpb",
14
+ "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v3.1.0/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: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine
@@ -164,10 +164,11 @@ dependencies:
164
164
  description: |
165
165
  rails-ai-context gives AI coding agents a complete mental model of your Rails
166
166
  app — not just files, but how schema, models, routes, controllers, views, and
167
- conventions connect. 25 live MCP tools let agents query structure on demand with
168
- semantic validation that catches cross-file errors (wrong columns, missing
169
- partials, broken routes) before code runs. Auto-generates context files for
170
- Claude Code, Cursor, GitHub Copilot, and OpenCode. Zero config.
167
+ conventions connect. 25 live tools (via MCP server or CLI) let agents query
168
+ structure on demand with semantic validation that catches cross-file errors
169
+ (wrong columns, missing partials, broken routes) before code runs.
170
+ Auto-generates context files for Claude Code, Cursor, GitHub Copilot, and
171
+ OpenCode. Zero config.
171
172
  email:
172
173
  - crisjosephnahine@gmail.com
173
174
  executables:
@@ -290,9 +291,9 @@ metadata:
290
291
  post_install_message: |
291
292
  rails-ai-context installed! Quick start:
292
293
  rails generate rails_ai_context:install
293
- rails ai:context # generate context files (compact mode)
294
- rails ai:context:full # full dump (good for small apps)
295
- rails ai:serve # start MCP server for Claude Code / Cursor
294
+ rails ai:context # generate context files
295
+ rails 'ai:tool[schema]' # run any of the 25 tools from CLI
296
+ rails ai:serve # start MCP server (optional)
296
297
  rdoc_options: []
297
298
  require_paths:
298
299
  - lib
@@ -309,5 +310,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
309
310
  requirements: []
310
311
  rubygems_version: 3.6.9
311
312
  specification_version: 4
312
- summary: Give AI agents a complete mental model of your Rails app via MCP zero config.
313
+ summary: Give AI agents a complete mental model of your Rails app 25 tools via MCP
314
+ or CLI. Zero config.
313
315
  test_files: []