rails-ai-context 2.0.1 → 2.0.3

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.
@@ -6,24 +6,15 @@ module RailsAiContext
6
6
  module Tools
7
7
  class SearchCode < BaseTool
8
8
  tool_name "rails_search_code"
9
- description "Search the Rails codebase by regex pattern, returning matching lines with file paths and line numbers. " \
10
- "Use when: finding where a method is called, locating class definitions, or tracing how a feature is implemented. " \
11
- "Requires pattern:\"def activate\". Narrow with path:\"app/models\" and file_type:\"rb\"."
9
+ description "Search the Rails codebase with smart modes. " \
10
+ "Use match_type:\"trace\" to see where a method is defined, who calls it, and what it calls in one call. " \
11
+ "Use match_type:\"definition\" for definitions only, \"call\" for call sites only, \"class\" for class/module definitions. " \
12
+ "Requires pattern:\"method_name\". Narrow with path:\"app/models\" and file_type:\"rb\"."
12
13
 
13
14
  def self.max_results_cap
14
15
  RailsAiContext.configuration.max_search_results
15
16
  end
16
17
 
17
- # Non-code files excluded from all searches — lock files, docs, generated context, config meta
18
- NON_CODE_GLOBS = %w[
19
- *.lock package-lock.json yarn.lock pnpm-lock.yaml bun.lockb
20
- *.md LICENSE* CHANGELOG* CONTRIBUTING*
21
- CLAUDE.md AGENTS.md .cursorrules .cursor/ .claude/
22
- Dockerfile* docker-compose*
23
- .rubocop.yml .ruby-version .node-version .tool-versions
24
- .github/ .circleci/ .gitlab-ci.yml
25
- ].freeze
26
-
27
18
  input_schema(
28
19
  properties: {
29
20
  pattern: {
@@ -40,16 +31,24 @@ module RailsAiContext
40
31
  },
41
32
  match_type: {
42
33
  type: "string",
43
- enum: %w[any definition class],
44
- description: "Filter match type. any: all matches (default). definition: only `def method_name` lines. class: only `class/module Name` lines."
34
+ enum: %w[any definition class call trace],
35
+ description: "any: all matches (default). definition: `def` lines only. class: `class/module` lines. call: call sites only (excludes definitions). trace: FULL PICTURE — shows definition + source code + all callers + what it calls internally."
45
36
  },
46
37
  exact_match: {
47
38
  type: "boolean",
48
39
  description: "Match whole words only (wraps pattern in \\b word boundaries). Default: false."
49
40
  },
50
- max_results: {
41
+ exclude_tests: {
42
+ type: "boolean",
43
+ description: "Exclude test/spec files from results. Default: false."
44
+ },
45
+ group_by_file: {
46
+ type: "boolean",
47
+ description: "Group results by file with match counts. Default: false."
48
+ },
49
+ offset: {
51
50
  type: "integer",
52
- description: "Maximum number of results. Default: 30, max: 200."
51
+ description: "Skip this many results for pagination. Default: 0."
53
52
  },
54
53
  context_lines: {
55
54
  type: "integer",
@@ -61,31 +60,40 @@ module RailsAiContext
61
60
 
62
61
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
63
62
 
64
- def self.call(pattern:, path: nil, file_type: nil, match_type: "any", exact_match: false, max_results: 30, context_lines: 2, server_context: nil)
63
+ def self.call(pattern:, path: nil, file_type: nil, match_type: "any", exact_match: false, exclude_tests: false, group_by_file: false, offset: 0, context_lines: 2, server_context: nil) # rubocop:disable Metrics
65
64
  root = Rails.root.to_s
65
+ original_pattern = pattern
66
66
 
67
67
  # Reject empty or whitespace-only patterns
68
68
  if pattern.nil? || pattern.strip.empty?
69
69
  return text_response("Pattern is required. Provide a search term or regex.")
70
70
  end
71
71
 
72
+ # Trace mode — the game changer: full method picture in one call
73
+ if match_type == "trace"
74
+ return trace_method(pattern.strip, root, path, exclude_tests)
75
+ end
76
+
72
77
  # Apply exact_match word boundaries
73
78
  pattern = "\\b#{pattern}\\b" if exact_match
74
79
 
75
- # Apply match_type filter to pattern (strip keyword if user already included it)
76
- pattern = case match_type
80
+ # Apply match_type filter to pattern
81
+ search_pattern = case match_type
77
82
  when "definition"
78
83
  cleaned = pattern.sub(/\A\s*def\s+/, "")
79
84
  "^\\s*def\\s+(self\\.)?#{cleaned}"
80
85
  when "class"
81
86
  cleaned = pattern.sub(/\A\s*(class|module)\s+/, "")
82
- "^\\s*(class|module)\\s+#{cleaned}"
83
- else pattern
87
+ "^\\s*(class|module)\\s+\\w*#{cleaned}"
88
+ when "call"
89
+ pattern
90
+ else
91
+ pattern
84
92
  end
85
93
 
86
94
  # Validate regex syntax early
87
95
  begin
88
- Regexp.new(pattern, timeout: 1)
96
+ Regexp.new(search_pattern, timeout: 1)
89
97
  rescue RegexpError => e
90
98
  return text_response("Invalid regex pattern: #{e.message}")
91
99
  end
@@ -95,10 +103,8 @@ module RailsAiContext
95
103
  return text_response("Invalid file_type: must contain only alphanumeric characters.")
96
104
  end
97
105
 
98
- # Cap max_results and context_lines
99
- max_results = [ max_results.to_i, max_results_cap ].min
100
- max_results = 30 if max_results < 1
101
106
  context_lines = [ [ context_lines.to_i, 0 ].max, 5 ].min
107
+ offset = [ offset.to_i, 0 ].max
102
108
 
103
109
  search_path = path ? File.join(root, path) : root
104
110
 
@@ -118,28 +124,60 @@ module RailsAiContext
118
124
  return text_response("Path not found: #{path}")
119
125
  end
120
126
 
121
- results = if ripgrep_available?
122
- search_with_ripgrep(pattern, search_path, file_type, max_results, root, context_lines)
127
+ # Fetch all results (capped at 200 for safety)
128
+ all_results = if ripgrep_available?
129
+ search_with_ripgrep(search_pattern, search_path, file_type, max_results_cap, root, context_lines, exclude_tests: exclude_tests)
123
130
  else
124
- search_with_ruby(pattern, search_path, file_type, max_results, root)
131
+ search_with_ruby(search_pattern, search_path, file_type, max_results_cap, root, exclude_tests: exclude_tests)
132
+ end
133
+
134
+ # Filter out definitions for match_type:"call"
135
+ all_results.reject! { |r| r[:content].match?(/\A\s*def\s/) } if match_type == "call"
136
+
137
+ if all_results.empty?
138
+ return text_response("No results found for '#{original_pattern}' in #{path || 'app'}.")
125
139
  end
126
140
 
127
- if results.empty?
128
- return text_response("No results found for '#{pattern}' in #{path || 'app'}.")
141
+ # Smart result limiting:
142
+ # <10 total show all, 10-100 show half, >100 → cap at 100
143
+ total = all_results.size
144
+ show_count = if total <= 10 then total
145
+ elsif total <= 100 then (total / 2.0).ceil
146
+ else 100
129
147
  end
130
148
 
131
- output = results.map { |r| "#{r[:file]}:#{r[:line_number]}: #{r[:content].strip}" }.join("\n")
132
- header = "# Search: `#{pattern}`\n**#{results.size} results**#{" in #{path}" if path}\n\n```\n"
133
- footer = "\n```"
149
+ # Apply pagination
150
+ paginated = all_results.drop(offset).first(show_count)
134
151
 
135
- text_response("#{header}#{output}#{footer}")
152
+ if paginated.empty? && total > 0
153
+ return text_response("No results at offset #{offset}. Total: #{total}. Use `offset:0`.")
154
+ end
155
+
156
+ # Build header with total count and pagination info
157
+ showing = offset > 0 ? "#{offset + 1}-#{offset + paginated.size}" : "#{paginated.size}"
158
+ pagination = if offset + paginated.size < total
159
+ "\n_Showing #{showing} of #{total}. Use `offset:#{offset + paginated.size}` for more._"
160
+ elsif total > paginated.size
161
+ "\n_Showing #{showing} of #{total}._"
162
+ else
163
+ ""
164
+ end
165
+
166
+ header = "# Search: `#{original_pattern}`\n**#{total} total results**#{" in #{path}" if path}, showing #{showing}\n"
167
+
168
+ if group_by_file
169
+ text_response(header + "\n" + format_grouped(paginated) + pagination)
170
+ else
171
+ output = paginated.map { |r| "#{r[:file]}:#{r[:line_number]}: #{r[:content].strip}" }.join("\n")
172
+ text_response("#{header}\n```\n#{output}\n```#{pagination}")
173
+ end
136
174
  end
137
175
 
138
176
  private_class_method def self.ripgrep_available?
139
177
  @rg_available ||= system("which rg > /dev/null 2>&1")
140
178
  end
141
179
 
142
- private_class_method def self.search_with_ripgrep(pattern, search_path, file_type, max_results, root, ctx_lines = 0)
180
+ private_class_method def self.search_with_ripgrep(pattern, search_path, file_type, max_results, root, ctx_lines = 0, exclude_tests: false)
143
181
  cmd = [ "rg", "--no-heading", "--line-number", "--sort=path", "--max-count", max_results.to_s ]
144
182
  if ctx_lines > 0
145
183
  cmd.push("-C", ctx_lines.to_s)
@@ -157,9 +195,12 @@ module RailsAiContext
157
195
  cmd << "--glob=!#{p}"
158
196
  end
159
197
 
160
- # Exclude non-code files that generate noise in search results
161
- # (excluded_paths already handles node_modules, tmp, log, vendor, .git)
162
- NON_CODE_GLOBS.each { |glob| cmd << "--glob=!#{glob}" }
198
+ # Exclude test/spec directories if requested
199
+ if exclude_tests
200
+ cmd << "--glob=!test/"
201
+ cmd << "--glob=!spec/"
202
+ cmd << "--glob=!features/"
203
+ end
163
204
 
164
205
  if file_type
165
206
  cmd.push("--type-add", "custom:*.#{file_type}", "--type", "custom")
@@ -178,7 +219,7 @@ module RailsAiContext
178
219
  [ { file: "error", line_number: 0, content: e.message } ]
179
220
  end
180
221
 
181
- private_class_method def self.search_with_ruby(pattern, search_path, file_type, max_results, root)
222
+ private_class_method def self.search_with_ruby(pattern, search_path, file_type, max_results, root, exclude_tests: false)
182
223
  results = []
183
224
  begin
184
225
  regex = Regexp.new(pattern, Regexp::IGNORECASE, timeout: 2)
@@ -189,11 +230,13 @@ module RailsAiContext
189
230
  glob = file_type ? "**/*.#{file_type}" : "**/*.{#{extensions}}"
190
231
  excluded = RailsAiContext.configuration.excluded_paths
191
232
  sensitive = RailsAiContext.configuration.sensitive_patterns
233
+ test_dirs = %w[test/ spec/ features/]
192
234
 
193
235
  Dir.glob(File.join(search_path, glob)).each do |file|
194
236
  relative = file.sub("#{root}/", "")
195
237
  next if excluded.any? { |ex| relative.start_with?(ex) }
196
238
  next if sensitive_file?(relative, sensitive)
239
+ next if exclude_tests && test_dirs.any? { |td| relative.start_with?(td) }
197
240
 
198
241
  File.readlines(file).each_with_index do |line, idx|
199
242
  if line.match?(regex)
@@ -216,6 +259,20 @@ module RailsAiContext
216
259
  end
217
260
  end
218
261
 
262
+ # Group results by file for cleaner output
263
+ private_class_method def self.format_grouped(results)
264
+ grouped = results.group_by { |r| r[:file] }
265
+ lines = []
266
+ grouped.each do |file, matches|
267
+ lines << "## #{file} (#{matches.size} matches)"
268
+ lines << "```"
269
+ matches.each { |r| lines << "#{r[:line_number]}: #{r[:content].strip}" }
270
+ lines << "```"
271
+ lines << ""
272
+ end
273
+ lines.join("\n")
274
+ end
275
+
219
276
  private_class_method def self.parse_rg_output(output, root)
220
277
  output.lines.filter_map do |line|
221
278
  next if line.strip == "--" # Skip group separators from -C context output
@@ -229,6 +286,216 @@ module RailsAiContext
229
286
  }
230
287
  end
231
288
  end
289
+
290
+ # ── Trace Mode — the game changer ──────────────────────────────
291
+ # Shows definition + source + callers + internal calls in one response
292
+
293
+ private_class_method def self.trace_method(method_name, root, path, exclude_tests) # rubocop:disable Metrics
294
+ # Clean input: strip "def ", "self.", parens
295
+ cleaned = method_name.sub(/\A\s*def\s+/, "").sub(/\Aself\./, "").sub(/\(.*/, "").strip
296
+ return text_response("Provide a method name to trace.") if cleaned.empty?
297
+
298
+ search_path = path ? File.join(root, path) : root
299
+ lines = [ "# Trace: `#{cleaned}`", "" ]
300
+
301
+ # 1. Find the definition (no \b after ? or ! since they ARE word boundaries)
302
+ def_pattern = "^\\s*def\\s+(self\\.)?#{Regexp.escape(cleaned)}"
303
+ def_pattern += "\\b" unless cleaned.end_with?("?") || cleaned.end_with?("!")
304
+ def_results = quick_search(def_pattern, search_path, root, 10, exclude_tests)
305
+
306
+ if def_results.any?
307
+ lines << "## Definition"
308
+ def_results.each do |r|
309
+ # Class/module context
310
+ class_context = extract_class_context(File.join(root, r[:file]), r[:line_number])
311
+ lines << "**#{r[:file]}:#{r[:line_number]}**#{class_context ? " in `#{class_context}`" : ""}"
312
+
313
+ # Full method body
314
+ body = extract_method_body(File.join(root, r[:file]), r[:line_number])
315
+ if body
316
+ lines << "```ruby"
317
+ lines << body
318
+ lines << "```"
319
+
320
+ # What does this method call?
321
+ internal_calls = body.scan(/\b([a-z_]\w*[!?]?)(?:\s*[\(])/).flatten.uniq
322
+ internal_calls += body.scan(/\b([A-Z]\w+(?:::\w+)*)\.(new|call|perform_later|perform_async|find|where|create)/).map { |c| "#{c[0]}.#{c[1]}" }
323
+ internal_calls.reject! { |c| %w[if else elsif unless return end def class module do begin rescue ensure raise puts print].include?(c) }
324
+ internal_calls.reject! { |c| c == cleaned }
325
+
326
+ if internal_calls.any?
327
+ lines << "" << "## Calls internally"
328
+ internal_calls.first(15).each { |c| lines << "- `#{c}`" }
329
+ end
330
+ end
331
+
332
+ # Sibling methods in the same file
333
+ siblings = extract_sibling_methods(File.join(root, r[:file]), r[:line_number], cleaned)
334
+ if siblings.any?
335
+ lines << "" << "## Sibling methods (same file)"
336
+ siblings.first(10).each { |s| lines << "- `#{s}`" }
337
+ end
338
+
339
+ lines << ""
340
+ end
341
+ else
342
+ lines << "_No definition found for `def #{cleaned}`_"
343
+ lines << ""
344
+ end
345
+
346
+ # 2. Find all callers (everywhere the method is referenced, excluding the def line)
347
+ call_pattern = if cleaned.end_with?("?") || cleaned.end_with?("!")
348
+ "#{Regexp.escape(cleaned)}"
349
+ else
350
+ "\\b#{Regexp.escape(cleaned)}\\b"
351
+ end
352
+ call_results = quick_search(call_pattern, search_path, root, max_results_cap, exclude_tests)
353
+ callers = call_results.reject { |r| r[:content].match?(/\A\s*def\s/) }
354
+
355
+ # Exclude the definition file+line to avoid self-reference
356
+ def_locations = def_results.map { |r| "#{r[:file]}:#{r[:line_number]}" }.to_set
357
+ callers.reject! { |r| def_locations.include?("#{r[:file]}:#{r[:line_number]}") }
358
+
359
+ if callers.any?
360
+ # Separate app code from tests
361
+ app_callers = callers.reject { |r| r[:file].match?(/\A(test|spec)\//) }
362
+ test_callers = callers.select { |r| r[:file].match?(/\A(test|spec)\//) }
363
+
364
+ if app_callers.any?
365
+ lines << "## Called from (#{app_callers.size} sites)"
366
+ grouped = app_callers.group_by { |r| r[:file] }
367
+ grouped.each do |file, matches|
368
+ category = case file
369
+ when /controller/i then "Controller"
370
+ when /model/i then "Model"
371
+ when /view|\.erb/i then "View"
372
+ when /job/i then "Job"
373
+ when /service/i then "Service"
374
+ when /\.js$|\.ts$/i then "JavaScript"
375
+ else "Other"
376
+ end
377
+
378
+ # Route chain for controller callers
379
+ route_hint = ""
380
+ if category == "Controller" && file.match?(/app\/controllers\/(.+)_controller\.rb/)
381
+ ctrl_path = $1
382
+ route_actions = extract_controller_actions_from_matches(matches)
383
+ routes = find_routes_for_controller(ctrl_path, route_actions, root)
384
+ route_hint = " → #{routes}" if routes
385
+ end
386
+
387
+ lines << "### #{file} (#{category})#{route_hint}"
388
+ matches.first(5).each do |r|
389
+ lines << " #{r[:line_number]}: #{r[:content].strip}"
390
+ end
391
+ lines << " _(#{matches.size - 5} more)_" if matches.size > 5
392
+ end
393
+ end
394
+
395
+ if test_callers.any?
396
+ lines << "" << "## Tested by (#{test_callers.size} references)"
397
+ test_callers.group_by { |r| r[:file] }.each do |file, matches|
398
+ lines << "- `#{file}` (#{matches.size} references)"
399
+ end
400
+ end
401
+ else
402
+ lines << "## Called from"
403
+ lines << "_No call sites found (method may be unused or called dynamically)_"
404
+ end
405
+
406
+ text_response(lines.join("\n"))
407
+ rescue => e
408
+ text_response("Trace error: #{e.message}")
409
+ end
410
+
411
+ # Fast ripgrep search for trace mode (no formatting, just results)
412
+ private_class_method def self.quick_search(pattern, search_path, root, limit, exclude_tests)
413
+ if ripgrep_available?
414
+ search_with_ripgrep(pattern, search_path, nil, limit, root, 0, exclude_tests: exclude_tests)
415
+ else
416
+ search_with_ruby(pattern, search_path, nil, limit, root, exclude_tests: exclude_tests)
417
+ end
418
+ end
419
+
420
+ # Extract class/module context for a line
421
+ private_class_method def self.extract_class_context(file_path, line_num)
422
+ return nil unless File.exist?(file_path)
423
+ lines = File.readlines(file_path)
424
+ # Walk backwards from the method to find the enclosing class/module
425
+ (line_num - 2).downto(0) do |i|
426
+ if lines[i]&.match?(/\A\s*(class|module)\s+(\S+)/)
427
+ return lines[i].strip.sub(/\s*<.*/, "")
428
+ end
429
+ end
430
+ nil
431
+ rescue
432
+ nil
433
+ end
434
+
435
+ # Extract sibling methods in the same file (other public methods)
436
+ private_class_method def self.extract_sibling_methods(file_path, def_line, exclude_method)
437
+ return [] unless File.exist?(file_path)
438
+ return [] if File.size(file_path) > RailsAiContext.configuration.max_file_size
439
+ source = File.read(file_path, encoding: "UTF-8", invalid: :replace, undef: :replace)
440
+ methods = []
441
+ in_private = false
442
+ source.each_line do |line|
443
+ in_private = true if line.match?(/\A\s*private\s*$/)
444
+ next if in_private
445
+ if (m = line.match(/\A\s*def\s+((?:self\.)?\w+[?!]?)/))
446
+ name = m[1]
447
+ methods << name unless name == exclude_method || name.start_with?("initialize")
448
+ end
449
+ end
450
+ methods
451
+ rescue
452
+ []
453
+ end
454
+
455
+ # Extract which action a controller caller is in
456
+ private_class_method def self.extract_controller_actions_from_matches(matches)
457
+ actions = []
458
+ matches.each do |m|
459
+ # Look for the method name from indentation context
460
+ actions << $1 if m[:content].match?(/\b(create|index|show|new|edit|update|destroy|[a-z_]+)\b/)
461
+ end
462
+ actions.uniq.first(3)
463
+ end
464
+
465
+ # Find routes for a controller
466
+ private_class_method def self.find_routes_for_controller(ctrl_path, _actions, _root)
467
+ routes = cached_context[:routes]
468
+ return nil unless routes
469
+ by_controller = routes[:by_controller] || {}
470
+ ctrl_routes = by_controller[ctrl_path]
471
+ return nil unless ctrl_routes&.any?
472
+ # Show the first 2 routes as hints
473
+ ctrl_routes.first(2).map { |r| "`#{r[:verb]} #{r[:path]}`" }.join(", ")
474
+ rescue
475
+ nil
476
+ end
477
+
478
+ # Extract a method body from a file given the def line number
479
+ private_class_method def self.extract_method_body(file_path, def_line)
480
+ return nil unless File.exist?(file_path)
481
+ return nil if File.size(file_path) > RailsAiContext.configuration.max_file_size
482
+
483
+ source_lines = File.readlines(file_path)
484
+ start_idx = def_line - 1
485
+ return nil if start_idx >= source_lines.size
486
+
487
+ def_indent = source_lines[start_idx][/\A\s*/].length
488
+ result = [ source_lines[start_idx].rstrip ]
489
+
490
+ source_lines[(start_idx + 1)..].each do |line|
491
+ result << line.rstrip
492
+ break if line.match?(/\A\s{#{def_indent}}end\b/)
493
+ end
494
+
495
+ result.join("\n")
496
+ rescue
497
+ nil
498
+ end
232
499
  end
233
500
  end
234
501
  end
@@ -95,6 +95,12 @@ module RailsAiContext
95
95
  end
96
96
  end
97
97
 
98
+ # Run Brakeman security scan on validated files (if installed and level:"rails")
99
+ if level == "rails"
100
+ brakeman_warnings = check_brakeman_security(files)
101
+ brakeman_warnings.each { |w| results << " \u26A0 #{w}" }
102
+ end
103
+
98
104
  output = results.join("\n")
99
105
  output += "\n\n#{passed}/#{total} files passed"
100
106
  output += " _(Prism unavailable — using fallback parser, some semantic checks skipped)_" unless prism_available?
@@ -950,6 +956,48 @@ module RailsAiContext
950
956
  rescue
951
957
  []
952
958
  end
959
+
960
+ # ── Brakeman security scan (runs once for all files) ───────────
961
+
962
+ private_class_method def self.check_brakeman_security(files)
963
+ return [] unless brakeman_available?
964
+
965
+ tracker = Brakeman.run(
966
+ app_path: Rails.root.to_s,
967
+ quiet: true,
968
+ report_progress: false,
969
+ print_report: false
970
+ )
971
+
972
+ warnings = tracker.filtered_warnings
973
+ return [] if warnings.empty?
974
+
975
+ # Filter to only warnings in the validated files
976
+ normalized = files.map { |f| f.delete_prefix("/") }
977
+ relevant = warnings.select do |w|
978
+ path = w.file.relative
979
+ normalized.any? { |f| path == f || path.start_with?(f) }
980
+ end
981
+ return [] if relevant.empty?
982
+
983
+ relevant.sort_by(&:confidence).first(5).map do |w|
984
+ loc = w.line ? "#{w.file.relative}:#{w.line}" : w.file.relative
985
+ "[#{w.confidence_name}] #{w.warning_type} — #{loc}: #{w.message}"
986
+ end
987
+ rescue
988
+ []
989
+ end
990
+
991
+ private_class_method def self.brakeman_available?
992
+ return @brakeman_available unless @brakeman_available.nil?
993
+
994
+ @brakeman_available = begin
995
+ require "brakeman"
996
+ true
997
+ rescue LoadError
998
+ false
999
+ end
1000
+ end
953
1001
  end
954
1002
  end
955
1003
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "2.0.1"
4
+ VERSION = "2.0.3"
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": "2.0.1",
10
+ "version": "2.0.3",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
14
- "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v2.0.1/rails-ai-context-mcp.mcpb",
14
+ "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v2.0.3/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: 2.0.1
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine