rails-ai-context 4.3.2 → 4.4.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 +4 -4
- data/CHANGELOG.md +127 -53
- data/CLAUDE.md +3 -1
- data/README.md +268 -197
- data/demo-trace.gif +0 -0
- data/demo-trace.tape +21 -0
- data/demo.gif +0 -0
- data/demo.tape +33 -0
- data/docs/GUIDE.md +9 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
- data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
- data/lib/rails_ai_context/configuration.rb +25 -1
- data/lib/rails_ai_context/doctor.rb +4 -2
- data/lib/rails_ai_context/fingerprinter.rb +6 -1
- data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
- data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
- data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
- data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
- data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
- data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
- data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -3
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
- data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
- data/lib/rails_ai_context/tools/base_tool.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +4 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
- data/lib/rails_ai_context/tools/get_concern.rb +12 -6
- data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
- data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
- data/lib/rails_ai_context/tools/get_env.rb +8 -4
- data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
- data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
- data/lib/rails_ai_context/tools/get_schema.rb +2 -1
- data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
- data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
- data/lib/rails_ai_context/tools/onboard.rb +2 -1
- data/lib/rails_ai_context/tools/performance_check.rb +2 -1
- data/lib/rails_ai_context/tools/query.rb +5 -1
- data/lib/rails_ai_context/tools/read_logs.rb +3 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
- data/lib/rails_ai_context/tools/search_code.rb +8 -4
- data/lib/rails_ai_context/tools/search_docs.rb +2 -1
- data/lib/rails_ai_context/tools/session_context.rb +2 -1
- data/lib/rails_ai_context/tools/validate.rb +16 -8
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +5 -1
|
@@ -14,6 +14,7 @@ module RailsAiContext
|
|
|
14
14
|
{
|
|
15
15
|
turbo_frames: extract_turbo_frames,
|
|
16
16
|
turbo_streams: extract_turbo_stream_templates,
|
|
17
|
+
stream_actions: extract_stream_actions,
|
|
17
18
|
model_broadcasts: extract_model_broadcasts,
|
|
18
19
|
morph_meta: detect_morph_meta,
|
|
19
20
|
permanent_elements: extract_permanent_elements,
|
|
@@ -43,13 +44,18 @@ module RailsAiContext
|
|
|
43
44
|
content = File.read(path)
|
|
44
45
|
relative = path.sub("#{views_dir}/", "")
|
|
45
46
|
|
|
46
|
-
content.
|
|
47
|
-
|
|
47
|
+
content.each_line do |line|
|
|
48
|
+
next unless (match = line.match(/turbo_frame_tag\s+[:"']?(\w+)/))
|
|
49
|
+
entry = { id: match[1], file: relative }
|
|
50
|
+
src_match = line.match(/src:\s*["']?([^"',\s)]+)/)
|
|
51
|
+
entry[:src] = src_match[1] if src_match
|
|
52
|
+
frames << entry
|
|
48
53
|
end
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
frames.sort_by { |f| f[:id] }
|
|
52
|
-
rescue
|
|
57
|
+
rescue => e
|
|
58
|
+
$stderr.puts "[rails-ai-context] extract_turbo_frames failed: #{e.message}" if ENV["DEBUG"]
|
|
53
59
|
[]
|
|
54
60
|
end
|
|
55
61
|
|
|
@@ -61,6 +67,21 @@ module RailsAiContext
|
|
|
61
67
|
end.sort
|
|
62
68
|
end
|
|
63
69
|
|
|
70
|
+
def extract_stream_actions
|
|
71
|
+
actions = Hash.new(0)
|
|
72
|
+
return actions unless Dir.exist?(views_dir)
|
|
73
|
+
|
|
74
|
+
Dir.glob(File.join(views_dir, "**", "*.turbo_stream.erb")).each do |path|
|
|
75
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
76
|
+
content.scan(/turbo_stream\.(\w+)/).each { |action| actions[action[0]] += 1 }
|
|
77
|
+
content.scan(/<turbo-stream\s+action=["'](\w+)["']/).each { |action| actions[action[0]] += 1 }
|
|
78
|
+
end
|
|
79
|
+
actions
|
|
80
|
+
rescue => e
|
|
81
|
+
$stderr.puts "[rails-ai-context] extract_stream_actions failed: #{e.message}" if ENV["DEBUG"]
|
|
82
|
+
{}
|
|
83
|
+
end
|
|
84
|
+
|
|
64
85
|
def extract_model_broadcasts
|
|
65
86
|
models_dir = File.join(root, "app/models")
|
|
66
87
|
return [] unless Dir.exist?(models_dir)
|
|
@@ -77,7 +98,8 @@ module RailsAiContext
|
|
|
77
98
|
end
|
|
78
99
|
|
|
79
100
|
broadcasts.sort_by { |b| b[:model] }
|
|
80
|
-
rescue
|
|
101
|
+
rescue => e
|
|
102
|
+
$stderr.puts "[rails-ai-context] extract_model_broadcasts failed: #{e.message}" if ENV["DEBUG"]
|
|
81
103
|
[]
|
|
82
104
|
end
|
|
83
105
|
|
|
@@ -89,7 +111,8 @@ module RailsAiContext
|
|
|
89
111
|
content = File.read(path) rescue next
|
|
90
112
|
content.include?('name="turbo-refresh-method"') && content.include?('content="morph"')
|
|
91
113
|
end
|
|
92
|
-
rescue
|
|
114
|
+
rescue => e
|
|
115
|
+
$stderr.puts "[rails-ai-context] detect_morph_meta failed: #{e.message}" if ENV["DEBUG"]
|
|
93
116
|
false
|
|
94
117
|
end
|
|
95
118
|
|
|
@@ -122,7 +145,8 @@ module RailsAiContext
|
|
|
122
145
|
end
|
|
123
146
|
|
|
124
147
|
elements.uniq
|
|
125
|
-
rescue
|
|
148
|
+
rescue => e
|
|
149
|
+
$stderr.puts "[rails-ai-context] extract_permanent_elements failed: #{e.message}" if ENV["DEBUG"]
|
|
126
150
|
[]
|
|
127
151
|
end
|
|
128
152
|
|
|
@@ -146,7 +170,8 @@ module RailsAiContext
|
|
|
146
170
|
end
|
|
147
171
|
|
|
148
172
|
counts
|
|
149
|
-
rescue
|
|
173
|
+
rescue => e
|
|
174
|
+
$stderr.puts "[rails-ai-context] extract_turbo_drive_settings failed: #{e.message}" if ENV["DEBUG"]
|
|
150
175
|
{ "data-turbo-false": 0, "data-turbo-action": 0, "data-turbo-preload": 0 }
|
|
151
176
|
end
|
|
152
177
|
|
|
@@ -159,7 +184,8 @@ module RailsAiContext
|
|
|
159
184
|
native_navigation: detect_native_navigation(controllers_dir),
|
|
160
185
|
native_conditionals: detect_native_conditionals
|
|
161
186
|
}
|
|
162
|
-
rescue
|
|
187
|
+
rescue => e
|
|
188
|
+
$stderr.puts "[rails-ai-context] detect_turbo_native failed: #{e.message}" if ENV["DEBUG"]
|
|
163
189
|
{ detected: false, native_helpers: [], native_navigation: [], native_conditionals: 0 }
|
|
164
190
|
end
|
|
165
191
|
|
|
@@ -170,7 +196,8 @@ module RailsAiContext
|
|
|
170
196
|
content = File.read(path) rescue next
|
|
171
197
|
content.match?(/include\s+Turbo::Native::Navigation/)
|
|
172
198
|
end
|
|
173
|
-
rescue
|
|
199
|
+
rescue => e
|
|
200
|
+
$stderr.puts "[rails-ai-context] detect_native_include failed: #{e.message}" if ENV["DEBUG"]
|
|
174
201
|
false
|
|
175
202
|
end
|
|
176
203
|
|
|
@@ -183,7 +210,8 @@ module RailsAiContext
|
|
|
183
210
|
path.sub("#{root}/", "")
|
|
184
211
|
end
|
|
185
212
|
end.sort
|
|
186
|
-
rescue
|
|
213
|
+
rescue => e
|
|
214
|
+
$stderr.puts "[rails-ai-context] detect_native_helpers failed: #{e.message}" if ENV["DEBUG"]
|
|
187
215
|
[]
|
|
188
216
|
end
|
|
189
217
|
|
|
@@ -207,7 +235,8 @@ module RailsAiContext
|
|
|
207
235
|
end
|
|
208
236
|
|
|
209
237
|
results.sort_by { |r| [ r[:file], r[:method] ] }
|
|
210
|
-
rescue
|
|
238
|
+
rescue => e
|
|
239
|
+
$stderr.puts "[rails-ai-context] detect_native_navigation failed: #{e.message}" if ENV["DEBUG"]
|
|
211
240
|
[]
|
|
212
241
|
end
|
|
213
242
|
|
|
@@ -221,7 +250,8 @@ module RailsAiContext
|
|
|
221
250
|
end
|
|
222
251
|
|
|
223
252
|
count
|
|
224
|
-
rescue
|
|
253
|
+
rescue => e
|
|
254
|
+
$stderr.puts "[rails-ai-context] detect_native_conditionals failed: #{e.message}" if ENV["DEBUG"]
|
|
225
255
|
0
|
|
226
256
|
end
|
|
227
257
|
|
|
@@ -248,7 +278,8 @@ module RailsAiContext
|
|
|
248
278
|
end
|
|
249
279
|
|
|
250
280
|
responses.uniq.sort_by { |r| [ r[:controller], r[:action] ] }
|
|
251
|
-
rescue
|
|
281
|
+
rescue => e
|
|
282
|
+
$stderr.puts "[rails-ai-context] extract_turbo_stream_responses failed: #{e.message}" if ENV["DEBUG"]
|
|
252
283
|
[]
|
|
253
284
|
end
|
|
254
285
|
end
|
|
@@ -21,7 +21,8 @@ module RailsAiContext
|
|
|
21
21
|
template_engines: detect_template_engines,
|
|
22
22
|
form_builders_detected: detect_form_builders,
|
|
23
23
|
component_usage: detect_component_usage,
|
|
24
|
-
layout_mapping: extract_layout_mapping
|
|
24
|
+
layout_mapping: extract_layout_mapping,
|
|
25
|
+
conditional_layouts: detect_conditional_layouts
|
|
25
26
|
}
|
|
26
27
|
rescue => e
|
|
27
28
|
{ error: e.message }
|
|
@@ -42,8 +43,16 @@ module RailsAiContext
|
|
|
42
43
|
return [] unless Dir.exist?(dir)
|
|
43
44
|
|
|
44
45
|
Dir.glob(File.join(dir, "*")).filter_map do |path|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
next unless File.file?(path)
|
|
47
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
48
|
+
yields = content.scan(/<%=?\s*(?:yield|content_for)\s*[:(]?\s*:?(\w*)/).flatten.reject(&:empty?)
|
|
49
|
+
entry = { name: File.basename(path) }
|
|
50
|
+
entry[:yields] = yields unless yields.empty?
|
|
51
|
+
entry
|
|
52
|
+
rescue => e
|
|
53
|
+
$stderr.puts "[rails-ai-context] extract_layouts read failed: #{e.message}" if ENV["DEBUG"]
|
|
54
|
+
{ name: File.basename(path) }
|
|
55
|
+
end.sort_by { |l| l[:name] }
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
def extract_templates
|
|
@@ -98,7 +107,8 @@ module RailsAiContext
|
|
|
98
107
|
file: relative,
|
|
99
108
|
methods: methods
|
|
100
109
|
}
|
|
101
|
-
rescue
|
|
110
|
+
rescue => e
|
|
111
|
+
$stderr.puts "[rails-ai-context] extract_helpers failed: #{e.message}" if ENV["DEBUG"]
|
|
102
112
|
nil
|
|
103
113
|
end.sort_by { |h| h[:file] }
|
|
104
114
|
end
|
|
@@ -150,7 +160,8 @@ module RailsAiContext
|
|
|
150
160
|
end
|
|
151
161
|
|
|
152
162
|
counts.sort_by { |_, v| -v }.to_h
|
|
153
|
-
rescue
|
|
163
|
+
rescue => e
|
|
164
|
+
$stderr.puts "[rails-ai-context] detect_form_builders failed: #{e.message}" if ENV["DEBUG"]
|
|
154
165
|
{}
|
|
155
166
|
end
|
|
156
167
|
|
|
@@ -168,7 +179,8 @@ module RailsAiContext
|
|
|
168
179
|
end
|
|
169
180
|
|
|
170
181
|
components.to_a.sort
|
|
171
|
-
rescue
|
|
182
|
+
rescue => e
|
|
183
|
+
$stderr.puts "[rails-ai-context] detect_component_usage failed: #{e.message}" if ENV["DEBUG"]
|
|
172
184
|
[]
|
|
173
185
|
end
|
|
174
186
|
|
|
@@ -183,7 +195,34 @@ module RailsAiContext
|
|
|
183
195
|
name = basename.sub(/\.(html|xml|json)\.(erb|haml|slim)\z/, "").sub(/\.(erb|haml|slim)\z/, "")
|
|
184
196
|
name
|
|
185
197
|
end.uniq.sort
|
|
186
|
-
rescue
|
|
198
|
+
rescue => e
|
|
199
|
+
$stderr.puts "[rails-ai-context] extract_layout_mapping failed: #{e.message}" if ENV["DEBUG"]
|
|
200
|
+
[]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def detect_conditional_layouts
|
|
204
|
+
layouts = []
|
|
205
|
+
controllers_dir = File.join(app.root, "app", "controllers")
|
|
206
|
+
return layouts unless Dir.exist?(controllers_dir)
|
|
207
|
+
|
|
208
|
+
Dir.glob(File.join(controllers_dir, "**", "*.rb")).each do |path|
|
|
209
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
210
|
+
content.each_line do |line|
|
|
211
|
+
if (match = line.match(/\A\s*layout\s+["':]*(\w+)["']?(.*)$/))
|
|
212
|
+
entry = { layout: match[1], controller: File.basename(path, ".rb").camelize }
|
|
213
|
+
conditions = match[2].strip
|
|
214
|
+
entry[:only] = conditions.scan(/only:\s*\[?([^\]]+)\]?/).flatten.first&.scan(/:(\w+)/)&.flatten if conditions.include?("only:")
|
|
215
|
+
entry[:except] = conditions.scan(/except:\s*\[?([^\]]+)\]?/).flatten.first&.scan(/:(\w+)/)&.flatten if conditions.include?("except:")
|
|
216
|
+
entry[:condition] = conditions.strip unless conditions.empty?
|
|
217
|
+
layouts << entry
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
rescue => e
|
|
221
|
+
$stderr.puts "[rails-ai-context] detect_conditional_layouts failed: #{e.message}" if ENV["DEBUG"]
|
|
222
|
+
end
|
|
223
|
+
layouts
|
|
224
|
+
rescue => e
|
|
225
|
+
$stderr.puts "[rails-ai-context] detect_conditional_layouts failed: #{e.message}" if ENV["DEBUG"]
|
|
187
226
|
[]
|
|
188
227
|
end
|
|
189
228
|
end
|
|
@@ -41,8 +41,10 @@ module RailsAiContext
|
|
|
41
41
|
relative = path.sub("#{views_dir}/", "")
|
|
42
42
|
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
43
43
|
|
|
44
|
+
slots = extract_slot_refs(content)
|
|
45
|
+
|
|
44
46
|
if phlex_view?(path, content)
|
|
45
|
-
|
|
47
|
+
entry = {
|
|
46
48
|
lines: content.lines.count,
|
|
47
49
|
partials: extract_partial_refs(content),
|
|
48
50
|
stimulus: extract_stimulus_refs(content),
|
|
@@ -50,12 +52,16 @@ module RailsAiContext
|
|
|
50
52
|
helpers: extract_phlex_helper_calls(content),
|
|
51
53
|
phlex: true
|
|
52
54
|
}
|
|
55
|
+
entry[:slots] = slots unless slots.empty?
|
|
56
|
+
templates[relative] = entry
|
|
53
57
|
else
|
|
54
|
-
|
|
58
|
+
entry = {
|
|
55
59
|
lines: content.lines.count,
|
|
56
60
|
partials: extract_partial_refs(content),
|
|
57
61
|
stimulus: extract_stimulus_refs(content)
|
|
58
62
|
}
|
|
63
|
+
entry[:slots] = slots unless slots.empty?
|
|
64
|
+
templates[relative] = entry
|
|
59
65
|
end
|
|
60
66
|
end
|
|
61
67
|
templates
|
|
@@ -569,7 +575,8 @@ module RailsAiContext
|
|
|
569
575
|
builders[:simple_form_for] = content.scan(/\bsimple_form_for\b/).size
|
|
570
576
|
builders[:formtastic] = content.scan(/\bsemantic_form_for\b/).size
|
|
571
577
|
builders.reject { |_, v| v == 0 }
|
|
572
|
-
rescue
|
|
578
|
+
rescue => e
|
|
579
|
+
$stderr.puts "[rails-ai-context] extract_form_builders failed: #{e.message}" if ENV["DEBUG"]
|
|
573
580
|
{}
|
|
574
581
|
end
|
|
575
582
|
|
|
@@ -580,7 +587,8 @@ module RailsAiContext
|
|
|
580
587
|
tags[tag.to_sym] = count if count > 0
|
|
581
588
|
end
|
|
582
589
|
tags
|
|
583
|
-
rescue
|
|
590
|
+
rescue => e
|
|
591
|
+
$stderr.puts "[rails-ai-context] extract_semantic_html failed: #{e.message}" if ENV["DEBUG"]
|
|
584
592
|
{}
|
|
585
593
|
end
|
|
586
594
|
|
|
@@ -593,7 +601,8 @@ module RailsAiContext
|
|
|
593
601
|
sr_only_count = content.scan(/\bsr-only\b/).size
|
|
594
602
|
patterns[:sr_only] = sr_only_count if sr_only_count > 0
|
|
595
603
|
patterns
|
|
596
|
-
rescue
|
|
604
|
+
rescue => e
|
|
605
|
+
$stderr.puts "[rails-ai-context] extract_accessibility_patterns failed: #{e.message}" if ENV["DEBUG"]
|
|
597
606
|
{}
|
|
598
607
|
end
|
|
599
608
|
|
|
@@ -636,7 +645,8 @@ module RailsAiContext
|
|
|
636
645
|
end
|
|
637
646
|
|
|
638
647
|
examples.values.map { |e| e.except(:score) }.first(5)
|
|
639
|
-
rescue
|
|
648
|
+
rescue => e
|
|
649
|
+
$stderr.puts "[rails-ai-context] extract_canonical_examples failed: #{e.message}" if ENV["DEBUG"]
|
|
640
650
|
[]
|
|
641
651
|
end
|
|
642
652
|
|
|
@@ -697,7 +707,8 @@ module RailsAiContext
|
|
|
697
707
|
description = infer_partial_description(name, content)
|
|
698
708
|
{ name: name, lines: content.lines.size, description: description }
|
|
699
709
|
end
|
|
700
|
-
rescue
|
|
710
|
+
rescue => e
|
|
711
|
+
$stderr.puts "[rails-ai-context] discover_shared_partials failed: #{e.message}" if ENV["DEBUG"]
|
|
701
712
|
[]
|
|
702
713
|
end
|
|
703
714
|
|
|
@@ -818,6 +829,13 @@ module RailsAiContext
|
|
|
818
829
|
end
|
|
819
830
|
refs.uniq
|
|
820
831
|
end
|
|
832
|
+
|
|
833
|
+
def extract_slot_refs(content)
|
|
834
|
+
content.scan(/\b(?:renders_one|renders_many)\s+:(\w+)/).flatten
|
|
835
|
+
rescue => e
|
|
836
|
+
$stderr.puts "[rails-ai-context] extract_slot_refs failed: #{e.message}" if ENV["DEBUG"]
|
|
837
|
+
[]
|
|
838
|
+
end
|
|
821
839
|
end
|
|
822
840
|
end
|
|
823
841
|
end
|
|
@@ -113,7 +113,7 @@ module RailsAiContext
|
|
|
113
113
|
content = JSON.pretty_generate(data)
|
|
114
114
|
[ { uri: uri, mime_type: "application/json", text: content } ]
|
|
115
115
|
else
|
|
116
|
-
raise "Unknown resource: #{uri}"
|
|
116
|
+
raise RailsAiContext::Error, "Unknown resource: #{uri}"
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
end
|
|
@@ -105,9 +105,10 @@ module RailsAiContext
|
|
|
105
105
|
|
|
106
106
|
def start_stdio(server)
|
|
107
107
|
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
108
|
+
tools = active_tools(RailsAiContext.configuration)
|
|
108
109
|
# Log to stderr so we don't pollute the JSON-RPC channel on stdout
|
|
109
110
|
$stderr.puts "[rails-ai-context] MCP server started (stdio transport)"
|
|
110
|
-
$stderr.puts "[rails-ai-context] Tools: #{
|
|
111
|
+
$stderr.puts "[rails-ai-context] Tools (#{tools.size}): #{tools.map { |t| t.tool_name }.join(', ')}"
|
|
111
112
|
maybe_start_live_reload(server)
|
|
112
113
|
transport.open
|
|
113
114
|
end
|
|
@@ -119,13 +120,15 @@ module RailsAiContext
|
|
|
119
120
|
# Build a minimal Rack app that delegates to the MCP transport
|
|
120
121
|
rack_app = build_rack_app(transport, config.http_path)
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
loopback = %w[127.0.0.1 ::1 localhost].freeze
|
|
124
|
+
unless loopback.include?(config.http_bind)
|
|
123
125
|
$stderr.puts "[rails-ai-context] WARNING: MCP HTTP transport binding to #{config.http_bind} — " \
|
|
124
126
|
"this exposes all tools to the network without authentication. " \
|
|
125
127
|
"Use 127.0.0.1 (default) unless you have external auth in place."
|
|
126
128
|
end
|
|
129
|
+
tools = active_tools(config)
|
|
127
130
|
$stderr.puts "[rails-ai-context] MCP server starting on #{config.http_bind}:#{config.http_port}#{config.http_path}"
|
|
128
|
-
$stderr.puts "[rails-ai-context] Tools: #{
|
|
131
|
+
$stderr.puts "[rails-ai-context] Tools (#{tools.size}): #{tools.map { |t| t.tool_name }.join(', ')}"
|
|
129
132
|
maybe_start_live_reload(server)
|
|
130
133
|
|
|
131
134
|
begin
|
|
@@ -91,7 +91,8 @@ def save_tool_mode_to_initializer(mode)
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
File.write(init_path, content)
|
|
94
|
-
rescue
|
|
94
|
+
rescue => e
|
|
95
|
+
$stderr.puts "[rails-ai-context] save_tool_mode_to_initializer failed: #{e.message}" if ENV["DEBUG"]
|
|
95
96
|
nil
|
|
96
97
|
end unless defined?(save_tool_mode_to_initializer)
|
|
97
98
|
|
|
@@ -113,7 +114,8 @@ def tool_mode_configured?
|
|
|
113
114
|
content = File.read(init_path)
|
|
114
115
|
# Check for uncommented tool_mode line (not just a comment)
|
|
115
116
|
content.match?(/^\s*config\.tool_mode\s*=/)
|
|
116
|
-
rescue
|
|
117
|
+
rescue => e
|
|
118
|
+
$stderr.puts "[rails-ai-context] tool_mode_configured? failed: #{e.message}" if ENV["DEBUG"]
|
|
117
119
|
false
|
|
118
120
|
end unless defined?(tool_mode_configured?)
|
|
119
121
|
|
|
@@ -136,7 +138,8 @@ def save_ai_tools_to_initializer(tools)
|
|
|
136
138
|
|
|
137
139
|
File.write(init_path, content)
|
|
138
140
|
puts "💾 Saved to config/initializers/rails_ai_context.rb"
|
|
139
|
-
rescue
|
|
141
|
+
rescue => e
|
|
142
|
+
$stderr.puts "[rails-ai-context] save_ai_tools_to_initializer failed: #{e.message}" if ENV["DEBUG"]
|
|
140
143
|
nil
|
|
141
144
|
end unless defined?(save_ai_tools_to_initializer)
|
|
142
145
|
|
|
@@ -165,7 +168,8 @@ def add_ai_tool_to_initializer(format)
|
|
|
165
168
|
File.write(init_path, content)
|
|
166
169
|
puts "💾 Set config.ai_tools = %i[#{format_sym}]"
|
|
167
170
|
end
|
|
168
|
-
rescue
|
|
171
|
+
rescue => e
|
|
172
|
+
$stderr.puts "[rails-ai-context] add_ai_tool_to_initializer failed: #{e.message}" if ENV["DEBUG"]
|
|
169
173
|
nil
|
|
170
174
|
end unless defined?(add_ai_tool_to_initializer)
|
|
171
175
|
|
|
@@ -202,14 +202,20 @@ module RailsAiContext
|
|
|
202
202
|
lines << "## Services (#{found.size})"
|
|
203
203
|
found.each do |path|
|
|
204
204
|
relative = path.sub("#{root}/", "")
|
|
205
|
-
source =
|
|
205
|
+
source = begin
|
|
206
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
207
|
+
rescue => e
|
|
208
|
+
$stderr.puts "[rails-ai-context] discover_services failed: #{e.message}" if ENV["DEBUG"]
|
|
209
|
+
next
|
|
210
|
+
end
|
|
206
211
|
line_count = source.lines.size
|
|
207
212
|
methods = source.scan(/\A\s*def (?:self\.)?(\w+)/m).flatten.reject { |m| m == "initialize" }
|
|
208
213
|
lines << "- `#{relative}` (#{line_count} lines)"
|
|
209
214
|
lines << " Methods: #{methods.first(20).join(', ')}" if methods.any?
|
|
210
215
|
end
|
|
211
216
|
lines << ""
|
|
212
|
-
rescue
|
|
217
|
+
rescue => e
|
|
218
|
+
$stderr.puts "[rails-ai-context] discover_services failed: #{e.message}" if ENV["DEBUG"]
|
|
213
219
|
nil
|
|
214
220
|
end
|
|
215
221
|
|
|
@@ -226,13 +232,19 @@ module RailsAiContext
|
|
|
226
232
|
lines << "## Jobs (#{found.size})"
|
|
227
233
|
found.each do |path|
|
|
228
234
|
relative = path.sub("#{root}/", "")
|
|
229
|
-
source =
|
|
235
|
+
source = begin
|
|
236
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
237
|
+
rescue => e
|
|
238
|
+
$stderr.puts "[rails-ai-context] discover_jobs failed: #{e.message}" if ENV["DEBUG"]
|
|
239
|
+
next
|
|
240
|
+
end
|
|
230
241
|
queue = source.match(/queue_as\s+[:'"](\w+)/)&.captures&.first || "default"
|
|
231
242
|
retries = source.match(/retry_on.*attempts:\s*(\d+)/)&.captures&.first
|
|
232
243
|
lines << "- `#{relative}` (queue: #{queue}#{retries ? ", retries: #{retries}" : ""})"
|
|
233
244
|
end
|
|
234
245
|
lines << ""
|
|
235
|
-
rescue
|
|
246
|
+
rescue => e
|
|
247
|
+
$stderr.puts "[rails-ai-context] discover_jobs failed: #{e.message}" if ENV["DEBUG"]
|
|
236
248
|
nil
|
|
237
249
|
end
|
|
238
250
|
|
|
@@ -249,7 +261,12 @@ module RailsAiContext
|
|
|
249
261
|
lines << "## Views (#{found.size})"
|
|
250
262
|
found.each do |path|
|
|
251
263
|
relative = path.sub("#{views_dir}/", "")
|
|
252
|
-
source =
|
|
264
|
+
source = begin
|
|
265
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
266
|
+
rescue => e
|
|
267
|
+
$stderr.puts "[rails-ai-context] discover_views failed: #{e.message}" if ENV["DEBUG"]
|
|
268
|
+
next
|
|
269
|
+
end
|
|
253
270
|
line_count = source.lines.size
|
|
254
271
|
partials = source.scan(/render\s+(?:partial:\s*)?["']([^"']+)["']/).flatten
|
|
255
272
|
stimulus = source.scan(/data-controller=["']([^"']+)["']/).flat_map { |m| m.first.split }
|
|
@@ -259,7 +276,8 @@ module RailsAiContext
|
|
|
259
276
|
lines << detail
|
|
260
277
|
end
|
|
261
278
|
lines << ""
|
|
262
|
-
rescue
|
|
279
|
+
rescue => e
|
|
280
|
+
$stderr.puts "[rails-ai-context] discover_views failed: #{e.message}" if ENV["DEBUG"]
|
|
263
281
|
nil
|
|
264
282
|
end
|
|
265
283
|
|
|
@@ -315,13 +333,19 @@ module RailsAiContext
|
|
|
315
333
|
lines << "## Tests (#{found.size})"
|
|
316
334
|
found.each do |path|
|
|
317
335
|
relative = path.sub("#{root}/", "")
|
|
318
|
-
source =
|
|
336
|
+
source = begin
|
|
337
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
338
|
+
rescue => e
|
|
339
|
+
$stderr.puts "[rails-ai-context] discover_tests failed: #{e.message}" if ENV["DEBUG"]
|
|
340
|
+
next
|
|
341
|
+
end
|
|
319
342
|
test_count = source.scan(/\b(?:it|test|should)\b/).size
|
|
320
343
|
lines << "- `#{relative}` (#{test_count} tests)"
|
|
321
344
|
end
|
|
322
345
|
lines << ""
|
|
323
346
|
found
|
|
324
|
-
rescue
|
|
347
|
+
rescue => e
|
|
348
|
+
$stderr.puts "[rails-ai-context] discover_tests failed: #{e.message}" if ENV["DEBUG"]
|
|
325
349
|
[]
|
|
326
350
|
end
|
|
327
351
|
|
|
@@ -377,7 +401,8 @@ module RailsAiContext
|
|
|
377
401
|
lines << "## Test Coverage Gaps"
|
|
378
402
|
gaps.each { |g| lines << "- #{g}" }
|
|
379
403
|
lines << ""
|
|
380
|
-
rescue
|
|
404
|
+
rescue => e
|
|
405
|
+
$stderr.puts "[rails-ai-context] discover_test_gaps failed: #{e.message}" if ENV["DEBUG"]
|
|
381
406
|
nil
|
|
382
407
|
end
|
|
383
408
|
|
|
@@ -392,14 +417,20 @@ module RailsAiContext
|
|
|
392
417
|
# Fallback: read source file
|
|
393
418
|
path = Rails.root.join("app", "controllers", "#{parent_class.underscore}.rb")
|
|
394
419
|
return [] unless File.exist?(path)
|
|
395
|
-
source =
|
|
420
|
+
source = begin
|
|
421
|
+
File.read(path, encoding: "UTF-8")
|
|
422
|
+
rescue => e
|
|
423
|
+
$stderr.puts "[rails-ai-context] detect_parent_filters_for_analyze failed: #{e.message}" if ENV["DEBUG"]
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
396
426
|
return [] unless source
|
|
397
427
|
|
|
398
428
|
source.each_line.filter_map do |line|
|
|
399
429
|
next if line.include?("only:") || line.include?("except:")
|
|
400
430
|
{ name: $1 } if line.match(/\A\s*before_action\s+:(\w+)/)
|
|
401
431
|
end
|
|
402
|
-
rescue
|
|
432
|
+
rescue => e
|
|
433
|
+
$stderr.puts "[rails-ai-context] detect_parent_filters_for_analyze failed: #{e.message}" if ENV["DEBUG"]
|
|
403
434
|
[]
|
|
404
435
|
end
|
|
405
436
|
|
|
@@ -444,7 +475,8 @@ module RailsAiContext
|
|
|
444
475
|
lines << "## Concerns"
|
|
445
476
|
concerns.sort.each { |name, count| lines << "- **#{name}** (used by #{count} model#{'s' if count > 1})" }
|
|
446
477
|
lines << ""
|
|
447
|
-
rescue
|
|
478
|
+
rescue => e
|
|
479
|
+
$stderr.puts "[rails-ai-context] discover_concerns failed: #{e.message}" if ENV["DEBUG"]
|
|
448
480
|
nil
|
|
449
481
|
end
|
|
450
482
|
|
|
@@ -481,7 +513,8 @@ module RailsAiContext
|
|
|
481
513
|
lines << "- `#{relative}`"
|
|
482
514
|
end
|
|
483
515
|
lines << ""
|
|
484
|
-
rescue
|
|
516
|
+
rescue => e
|
|
517
|
+
$stderr.puts "[rails-ai-context] discover_channels failed: #{e.message}" if ENV["DEBUG"]
|
|
485
518
|
nil
|
|
486
519
|
end
|
|
487
520
|
|
|
@@ -496,12 +529,18 @@ module RailsAiContext
|
|
|
496
529
|
lines << "## Mailers (#{found.size})"
|
|
497
530
|
found.each do |path|
|
|
498
531
|
relative = path.sub("#{root}/", "")
|
|
499
|
-
source =
|
|
532
|
+
source = begin
|
|
533
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
534
|
+
rescue => e
|
|
535
|
+
$stderr.puts "[rails-ai-context] discover_mailers failed: #{e.message}" if ENV["DEBUG"]
|
|
536
|
+
next
|
|
537
|
+
end
|
|
500
538
|
methods = source.scan(/\A\s*def (\w+)/m).flatten.reject { |m| m == "initialize" }
|
|
501
539
|
lines << "- `#{relative}` — #{methods.join(', ')}" if methods.any?
|
|
502
540
|
end
|
|
503
541
|
lines << ""
|
|
504
|
-
rescue
|
|
542
|
+
rescue => e
|
|
543
|
+
$stderr.puts "[rails-ai-context] discover_mailers failed: #{e.message}" if ENV["DEBUG"]
|
|
505
544
|
nil
|
|
506
545
|
end
|
|
507
546
|
|
|
@@ -519,7 +558,8 @@ module RailsAiContext
|
|
|
519
558
|
lines << "- `#{f[:file]}`: #{f[:issue]}"
|
|
520
559
|
end
|
|
521
560
|
lines << ""
|
|
522
|
-
rescue
|
|
561
|
+
rescue => e
|
|
562
|
+
$stderr.puts "[rails-ai-context] discover_accessibility failed: #{e.message}" if ENV["DEBUG"]
|
|
523
563
|
nil
|
|
524
564
|
end
|
|
525
565
|
|
|
@@ -543,7 +583,8 @@ module RailsAiContext
|
|
|
543
583
|
lines << "- **#{c[:name]}**#{slot_info}#{used_in}"
|
|
544
584
|
end
|
|
545
585
|
lines << ""
|
|
546
|
-
rescue
|
|
586
|
+
rescue => e
|
|
587
|
+
$stderr.puts "[rails-ai-context] discover_components failed: #{e.message}" if ENV["DEBUG"]
|
|
547
588
|
nil
|
|
548
589
|
end
|
|
549
590
|
|
|
@@ -556,7 +597,12 @@ module RailsAiContext
|
|
|
556
597
|
dirs.each do |dir|
|
|
557
598
|
Dir.glob(File.join(dir, "**", "*.rb")).each do |path|
|
|
558
599
|
next unless File.basename(path, ".rb").include?(pattern) || path.downcase.include?(pattern)
|
|
559
|
-
source =
|
|
600
|
+
source = begin
|
|
601
|
+
File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
602
|
+
rescue => e
|
|
603
|
+
$stderr.puts "[rails-ai-context] discover_env_dependencies failed: #{e.message}" if ENV["DEBUG"]
|
|
604
|
+
next
|
|
605
|
+
end
|
|
560
606
|
source.scan(/ENV\[["']([^"']+)["']\]|ENV\.fetch\(["']([^"']+)["']\)/).each do |m|
|
|
561
607
|
env_vars << (m[0] || m[1])
|
|
562
608
|
end
|
|
@@ -567,7 +613,8 @@ module RailsAiContext
|
|
|
567
613
|
lines << "## Environment Dependencies"
|
|
568
614
|
env_vars.sort.each { |v| lines << "- `#{v}`" }
|
|
569
615
|
lines << ""
|
|
570
|
-
rescue
|
|
616
|
+
rescue => e
|
|
617
|
+
$stderr.puts "[rails-ai-context] discover_env_dependencies failed: #{e.message}" if ENV["DEBUG"]
|
|
571
618
|
nil
|
|
572
619
|
end
|
|
573
620
|
end
|
|
@@ -150,7 +150,7 @@ module RailsAiContext
|
|
|
150
150
|
# App size classification — tools use this to auto-tune pagination and detail
|
|
151
151
|
# small: <15 models, medium: 15-50 models, large: 50+ models
|
|
152
152
|
def app_size
|
|
153
|
-
ctx = SHARED_CACHE[:context]
|
|
153
|
+
ctx = SHARED_CACHE[:mutex].synchronize { SHARED_CACHE[:context] }
|
|
154
154
|
return :medium unless ctx
|
|
155
155
|
|
|
156
156
|
model_count = ctx[:models]&.size || 0
|
|
@@ -377,7 +377,8 @@ module RailsAiContext
|
|
|
377
377
|
end
|
|
378
378
|
|
|
379
379
|
lines
|
|
380
|
-
rescue
|
|
380
|
+
rescue => e
|
|
381
|
+
$stderr.puts "[rails-ai-context] gather_git_context failed: #{e.message}" if ENV["DEBUG"]
|
|
381
382
|
[]
|
|
382
383
|
end
|
|
383
384
|
|
|
@@ -390,7 +391,8 @@ module RailsAiContext
|
|
|
390
391
|
return [] if text.include?("Log file is empty") || text.include?("not found") || text.include?("No entries")
|
|
391
392
|
|
|
392
393
|
[ "## Recent Error Logs", text, "" ]
|
|
393
|
-
rescue
|
|
394
|
+
rescue => e
|
|
395
|
+
$stderr.puts "[rails-ai-context] gather_log_context failed: #{e.message}" if ENV["DEBUG"]
|
|
394
396
|
[]
|
|
395
397
|
end
|
|
396
398
|
end
|
|
@@ -257,7 +257,8 @@ module RailsAiContext
|
|
|
257
257
|
start_line: start_idx + 1,
|
|
258
258
|
end_line: end_idx + 1
|
|
259
259
|
}
|
|
260
|
-
rescue
|
|
260
|
+
rescue => e
|
|
261
|
+
$stderr.puts "[rails-ai-context] extract_method_source failed: #{e.message}" if ENV["DEBUG"]
|
|
261
262
|
nil
|
|
262
263
|
end
|
|
263
264
|
|
|
@@ -294,7 +295,8 @@ module RailsAiContext
|
|
|
294
295
|
end
|
|
295
296
|
|
|
296
297
|
concern_callbacks
|
|
297
|
-
rescue
|
|
298
|
+
rescue => e
|
|
299
|
+
$stderr.puts "[rails-ai-context] find_concern_callbacks failed: #{e.message}" if ENV["DEBUG"]
|
|
298
300
|
{}
|
|
299
301
|
end
|
|
300
302
|
end
|