rails-ai-context 4.2.3 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -7
- data/SECURITY.md +2 -1
- data/docs/GUIDE.md +3 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
- data/lib/rails_ai_context/configuration.rb +4 -2
- data/lib/rails_ai_context/doctor.rb +6 -1
- data/lib/rails_ai_context/fingerprinter.rb +24 -0
- data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
- data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
- data/lib/rails_ai_context/serializers/claude_serializer.rb +42 -11
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +165 -64
- data/lib/rails_ai_context/server.rb +12 -1
- data/lib/rails_ai_context/tools/base_tool.rb +63 -1
- data/lib/rails_ai_context/tools/diagnose.rb +436 -0
- data/lib/rails_ai_context/tools/generate_test.rb +571 -0
- data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
- data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
- data/lib/rails_ai_context/tools/get_context.rb +70 -8
- data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
- data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
- data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
- data/lib/rails_ai_context/tools/get_env.rb +51 -24
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
- data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
- data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
- data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
- data/lib/rails_ai_context/tools/get_view.rb +65 -9
- data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
- data/lib/rails_ai_context/tools/onboard.rb +755 -0
- data/lib/rails_ai_context/tools/query.rb +4 -2
- data/lib/rails_ai_context/tools/read_logs.rb +4 -1
- data/lib/rails_ai_context/tools/review_changes.rb +299 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
- data/lib/rails_ai_context/tools/search_code.rb +23 -4
- data/lib/rails_ai_context/tools/security_scan.rb +7 -1
- data/lib/rails_ai_context/tools/session_context.rb +132 -0
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +10 -4
|
@@ -24,20 +24,20 @@ module RailsAiContext
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def tools_header
|
|
27
|
-
"## Tools (
|
|
27
|
+
"## Tools (39) — MANDATORY, Use Before Read"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def tools_intro
|
|
31
31
|
case tool_mode
|
|
32
32
|
when :cli
|
|
33
33
|
[
|
|
34
|
-
"This project has
|
|
34
|
+
"This project has 39 introspection tools. **MANDATORY — use these instead of reading files.**",
|
|
35
35
|
"They return only relevant, structured data and save tokens. Read files ONLY when you are about to Edit them.",
|
|
36
36
|
""
|
|
37
37
|
]
|
|
38
38
|
else
|
|
39
39
|
[
|
|
40
|
-
"This project has
|
|
40
|
+
"This project has 39 MCP tools via `rails ai:serve` (configured in `.mcp.json`).",
|
|
41
41
|
"**MANDATORY — use these instead of reading files.** They return structured data and save tokens.",
|
|
42
42
|
"Read files ONLY when you are about to Edit them.",
|
|
43
43
|
"If MCP tools are not connected, use CLI fallback: `#{cli_cmd("TOOL_NAME", "param=value")}`",
|
|
@@ -46,57 +46,92 @@ module RailsAiContext
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def
|
|
49
|
+
def tools_detail_guidance
|
|
50
|
+
detail_param = tool_mode == :cli ? "detail=summary" : "detail:\"summary\""
|
|
50
51
|
[
|
|
51
|
-
"###
|
|
52
|
+
"### detail parameter — ALWAYS start with summary",
|
|
52
53
|
"",
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
"Most tools accept `#{detail_param}`. Use the right level:",
|
|
55
|
+
"- **summary** — first call, orient yourself (table list, model names, route overview)",
|
|
56
|
+
"- **standard** — working detail (columns with types, associations, action source) — DEFAULT",
|
|
57
|
+
"- **full** — only when you need indexes, foreign keys, code snippets, or complete content",
|
|
57
58
|
"",
|
|
58
|
-
"
|
|
59
|
-
|
|
59
|
+
"Pattern: summary to find the target → standard to understand it → full only if needed.",
|
|
60
|
+
""
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tools_power_tool_section
|
|
65
|
+
[
|
|
66
|
+
"### Start here — composite tools save multiple calls",
|
|
60
67
|
"",
|
|
61
|
-
"**
|
|
62
|
-
tool_call("
|
|
63
|
-
tool_call("rails_get_model_details(model:\"Cook\")", cli_cmd("model_details", "model=Cook")),
|
|
64
|
-
tool_call("rails_migration_advisor(action:\"add_column\", table:\"cooks\", column:\"rating\", type:\"integer\")", cli_cmd("migration_advisor", "action=add_column table=cooks column=rating type=integer")),
|
|
68
|
+
"**New to this project?** Get a full walkthrough first:",
|
|
69
|
+
tool_call("rails_onboard(detail:\"standard\")", cli_cmd("onboard", "detail=standard")),
|
|
65
70
|
"",
|
|
66
|
-
"**
|
|
67
|
-
tool_call("
|
|
71
|
+
"**`get_context` is your power tool** — bundles schema + model + controller + routes + views in ONE call:",
|
|
72
|
+
tool_call("rails_get_context(controller:\"CooksController\", action:\"create\")", cli_cmd("context", "controller=CooksController action=create")),
|
|
73
|
+
tool_call("rails_get_context(model:\"Cook\")", cli_cmd("context", "model=Cook")),
|
|
74
|
+
tool_call("rails_get_context(feature:\"cook\")", cli_cmd("context", "feature=cook")),
|
|
68
75
|
"",
|
|
69
|
-
"**
|
|
70
|
-
tool_call("
|
|
71
|
-
tool_call("rails_get_view(controller:\"cooks\")", cli_cmd("view", "controller=cooks")),
|
|
72
|
-
tool_call("rails_get_partial_interface(partial:\"shared/status_badge\")", cli_cmd("partial_interface", "partial=shared/status_badge")),
|
|
73
|
-
tool_call("rails_get_component_catalog(component:\"Alert\")", cli_cmd("component_catalog", "component=Alert")),
|
|
76
|
+
"**`analyze_feature` for broad discovery** — scans all layers (models, controllers, routes, services, jobs, views, tests):",
|
|
77
|
+
tool_call("rails_analyze_feature(feature:\"authentication\")", cli_cmd("analyze_feature", "feature=authentication")),
|
|
74
78
|
"",
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
"Use individual tools only when you need deeper detail on a specific layer.",
|
|
80
|
+
""
|
|
81
|
+
]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def tools_workflow_section # rubocop:disable Metrics/MethodLength
|
|
85
|
+
[
|
|
86
|
+
"### Step-by-step workflows (follow this order)",
|
|
78
87
|
"",
|
|
79
|
-
"**
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
"**Modify a model** (add field, change validation, add scope):",
|
|
89
|
+
"1. #{tool_call_inline("rails_get_context", "model:\"Cook\"", "context", "model=Cook")} — schema + associations + validations in one call",
|
|
90
|
+
"2. Read the model file, make your edit",
|
|
91
|
+
"3. #{tool_call_inline("rails_migration_advisor", "action:\"add_column\", table:\"cooks\", column:\"rating\", type:\"integer\"", "migration_advisor", "action=add_column table=cooks column=rating type=integer")} — if schema change needed",
|
|
92
|
+
"4. #{tool_call_inline("rails_validate", "files:[\"app/models/cook.rb\"], level:\"rails\"", "validate", "files=app/models/cook.rb level=rails")} — EVERY time after editing",
|
|
93
|
+
"5. #{tool_call_inline("rails_generate_test", "model:\"Cook\"", "generate_test", "model=Cook")} — generate tests matching project patterns",
|
|
82
94
|
"",
|
|
83
|
-
"**
|
|
84
|
-
|
|
95
|
+
"**Fix a controller bug:**",
|
|
96
|
+
"1. #{tool_call_inline("rails_get_context", "controller:\"CooksController\", action:\"create\"", "context", "controller=CooksController action=create")} — action source + routes + views + model",
|
|
97
|
+
"2. Read the controller file, make your fix",
|
|
98
|
+
"3. #{tool_call_inline("rails_validate", "files:[\"app/controllers/cooks_controller.rb\"], level:\"rails\"", "validate", "files=app/controllers/cooks_controller.rb level=rails")}",
|
|
85
99
|
"",
|
|
86
|
-
"**
|
|
87
|
-
|
|
100
|
+
"**Build or modify a view:**",
|
|
101
|
+
"1. #{tool_call_inline("rails_get_design_system", "detail:\"standard\"", "design_system", "detail=standard")} — get copy-paste HTML/ERB patterns",
|
|
102
|
+
"2. #{tool_call_inline("rails_get_view", "controller:\"cooks\"", "view", "controller=cooks")} — existing templates, partials, Stimulus refs",
|
|
103
|
+
"3. #{tool_call_inline("rails_get_partial_interface", "partial:\"shared/status_badge\"", "partial_interface", "partial=shared/status_badge")} — partial locals contract",
|
|
104
|
+
"4. Read the view file, make your edit",
|
|
105
|
+
"5. #{tool_call_inline("rails_validate", "files:[\"app/views/cooks/index.html.erb\"]", "validate", "files=app/views/cooks/index.html.erb")}",
|
|
88
106
|
"",
|
|
89
|
-
"**
|
|
90
|
-
tool_call("
|
|
107
|
+
"**Trace a method:**",
|
|
108
|
+
tool_call("rails_search_code(pattern:\"can_cook?\", match_type:\"trace\")", cli_cmd("search_code", "pattern=\"can_cook?\" match_type=trace")),
|
|
91
109
|
"",
|
|
92
|
-
"**Debug
|
|
93
|
-
tool_call("
|
|
110
|
+
"**Debug an error (one call — gathers context + git + logs + fix):**",
|
|
111
|
+
tool_call("rails_diagnose(error:\"NoMethodError: undefined method `foo` for nil\", file:\"app/models/cook.rb\")", cli_cmd("diagnose", "error=\"NoMethodError: undefined method foo\" file=app/models/cook.rb")),
|
|
94
112
|
"",
|
|
95
|
-
"**
|
|
96
|
-
tool_call("
|
|
113
|
+
"**Review changes before merging:**",
|
|
114
|
+
tool_call("rails_review_changes(ref:\"main\")", cli_cmd("review_changes", "ref=main")),
|
|
97
115
|
"",
|
|
98
|
-
"**
|
|
99
|
-
tool_call("
|
|
116
|
+
"**Generate tests matching project patterns:**",
|
|
117
|
+
tool_call("rails_generate_test(model:\"Cook\")", cli_cmd("generate_test", "model=Cook")),
|
|
118
|
+
""
|
|
119
|
+
]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def tools_antipatterns_section
|
|
123
|
+
search_tool = tool_mode == :cli ? cli_cmd("search_code") : "rails_search_code"
|
|
124
|
+
validate_tool = tool_mode == :cli ? cli_cmd("validate") : "rails_validate"
|
|
125
|
+
[
|
|
126
|
+
"### Common mistakes — avoid these",
|
|
127
|
+
"",
|
|
128
|
+
"- **Don't read db/schema.rb** — use `get_schema`. It adds [indexed]/[unique] hints you'd miss.",
|
|
129
|
+
"- **Don't read model files for reference** — use `get_model_details`. It resolves concerns, inherited methods, and implicit belongs_to validations.",
|
|
130
|
+
"- **Prefer `#{search_tool}` over Grep** for method tracing and cross-layer search. It excludes sensitive files, supports `match_type:\"trace\"`, and paginates.",
|
|
131
|
+
"- **Don't call tools without a target** — `get_model_details()` without `model:` returns a paginated list, not an error. Always specify what you want.",
|
|
132
|
+
"- **Don't skip validation** — run `#{validate_tool}` after EVERY edit. It catches syntax errors AND Rails-specific issues (missing partials, bad column refs).",
|
|
133
|
+
"- **Don't ignore cross-references** — tool responses include `_Next:` hints suggesting the best follow-up call. Follow them.",
|
|
134
|
+
"- **Don't call `detail:\"full\"` first** — start with `summary` to find your target, then drill in. Full responses waste tokens.",
|
|
100
135
|
""
|
|
101
136
|
]
|
|
102
137
|
end
|
|
@@ -107,30 +142,32 @@ module RailsAiContext
|
|
|
107
142
|
[
|
|
108
143
|
"### Rules",
|
|
109
144
|
"",
|
|
110
|
-
"1.
|
|
111
|
-
"2. NEVER
|
|
112
|
-
"3.
|
|
113
|
-
"4. Read files ONLY
|
|
114
|
-
"5.
|
|
145
|
+
"1. **Use composite tools first** — `#{cli_cmd("context")}` and `#{cli_cmd("analyze_feature")}` before individual tools",
|
|
146
|
+
"2. **NEVER read reference files** — db/schema.rb, config/routes.rb, model files, test files — tools are better",
|
|
147
|
+
"3. **Prefer `#{cli_cmd("search_code")}`** for tracing and cross-layer search — standard search tools are fine for simple targeted lookups",
|
|
148
|
+
"4. **Read files ONLY to Edit them** — not for reference",
|
|
149
|
+
"5. **Validate EVERY edit** — `#{cli_cmd("validate", "files=... level=rails")}`",
|
|
150
|
+
"6. **Follow _Next:_ hints** — tool responses suggest the best follow-up call",
|
|
115
151
|
""
|
|
116
152
|
]
|
|
117
153
|
else
|
|
118
154
|
[
|
|
119
155
|
"### Rules",
|
|
120
156
|
"",
|
|
121
|
-
"1.
|
|
122
|
-
"2. NEVER
|
|
123
|
-
"3.
|
|
124
|
-
"4. Read files ONLY
|
|
125
|
-
"5.
|
|
126
|
-
"6.
|
|
157
|
+
"1. **Use composite tools first** — `rails_get_context` and `rails_analyze_feature` before individual tools",
|
|
158
|
+
"2. **NEVER read reference files** — db/schema.rb, config/routes.rb, model files, test files — tools are better",
|
|
159
|
+
"3. **Prefer `rails_search_code`** for tracing and cross-layer search — standard search tools are fine for simple targeted lookups",
|
|
160
|
+
"4. **Read files ONLY to Edit them** — not for reference",
|
|
161
|
+
"5. **Validate EVERY edit** — `rails_validate(files:[...], level:\"rails\")`",
|
|
162
|
+
"6. **Follow _Next:_ hints** — tool responses suggest the best follow-up call",
|
|
163
|
+
"7. If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`",
|
|
127
164
|
""
|
|
128
165
|
]
|
|
129
166
|
end
|
|
130
167
|
end
|
|
131
168
|
|
|
132
169
|
def tools_table # rubocop:disable Metrics/MethodLength
|
|
133
|
-
lines = [ "### All
|
|
170
|
+
lines = [ "### All 39 Tools", "" ]
|
|
134
171
|
|
|
135
172
|
if tool_mode == :cli
|
|
136
173
|
lines.concat(tools_table_cli)
|
|
@@ -145,11 +182,11 @@ module RailsAiContext
|
|
|
145
182
|
[
|
|
146
183
|
"| MCP | CLI | What it does |",
|
|
147
184
|
"|-----|-----|-------------|",
|
|
185
|
+
"| `rails_get_context(model:\"X\")` | `#{cli_cmd("context", "model=X")}` | **START HERE** — schema + model + controller + routes + views in one call |",
|
|
148
186
|
"| `rails_analyze_feature(feature:\"X\")` | `#{cli_cmd("analyze_feature", "feature=X")}` | Full-stack: models + controllers + routes + services + jobs + views + tests |",
|
|
149
|
-
"| `
|
|
150
|
-
"| `rails_search_code(pattern:\"X\", match_type:\"trace\")` | `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Trace: definition + source + siblings + callers + test coverage |",
|
|
187
|
+
"| `rails_search_code(pattern:\"X\", match_type:\"trace\")` | `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Search + trace: definition, source, callers, test coverage. Also: `match_type:\"any\"` for regex search |",
|
|
151
188
|
"| `rails_get_controllers(controller:\"X\", action:\"Y\")` | `#{cli_cmd("controllers", "controller=X action=Y")}` | Action source + inherited filters + render map + private methods |",
|
|
152
|
-
"| `rails_validate(files:[...], level:\"rails\")` | `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation
|
|
189
|
+
"| `rails_validate(files:[...], level:\"rails\")` | `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation (run after EVERY edit) |",
|
|
153
190
|
"| `rails_get_schema(table:\"X\")` | `#{cli_cmd("schema", "table=X")}` | Columns with [indexed]/[unique]/[encrypted]/[default] hints |",
|
|
154
191
|
"| `rails_get_model_details(model:\"X\")` | `#{cli_cmd("model_details", "model=X")}` | Associations, validations, scopes, enums, macros, delegations |",
|
|
155
192
|
"| `rails_get_routes(controller:\"X\")` | `#{cli_cmd("routes", "controller=X")}` | Routes with code-ready helpers and controller filters inline |",
|
|
@@ -160,7 +197,6 @@ module RailsAiContext
|
|
|
160
197
|
"| `rails_get_concern(name:\"X\", detail:\"full\")` | `#{cli_cmd("concern", "name=X detail=full")}` | Concern methods with source + which models include it |",
|
|
161
198
|
"| `rails_get_callbacks(model:\"X\")` | `#{cli_cmd("callbacks", "model=X")}` | Callbacks in Rails execution order with source |",
|
|
162
199
|
"| `rails_get_edit_context(file:\"X\", near:\"Y\")` | `#{cli_cmd("edit_context", "file=X near=Y")}` | Code around a match with class/method context |",
|
|
163
|
-
"| `rails_search_code(pattern:\"X\")` | `#{cli_cmd("search_code", "pattern=X")}` | Regex search + `exclude_tests` + `group_by_file` + pagination |",
|
|
164
200
|
"| `rails_get_service_pattern` | `#{cli_cmd("service_pattern")}` | Service objects: interface, dependencies, side effects, callers |",
|
|
165
201
|
"| `rails_get_job_pattern` | `#{cli_cmd("job_pattern")}` | Jobs: queue, retries, guard clauses, broadcasts, schedules |",
|
|
166
202
|
"| `rails_get_env` | `#{cli_cmd("env")}` | Environment variables + credentials keys (not values) |",
|
|
@@ -178,7 +214,13 @@ module RailsAiContext
|
|
|
178
214
|
"| `rails_get_frontend_stack` | `#{cli_cmd("frontend_stack")}` | React/Vue/Svelte/Angular, Inertia, TypeScript, package manager |",
|
|
179
215
|
"| `rails_search_docs(query:\"X\")` | `#{cli_cmd("search_docs", "query=X")}` | Bundled topic index with weighted keyword search, on-demand GitHub fetch |",
|
|
180
216
|
"| `rails_query(sql:\"X\")` | `#{cli_cmd("query", "sql=X")}` | Safe read-only SQL queries with timeout, row limit, column redaction |",
|
|
181
|
-
"| `rails_read_logs(level:\"X\")` | `#{cli_cmd("read_logs", "level=X")}` | Reverse file tail with level filtering and sensitive data redaction |"
|
|
217
|
+
"| `rails_read_logs(level:\"X\")` | `#{cli_cmd("read_logs", "level=X")}` | Reverse file tail with level filtering and sensitive data redaction |",
|
|
218
|
+
"| `rails_generate_test(model:\"X\")` | `#{cli_cmd("generate_test", "model=X")}` | Generate test scaffolding matching project patterns (framework, factories, style) |",
|
|
219
|
+
"| `rails_diagnose(error:\"X\")` | `#{cli_cmd("diagnose", "error=\"X\"")}` | One-call error diagnosis: context + git changes + logs + fix suggestions |",
|
|
220
|
+
"| `rails_review_changes(ref:\"main\")` | `#{cli_cmd("review_changes", "ref=main")}` | PR/commit review: file context + warnings (missing indexes, removed validations) |",
|
|
221
|
+
"| `rails_onboard(detail:\"standard\")` | `#{cli_cmd("onboard", "detail=standard")}` | Narrative app walkthrough for new developers or AI agents |",
|
|
222
|
+
"| `rails_runtime_info(detail:\"standard\")` | `#{cli_cmd("runtime_info", "detail=standard")}` | Live runtime: DB pool, table sizes, cache stats, job queues, pending migrations |",
|
|
223
|
+
"| `rails_session_context(action:\"status\")` | `#{cli_cmd("session_context", "action=status")}` | Track what you've already queried, avoid redundant calls |"
|
|
182
224
|
]
|
|
183
225
|
end
|
|
184
226
|
|
|
@@ -186,11 +228,11 @@ module RailsAiContext
|
|
|
186
228
|
[
|
|
187
229
|
"| CLI | What it does |",
|
|
188
230
|
"|-----|-------------|",
|
|
231
|
+
"| `#{cli_cmd("context", "model=X")}` | **START HERE** — schema + model + controller + routes + views in one call |",
|
|
189
232
|
"| `#{cli_cmd("analyze_feature", "feature=X")}` | Full-stack: models + controllers + routes + services + jobs + views + tests |",
|
|
190
|
-
"| `#{cli_cmd("
|
|
191
|
-
"| `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Trace: definition + source + siblings + callers + test coverage |",
|
|
233
|
+
"| `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Search + trace: definition, source, callers, test coverage. Also: `match_type=any` for regex search |",
|
|
192
234
|
"| `#{cli_cmd("controllers", "controller=X action=Y")}` | Action source + inherited filters + render map + private methods |",
|
|
193
|
-
"| `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation
|
|
235
|
+
"| `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation (run after EVERY edit) |",
|
|
194
236
|
"| `#{cli_cmd("schema", "table=X")}` | Columns with [indexed]/[unique]/[encrypted]/[default] hints |",
|
|
195
237
|
"| `#{cli_cmd("model_details", "model=X")}` | Associations, validations, scopes, enums, macros, delegations |",
|
|
196
238
|
"| `#{cli_cmd("routes", "controller=X")}` | Routes with code-ready helpers and controller filters inline |",
|
|
@@ -201,7 +243,6 @@ module RailsAiContext
|
|
|
201
243
|
"| `#{cli_cmd("concern", "name=X detail=full")}` | Concern methods with source + which models include it |",
|
|
202
244
|
"| `#{cli_cmd("callbacks", "model=X")}` | Callbacks in Rails execution order with source |",
|
|
203
245
|
"| `#{cli_cmd("edit_context", "file=X near=Y")}` | Code around a match with class/method context |",
|
|
204
|
-
"| `#{cli_cmd("search_code", "pattern=X")}` | Regex search + `exclude_tests` + `group_by_file` + pagination |",
|
|
205
246
|
"| `#{cli_cmd("service_pattern")}` | Service objects: interface, dependencies, side effects, callers |",
|
|
206
247
|
"| `#{cli_cmd("job_pattern")}` | Jobs: queue, retries, guard clauses, broadcasts, schedules |",
|
|
207
248
|
"| `#{cli_cmd("env")}` | Environment variables + credentials keys (not values) |",
|
|
@@ -219,22 +260,69 @@ module RailsAiContext
|
|
|
219
260
|
"| `#{cli_cmd("frontend_stack")}` | React/Vue/Svelte/Angular, Inertia, TypeScript, package manager |",
|
|
220
261
|
"| `#{cli_cmd("search_docs", "query=X")}` | Bundled topic index with weighted keyword search, on-demand GitHub fetch |",
|
|
221
262
|
"| `#{cli_cmd("query", "sql=X")}` | Safe read-only SQL queries with timeout, row limit, column redaction |",
|
|
222
|
-
"| `#{cli_cmd("read_logs", "level=X")}` | Reverse file tail with level filtering and sensitive data redaction |"
|
|
263
|
+
"| `#{cli_cmd("read_logs", "level=X")}` | Reverse file tail with level filtering and sensitive data redaction |",
|
|
264
|
+
"| `#{cli_cmd("generate_test", "model=X")}` | Generate test scaffolding matching project patterns (framework, factories, style) |",
|
|
265
|
+
"| `#{cli_cmd("diagnose", "error=\"X\"")}` | One-call error diagnosis: context + git changes + logs + fix suggestions |",
|
|
266
|
+
"| `#{cli_cmd("review_changes", "ref=main")}` | PR/commit review: file context + warnings (missing indexes, removed validations) |",
|
|
267
|
+
"| `#{cli_cmd("onboard", "detail=standard")}` | Narrative app walkthrough for new developers or AI agents |",
|
|
268
|
+
"| `#{cli_cmd("runtime_info", "detail=standard")}` | Live runtime: DB pool, table sizes, cache stats, job queues, pending migrations |",
|
|
269
|
+
"| `#{cli_cmd("session_context", "action=status")}` | Track what you've already queried, avoid redundant calls |"
|
|
223
270
|
]
|
|
224
271
|
end
|
|
225
272
|
|
|
226
|
-
# Full tool guide section — used by
|
|
273
|
+
# Full tool guide section — used by split rules files (.claude/rules/, .cursor/rules/, etc.)
|
|
227
274
|
def render_tools_guide
|
|
228
275
|
lines = []
|
|
229
276
|
lines << tools_header
|
|
230
277
|
lines << ""
|
|
231
278
|
lines.concat(tools_intro)
|
|
232
|
-
lines.concat(
|
|
279
|
+
lines.concat(tools_detail_guidance)
|
|
280
|
+
lines.concat(tools_power_tool_section)
|
|
281
|
+
lines.concat(tools_workflow_section)
|
|
282
|
+
lines.concat(tools_antipatterns_section)
|
|
233
283
|
lines.concat(tools_rules_section)
|
|
234
284
|
lines.concat(tools_table)
|
|
235
285
|
lines
|
|
236
286
|
end
|
|
237
287
|
|
|
288
|
+
# Compact tool guide for root files (CLAUDE.md, AGENTS.md) that have line limits.
|
|
289
|
+
# Includes power tools + workflows + rules + dense tool name list (no table).
|
|
290
|
+
def render_tools_guide_compact
|
|
291
|
+
lines = []
|
|
292
|
+
lines << tools_header
|
|
293
|
+
lines << ""
|
|
294
|
+
lines.concat(tools_intro)
|
|
295
|
+
lines.concat(tools_power_tool_section)
|
|
296
|
+
lines.concat(tools_workflow_section)
|
|
297
|
+
lines.concat(tools_antipatterns_section)
|
|
298
|
+
lines.concat(tools_rules_section)
|
|
299
|
+
lines.concat(tools_name_list)
|
|
300
|
+
lines
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Dense one-line-per-tool listing — fits in compact mode without the table overhead
|
|
304
|
+
def tools_name_list
|
|
305
|
+
all_tools = %w[
|
|
306
|
+
rails_get_context rails_analyze_feature rails_search_code rails_get_controllers
|
|
307
|
+
rails_validate rails_get_schema rails_get_model_details rails_get_routes
|
|
308
|
+
rails_get_view rails_get_design_system rails_get_stimulus rails_get_test_info
|
|
309
|
+
rails_get_concern rails_get_callbacks rails_get_edit_context
|
|
310
|
+
rails_get_service_pattern rails_get_job_pattern rails_get_env
|
|
311
|
+
rails_get_partial_interface rails_get_turbo_map rails_get_helper_methods
|
|
312
|
+
rails_get_config rails_get_gems rails_get_conventions rails_security_scan
|
|
313
|
+
rails_get_component_catalog rails_performance_check rails_dependency_graph
|
|
314
|
+
rails_migration_advisor rails_get_frontend_stack rails_search_docs
|
|
315
|
+
rails_query rails_read_logs rails_generate_test rails_diagnose
|
|
316
|
+
rails_review_changes rails_onboard
|
|
317
|
+
rails_runtime_info rails_session_context
|
|
318
|
+
]
|
|
319
|
+
[
|
|
320
|
+
"### All #{all_tools.size} tools",
|
|
321
|
+
"`#{all_tools.join('` `')}`",
|
|
322
|
+
""
|
|
323
|
+
]
|
|
324
|
+
end
|
|
325
|
+
|
|
238
326
|
private
|
|
239
327
|
|
|
240
328
|
# Generate zsh-safe CLI command: rails 'ai:tool[name]' params
|
|
@@ -243,6 +331,19 @@ module RailsAiContext
|
|
|
243
331
|
cmd += " #{params}" if params
|
|
244
332
|
cmd
|
|
245
333
|
end
|
|
334
|
+
|
|
335
|
+
# Inline tool call for workflow steps (shorter format).
|
|
336
|
+
# mcp_name is the full MCP tool name (e.g. "rails_validate", "rails_get_context").
|
|
337
|
+
def tool_call_inline(mcp_name, mcp_params, cli_short, cli_params)
|
|
338
|
+
case tool_mode
|
|
339
|
+
when :cli
|
|
340
|
+
"`#{cli_cmd(cli_short, cli_params)}`"
|
|
341
|
+
when :mcp
|
|
342
|
+
"`#{mcp_name}(#{mcp_params})` or `#{cli_cmd(cli_short, cli_params)}`"
|
|
343
|
+
else
|
|
344
|
+
"`#{mcp_name}(#{mcp_params})`"
|
|
345
|
+
end
|
|
346
|
+
end
|
|
246
347
|
end
|
|
247
348
|
end
|
|
248
349
|
end
|
|
@@ -41,7 +41,13 @@ module RailsAiContext
|
|
|
41
41
|
Tools::GetFrontendStack,
|
|
42
42
|
Tools::SearchDocs,
|
|
43
43
|
Tools::Query,
|
|
44
|
-
Tools::ReadLogs
|
|
44
|
+
Tools::ReadLogs,
|
|
45
|
+
Tools::GenerateTest,
|
|
46
|
+
Tools::Diagnose,
|
|
47
|
+
Tools::ReviewChanges,
|
|
48
|
+
Tools::Onboard,
|
|
49
|
+
Tools::RuntimeInfo,
|
|
50
|
+
Tools::SessionContext
|
|
45
51
|
].freeze
|
|
46
52
|
|
|
47
53
|
def initialize(app, transport: :stdio)
|
|
@@ -113,6 +119,11 @@ module RailsAiContext
|
|
|
113
119
|
# Build a minimal Rack app that delegates to the MCP transport
|
|
114
120
|
rack_app = build_rack_app(transport, config.http_path)
|
|
115
121
|
|
|
122
|
+
unless config.http_bind == "127.0.0.1" || config.http_bind == "::1" || config.http_bind == "localhost"
|
|
123
|
+
$stderr.puts "[rails-ai-context] WARNING: MCP HTTP transport binding to #{config.http_bind} — " \
|
|
124
|
+
"this exposes all tools to the network without authentication. " \
|
|
125
|
+
"Use 127.0.0.1 (default) unless you have external auth in place."
|
|
126
|
+
end
|
|
116
127
|
$stderr.puts "[rails-ai-context] MCP server starting on #{config.http_bind}:#{config.http_port}#{config.http_path}"
|
|
117
128
|
$stderr.puts "[rails-ai-context] Tools: #{TOOLS.map { |t| t.tool_name }.join(', ')}"
|
|
118
129
|
maybe_start_live_reload(server)
|
|
@@ -12,6 +12,11 @@ module RailsAiContext
|
|
|
12
12
|
# for thread safety in multi-threaded servers (e.g., Puma).
|
|
13
13
|
SHARED_CACHE = { mutex: Mutex.new }
|
|
14
14
|
|
|
15
|
+
# Session-level context tracking. Lets AI avoid redundant queries
|
|
16
|
+
# by recording what tools have been called with what params.
|
|
17
|
+
# In-memory only — resets on server restart (matches conversation lifecycle).
|
|
18
|
+
SESSION_CONTEXT = { mutex: Mutex.new, queries: {} }
|
|
19
|
+
|
|
15
20
|
class << self
|
|
16
21
|
# Convenience: access the Rails app and cached introspection
|
|
17
22
|
def rails_app
|
|
@@ -52,12 +57,54 @@ module RailsAiContext
|
|
|
52
57
|
# Reset the shared cache. Used by LiveReload to invalidate on file change.
|
|
53
58
|
def reset_all_caches!
|
|
54
59
|
reset_cache!
|
|
60
|
+
session_reset!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ── Session context helpers ──────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
def session_record(tool_name, params, summary = nil)
|
|
66
|
+
SESSION_CONTEXT[:mutex].synchronize do
|
|
67
|
+
key = session_key(tool_name, params)
|
|
68
|
+
SESSION_CONTEXT[:queries][key] = {
|
|
69
|
+
tool: tool_name.to_s,
|
|
70
|
+
params: params,
|
|
71
|
+
timestamp: Time.now.iso8601,
|
|
72
|
+
summary: summary
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def session_queried?(tool_name, **params)
|
|
78
|
+
SESSION_CONTEXT[:mutex].synchronize do
|
|
79
|
+
SESSION_CONTEXT[:queries].key?(session_key(tool_name, params))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def session_queries
|
|
84
|
+
SESSION_CONTEXT[:mutex].synchronize do
|
|
85
|
+
SESSION_CONTEXT[:queries].values.dup
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def session_reset!
|
|
90
|
+
SESSION_CONTEXT[:mutex].synchronize do
|
|
91
|
+
SESSION_CONTEXT[:queries].clear
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Auto-compress: if text exceeds 85% of max, call the fallback lambda for a shorter version
|
|
96
|
+
def auto_compress(full_text, &fallback)
|
|
97
|
+
max = config.max_tool_response_chars
|
|
98
|
+
return full_text if !max || full_text.length <= (max * 0.85).to_i
|
|
99
|
+
fallback ? fallback.call : full_text
|
|
55
100
|
end
|
|
56
101
|
|
|
57
102
|
# Structured not-found error with fuzzy suggestion and recovery hint.
|
|
58
103
|
# Helps AI agents self-correct without retrying blind.
|
|
59
104
|
def not_found_response(type, name, available, recovery_tool: nil)
|
|
60
105
|
suggestion = find_closest_match(name, available)
|
|
106
|
+
# Don't suggest the exact same string the user typed — that's useless
|
|
107
|
+
suggestion = nil if suggestion == name
|
|
61
108
|
lines = [ "#{type} '#{name}' not found." ]
|
|
62
109
|
lines << "Did you mean '#{suggestion}'?" if suggestion
|
|
63
110
|
lines << "Available: #{available.first(20).join(', ')}#{"..." if available.size > 20}"
|
|
@@ -108,8 +155,15 @@ module RailsAiContext
|
|
|
108
155
|
end
|
|
109
156
|
end
|
|
110
157
|
|
|
111
|
-
# Helper: wrap text in an MCP::Tool::Response with safety-net truncation
|
|
158
|
+
# Helper: wrap text in an MCP::Tool::Response with safety-net truncation.
|
|
159
|
+
# Auto-records the call in session context so session_context(action:"status") works.
|
|
112
160
|
def text_response(text)
|
|
161
|
+
# Auto-track: record this tool call in session context (skip SessionContext itself to avoid recursion)
|
|
162
|
+
if respond_to?(:tool_name) && tool_name != "rails_session_context"
|
|
163
|
+
summary = text.lines.first&.strip&.truncate(80)
|
|
164
|
+
session_record(tool_name, {}, summary)
|
|
165
|
+
end
|
|
166
|
+
|
|
113
167
|
max = RailsAiContext.configuration.max_tool_response_chars
|
|
114
168
|
if max && text.length > max
|
|
115
169
|
truncated = text[0...max]
|
|
@@ -119,6 +173,14 @@ module RailsAiContext
|
|
|
119
173
|
MCP::Tool::Response.new([ { type: "text", text: text } ])
|
|
120
174
|
end
|
|
121
175
|
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def session_key(tool_name, params)
|
|
180
|
+
normalized = tool_name.to_s.sub(/\Arails_/, "")
|
|
181
|
+
param_str = params.is_a?(Hash) ? params.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}:#{v}" }.join(",") : params.to_s
|
|
182
|
+
"#{normalized}:#{param_str}"
|
|
183
|
+
end
|
|
122
184
|
end
|
|
123
185
|
end
|
|
124
186
|
end
|