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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/CLAUDE.md +4 -4
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +7 -7
  6. data/SECURITY.md +2 -1
  7. data/docs/GUIDE.md +3 -3
  8. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
  9. data/lib/rails_ai_context/configuration.rb +4 -2
  10. data/lib/rails_ai_context/doctor.rb +6 -1
  11. data/lib/rails_ai_context/fingerprinter.rb +24 -0
  12. data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
  13. data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
  14. data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
  15. data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
  16. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
  17. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
  18. data/lib/rails_ai_context/serializers/claude_serializer.rb +42 -11
  19. data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
  20. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
  21. data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
  22. data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -2
  23. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +165 -64
  24. data/lib/rails_ai_context/server.rb +12 -1
  25. data/lib/rails_ai_context/tools/base_tool.rb +63 -1
  26. data/lib/rails_ai_context/tools/diagnose.rb +436 -0
  27. data/lib/rails_ai_context/tools/generate_test.rb +571 -0
  28. data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
  29. data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
  30. data/lib/rails_ai_context/tools/get_context.rb +70 -8
  31. data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
  32. data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
  33. data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
  34. data/lib/rails_ai_context/tools/get_env.rb +51 -24
  35. data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
  36. data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
  37. data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
  38. data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
  39. data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
  40. data/lib/rails_ai_context/tools/get_view.rb +65 -9
  41. data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
  42. data/lib/rails_ai_context/tools/onboard.rb +755 -0
  43. data/lib/rails_ai_context/tools/query.rb +4 -2
  44. data/lib/rails_ai_context/tools/read_logs.rb +4 -1
  45. data/lib/rails_ai_context/tools/review_changes.rb +299 -0
  46. data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
  47. data/lib/rails_ai_context/tools/search_code.rb +23 -4
  48. data/lib/rails_ai_context/tools/security_scan.rb +7 -1
  49. data/lib/rails_ai_context/tools/session_context.rb +132 -0
  50. data/lib/rails_ai_context/version.rb +1 -1
  51. metadata +10 -4
@@ -24,20 +24,20 @@ module RailsAiContext
24
24
  end
25
25
 
26
26
  def tools_header
27
- "## Tools (33) — MANDATORY, Use Before Read"
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 33 introspection tools. **MANDATORY — use these instead of reading files.**",
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 33 MCP tools via `rails ai:serve` (configured in `.mcp.json`).",
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 tools_task_section # rubocop:disable Metrics/MethodLength
49
+ def tools_detail_guidance
50
+ detail_param = tool_mode == :cli ? "detail=summary" : "detail:\"summary\""
50
51
  [
51
- "### What Are You Trying to Do?",
52
+ "### detail parameter ALWAYS start with summary",
52
53
  "",
53
- "**Understand a feature or area:**",
54
- tool_call("rails_analyze_feature(feature:\"cook\")", cli_cmd("analyze_feature", "feature=cook")),
55
- tool_call("rails_get_context(model:\"Cook\")", cli_cmd("context", "model=Cook")),
56
- tool_call("rails_get_frontend_stack", cli_cmd("frontend_stack")),
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
- "**Understand a method (who calls it, what it calls):**",
59
- tool_call("rails_search_code(pattern:\"can_cook?\", match_type:\"trace\")", cli_cmd("search_code", "pattern=\"can_cook?\" match_type=trace")),
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
- "**Add a field or modify a model:**",
62
- tool_call("rails_get_schema(table:\"cooks\")", cli_cmd("schema", "table=cooks")),
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
- "**Fix a controller bug:**",
67
- tool_call("rails_get_controllers(controller:\"CooksController\", action:\"create\")", cli_cmd("controllers", "controller=CooksController action=create")),
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
- "**Build or modify a view:**",
70
- tool_call("rails_get_design_system(detail:\"standard\")", cli_cmd("design_system", "detail=standard")),
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
- "**Write tests:**",
76
- tool_call("rails_get_test_info(detail:\"standard\")", cli_cmd("test_info", "detail=standard")),
77
- tool_call("rails_get_test_info(model:\"Cook\")", cli_cmd("test_info", "model=Cook")),
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
- "**Find code:**",
80
- tool_call("rails_search_code(pattern:\"has_many\")", cli_cmd("search_code", "pattern=\"has_many\"")),
81
- tool_call("rails_search_code(pattern:\"create\", match_type:\"definition\")", cli_cmd("search_code", "pattern=create match_type=definition")),
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
- "**Check performance:**",
84
- tool_call("rails_performance_check(model:\"Cook\")", cli_cmd("performance_check", "model=Cook")),
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
- "**Understand dependencies:**",
87
- tool_call("rails_dependency_graph(model:\"Cook\", format:\"mermaid\")", cli_cmd("dependency_graph", "model=Cook format=mermaid")),
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
- "**Query the database:**",
90
- tool_call("rails_query(sql:\"SELECT COUNT(*) FROM users WHERE created_at > '2024-01-01'\")", cli_cmd("query", "sql=\"SELECT COUNT(*) FROM users WHERE created_at > '2024-01-01'\"")),
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 errors:**",
93
- tool_call("rails_read_logs(level:\"error\", lines:100)", cli_cmd("read_logs", "level=error lines=100")),
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
- "**Search docs:**",
96
- tool_call("rails_search_docs(query:\"active_record_queries\")", cli_cmd("search_docs", "query=active_record_queries")),
113
+ "**Review changes before merging:**",
114
+ tool_call("rails_review_changes(ref:\"main\")", cli_cmd("review_changes", "ref=main")),
97
115
  "",
98
- "**After editing (EVERY time):**",
99
- tool_call("rails_validate(files:[\"app/models/cook.rb\"], level:\"rails\")", cli_cmd("validate", "files=app/models/cook.rb level=rails")),
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. NEVER read db/schema.rb, config/routes.rb, model files, or test files for reference — use the CLI tools above",
111
- "2. NEVER use Grep or search agents for code searchuse `#{cli_cmd("search_code")}`",
112
- "3. NEVER run `ruby -c`, `erb`, or `node -c` use `#{cli_cmd("validate")}`",
113
- "4. Read files ONLY when you are about to Edit them",
114
- "5. Start with `detail=summary` to orient, then drill into specifics",
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. NEVER read db/schema.rb, config/routes.rb, model files, or test files for reference — use the MCP tools above",
122
- "2. NEVER use Grep or search agents for code searchuse `rails_search_code`",
123
- "3. NEVER run `ruby -c`, `erb`, or `node -c`use `rails_validate`",
124
- "4. Read files ONLY when you are about to Edit them",
125
- "5. Start with `detail:\"summary\"` to orient, then drill into specifics",
126
- "6. If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`",
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 33 Tools", "" ]
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
- "| `rails_get_context(model:\"X\")` | `#{cli_cmd("context", "model=X")}` | Composite: schema + model + controller + routes + views in one call |",
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 + Brakeman security |",
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("context", "model=X")}` | Composite: schema + model + controller + routes + views in one call |",
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 + Brakeman security |",
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 all serializers.
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(tools_task_section)
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