rails-ai-context 0.15.5 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9e71509ab3fcf2437fdd2616f8cb88140b478677abdd4ebd7cf6818eb8cead3
4
- data.tar.gz: c6847c4a500d762665ea2493c0ee56943bfe2e601b645bf5e9ec97df81f7e495
3
+ metadata.gz: 0eb46f9da0fc24418021dbb2ca4a6850017ab555ac0bbe98b589777ef30593a2
4
+ data.tar.gz: 5b77c8e0b1c7832c17ea84fffdc96524060fc594e4691f11a84b5e010158f925
5
5
  SHA512:
6
- metadata.gz: fb13a050a3e8200d1bac5f8d0d59bc1a67469a3fbe904682824541430f9f10755197fa3bd17858c08a2eedcc89f71c9da2dfffea8a8bcd0c177784f85ef511c6
7
- data.tar.gz: 4e05a8e22c395b4091d336268aa3e3994f416b11d174e63d922b5979b46695a222176a0871253ddd940638ead977e2917d14998618a91c25ad4a6f891eca9694
6
+ metadata.gz: 13a6f6e8e274cf47210b6cb94f160ba00e4cebe3349eb6c1e2612b4ec53561ab87d99690c9949c4eae389427bf7e80850b1c5a930283792bfa3294010303ec04
7
+ data.tar.gz: 5ae9e78447e0ec33496b608729be0088bf57f180a37f27690f7eaa0b6ca0a3c1a6637887a747d3de67bec808636a139be88e2b9ba72e71db0e07faa156541182
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.15.6] - 2026-03-22
9
+
10
+ ### Added
11
+
12
+ - **7 new configurable options** — `excluded_controllers`, `excluded_route_prefixes`, `excluded_concerns`, `excluded_filters`, `excluded_middleware`, `search_extensions`, `concern_paths` for stack-specific customization.
13
+ - **Configurable file size limits** — `max_file_size`, `max_test_file_size`, `max_schema_file_size`, `max_view_total_size`, `max_view_file_size`, `max_search_results`, `max_validate_files` all exposed via `Configuration`.
14
+ - **Class methods in model detail** — `rails_get_model_details` now shows class methods section.
15
+ - **Custom validate methods** — `validate :method_name` calls extracted from source and shown in model detail.
16
+
17
+ ### Fixed
18
+
19
+ - **Schema defaults always visible** — Null and Default columns always shown (NOT NULL marked bold). Previous token-saving logic accidentally hid critical migration data.
20
+ - **Optional associations** — `belongs_to` with `optional: true` now shows `[optional]` flag.
21
+ - **Concern methods inline** — shows public methods from concern source files (e.g. `PlanLimitable — can_cook?, increment_cook_count!`).
22
+ - **MCP tool error messages** — all tools now show available values on error/not-found for AI self-correction.
23
+
8
24
  ## [0.15.5] - 2026-03-22
9
25
 
10
26
  ### Fixed
data/README.md CHANGED
@@ -295,22 +295,45 @@ end
295
295
 
296
296
  | Option | Default | Description |
297
297
  |--------|---------|-------------|
298
+ | **Presets & Introspectors** | | |
298
299
  | `preset` | `:standard` | Introspector preset (`:standard` or `:full`) |
299
300
  | `introspectors` | 13 core | Array of introspector symbols |
301
+ | **Context Generation** | | |
300
302
  | `context_mode` | `:compact` | `:compact` (≤150 lines) or `:full` (dump everything) |
301
303
  | `claude_max_lines` | `150` | Max lines for CLAUDE.md in compact mode |
302
- | `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
303
- | `excluded_models` | internal Rails models | Models to skip during introspection |
304
- | `excluded_paths` | `node_modules tmp log vendor .git` | Paths excluded from code search |
305
- | `sensitive_patterns` | `.env .env.* config/master.key config/credentials.yml.enc config/credentials/*.yml.enc *.pem *.key` | File patterns blocked from search and read tools |
304
+ | `generate_root_files` | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
305
+ | `output_dir` | `Rails.root` | Output directory for generated context files |
306
+ | **MCP Server** | | |
307
+ | `server_name` | `"rails-ai-context"` | MCP server name |
308
+ | `server_version` | gem version | MCP server version |
306
309
  | `auto_mount` | `false` | Auto-mount HTTP MCP endpoint |
307
310
  | `http_path` | `"/mcp"` | HTTP endpoint path |
308
311
  | `http_port` | `6029` | HTTP server port |
309
312
  | `http_bind` | `"127.0.0.1"` | HTTP server bind address |
310
313
  | `cache_ttl` | `30` | Cache TTL in seconds |
314
+ | `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
311
315
  | `live_reload` | `:auto` | `:auto`, `true`, or `false` — MCP live reload |
312
316
  | `live_reload_debounce` | `1.5` | Debounce interval in seconds |
313
- | `generate_root_files` | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
317
+ | **Filtering & Exclusions** | | |
318
+ | `excluded_models` | internal Rails models | Models to skip during introspection |
319
+ | `excluded_paths` | `node_modules tmp log vendor .git` | Paths excluded from code search |
320
+ | `sensitive_patterns` | `.env .env.* config/master.key config/credentials.yml.enc config/credentials/*.yml.enc *.pem *.key` | File patterns blocked from search and read tools |
321
+ | `excluded_controllers` | `DeviseController` etc. | Controller classes hidden from listings |
322
+ | `excluded_route_prefixes` | `action_mailbox/ active_storage/ rails/` etc. | Route controller prefixes hidden with app_only |
323
+ | `excluded_concerns` | Rails/Devise/framework patterns | Regex patterns for concerns to hide |
324
+ | `excluded_filters` | `verify_authenticity_token` etc. | Framework filter names hidden from controller output |
325
+ | `excluded_middleware` | standard Rack/Rails middleware | Default middleware hidden from config output |
326
+ | **File Size Limits** | | |
327
+ | `max_file_size` | `2_000_000` | Per-file read limit for tools (bytes) |
328
+ | `max_test_file_size` | `500_000` | Test file read limit (bytes) |
329
+ | `max_schema_file_size` | `10_000_000` | schema.rb / structure.sql parse limit (bytes) |
330
+ | `max_view_total_size` | `5_000_000` | Total aggregated view content for UI patterns (bytes) |
331
+ | `max_view_file_size` | `500_000` | Per-view file during aggregation (bytes) |
332
+ | `max_search_results` | `100` | Max search results per call |
333
+ | `max_validate_files` | `20` | Max files per validate call |
334
+ | **Search & Discovery** | | |
335
+ | `search_extensions` | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
336
+ | `concern_paths` | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
314
337
  </details>
315
338
 
316
339
  ---
data/docs/GUIDE.md CHANGED
@@ -297,6 +297,8 @@ Returns model details: associations, validations, scopes, enums, callbacks, conc
297
297
  |-------|------|-------------|
298
298
  | `model` | string | Model class name (e.g. `User`). Case-insensitive. Omit for listing. |
299
299
  | `detail` | string | `summary` / `standard` (default) / `full`. Ignored when model is specified. |
300
+ | `limit` | integer | Max models to return when listing. Default: 50. |
301
+ | `offset` | integer | Skip models for pagination. Default: 0. |
300
302
 
301
303
  **Examples:**
302
304
 
@@ -329,6 +331,7 @@ Returns all routes: HTTP verbs, paths, controller actions, route names.
329
331
  | `detail` | string | `summary` / `standard` (default) / `full` |
330
332
  | `limit` | integer | Max routes to return. Default: 100 (standard), 200 (full). |
331
333
  | `offset` | integer | Skip routes for pagination. Default: 0. |
334
+ | `app_only` | boolean | Filter out internal Rails routes (Active Storage, Action Mailbox, Conductor, etc.). Default: true. |
332
335
 
333
336
  **Examples:**
334
337
 
@@ -386,7 +389,7 @@ rails_get_controllers(detail: "full")
386
389
 
387
390
  Returns application configuration. No parameters.
388
391
 
389
- **Returns:** cache store, session store, timezone, queue adapter, mailer settings, custom middleware, initializers, current attributes.
392
+ **Returns:** cache store, session store, timezone, queue adapter, mailer settings, custom middleware (framework defaults are filtered out), notable initializers, CurrentAttributes classes.
390
393
 
391
394
  ```
392
395
  rails_get_config()
@@ -441,6 +444,108 @@ rails_get_conventions()
441
444
  → Architecture: [MVC, Service objects, Concerns], Patterns: [STI, Polymorphism], ...
442
445
  ```
443
446
 
447
+ ### rails_get_stimulus
448
+
449
+ Returns Stimulus controller details: targets, values, actions, outlets, classes.
450
+
451
+ **Parameters:**
452
+
453
+ | Param | Type | Description |
454
+ |-------|------|-------------|
455
+ | `controller` | string | Specific Stimulus controller name (e.g. `hello`, `filter-form`). Case-insensitive. |
456
+ | `detail` | string | `summary` / `standard` (default) / `full` |
457
+ | `limit` | integer | Max controllers to return when listing. Default: 50. |
458
+ | `offset` | integer | Skip controllers for pagination. Default: 0. |
459
+
460
+ **Examples:**
461
+
462
+ ```
463
+ rails_get_stimulus()
464
+ → Standard: controller names with targets and actions
465
+
466
+ rails_get_stimulus(detail: "summary")
467
+ → Names with target/action counts
468
+
469
+ rails_get_stimulus(controller: "filter-form")
470
+ → Full detail: targets, actions, values, outlets, classes, file path
471
+
472
+ rails_get_stimulus(detail: "full")
473
+ → All controllers with all details
474
+ ```
475
+
476
+ ### rails_get_view
477
+
478
+ Returns view template contents, partials, and Stimulus controller references.
479
+
480
+ **Parameters:**
481
+
482
+ | Param | Type | Description |
483
+ |-------|------|-------------|
484
+ | `controller` | string | Filter views by controller name (e.g. `cooks`, `brand_profiles`). Use `layouts` for layout files. |
485
+ | `path` | string | Specific view path relative to `app/views` (e.g. `cooks/index.html.erb`). Returns full content. |
486
+ | `detail` | string | `summary` / `standard` (default) / `full` |
487
+
488
+ **Examples:**
489
+
490
+ ```
491
+ rails_get_view()
492
+ → Standard: all view files with partial/stimulus refs
493
+
494
+ rails_get_view(controller: "cooks")
495
+ → All templates and partials for CooksController
496
+
497
+ rails_get_view(path: "cooks/index.html.erb")
498
+ → Full template content
499
+
500
+ rails_get_view(controller: "layouts")
501
+ → Layout files
502
+
503
+ rails_get_view(controller: "cooks", detail: "full")
504
+ → Full template content for all cooks views
505
+ ```
506
+
507
+ ### rails_get_edit_context
508
+
509
+ Returns just enough context to make a surgical Edit to a file. Returns the target area with line numbers and surrounding code.
510
+
511
+ **Parameters:**
512
+
513
+ | Param | Type | Description |
514
+ |-------|------|-------------|
515
+ | `file` | string | **Required.** File path relative to Rails root (e.g. `app/models/cook.rb`). |
516
+ | `near` | string | **Required.** What to find — a method name, keyword, or string to locate (e.g. `scope`, `def index`). |
517
+ | `context_lines` | integer | Lines of context above and below the match. Default: 5. |
518
+
519
+ **Examples:**
520
+
521
+ ```
522
+ rails_get_edit_context(file: "app/models/cook.rb", near: "scope")
523
+ → Code around the first scope with line numbers, expanded to full method
524
+
525
+ rails_get_edit_context(file: "app/controllers/cooks_controller.rb", near: "def index")
526
+ → The index action source with surrounding context
527
+ ```
528
+
529
+ ### rails_validate
530
+
531
+ Validates syntax of multiple files at once (Ruby, ERB, JavaScript).
532
+
533
+ **Parameters:**
534
+
535
+ | Param | Type | Description |
536
+ |-------|------|-------------|
537
+ | `files` | array | **Required.** File paths relative to Rails root (e.g. `["app/models/cook.rb", "app/views/cooks/index.html.erb"]`). |
538
+
539
+ **Examples:**
540
+
541
+ ```
542
+ rails_validate(files: ["app/models/cook.rb"])
543
+ → ✓ app/models/cook.rb — syntax OK
544
+
545
+ rails_validate(files: ["app/models/cook.rb", "app/controllers/cooks_controller.rb", "app/views/cooks/index.html.erb"])
546
+ → Checks all three files, reports pass/fail for each
547
+ ```
548
+
444
549
  ### rails_search_code
445
550
 
446
551
  Ripgrep-powered regex search across the codebase.
@@ -450,8 +555,10 @@ Ripgrep-powered regex search across the codebase.
450
555
  | Param | Type | Description |
451
556
  |-------|------|-------------|
452
557
  | `pattern` | string | **Required.** Regex pattern to search for. |
558
+ | `path` | string | Subdirectory to search in (e.g. `app/models`, `config`). Default: entire app. |
453
559
  | `file_type` | string | Filter by file type (e.g. `rb`, `erb`, `js`). Alphanumeric only. |
454
- | `max_results` | integer | Max results to return. Default: 20, max: 100. |
560
+ | `max_results` | integer | Max results to return. Default: 30, max: 100. |
561
+ | `context_lines` | integer | Lines of context before and after each match (like grep -C). Default: 0, max: 5. |
455
562
 
456
563
  **Examples:**
457
564
 
@@ -464,18 +571,28 @@ rails_search_code(pattern: "class.*Controller", file_type: "rb")
464
571
 
465
572
  rails_search_code(pattern: "def create", file_type: "rb", max_results: 50)
466
573
  → First 50 create methods across the codebase
574
+
575
+ rails_search_code(pattern: "current_user", path: "app/controllers")
576
+ → Search only in app/controllers/
577
+
578
+ rails_search_code(pattern: "validates", context_lines: 2)
579
+ → Matches with 2 lines of context before and after
467
580
  ```
468
581
 
469
- **Security:** Uses `Open3.capture2` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` config.
582
+ **Security:** Uses `Open3.capture2` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` and `sensitive_patterns` config.
470
583
 
471
584
  ### Detail Level Summary
472
585
 
473
- | Level | What it returns | Default limit | Best for |
474
- |-------|----------------|---------------|----------|
586
+ All tools that support `detail` use these three levels. Default limits vary by tool — schema defaults shown below:
587
+
588
+ | Level | What it returns | Schema default limit | Best for |
589
+ |-------|----------------|---------------------|----------|
475
590
  | `summary` | Names + counts | 50 | Getting the landscape, understanding what exists |
476
591
  | `standard` | Names + key details | 15 | Working context, column types, action names |
477
592
  | `full` | Everything | 5 | Deep inspection, indexes, FKs, constraints |
478
593
 
594
+ Other tools default to higher limits (e.g. models/controllers/stimulus: 50 for all levels, routes: 100/200).
595
+
479
596
  ### Recommended Workflow
480
597
 
481
598
  1. **Start with `detail:"summary"`** to see what exists
@@ -636,6 +753,49 @@ RailsAiContext.configure do |config|
636
753
  # Paths to exclude from code search
637
754
  config.excluded_paths += %w[vendor/bundle]
638
755
 
756
+ # Sensitive file patterns blocked from search and read tools
757
+ # config.sensitive_patterns += %w[config/my_secret.yml]
758
+
759
+ # Controllers hidden from listings (e.g. Devise internals)
760
+ # config.excluded_controllers += %w[MyInternalController]
761
+
762
+ # Route prefixes hidden with app_only (e.g. admin frameworks)
763
+ # config.excluded_route_prefixes += %w[admin/]
764
+
765
+ # Regex patterns for concerns to hide from model output
766
+ # config.excluded_concerns += [/MyInternal::/]
767
+
768
+ # Framework filter names hidden from controller output
769
+ # config.excluded_filters += %w[my_internal_filter]
770
+
771
+ # Default middleware hidden from config output
772
+ # config.excluded_middleware += %w[MyMiddleware]
773
+
774
+ # --- File size limits ---
775
+
776
+ # Per-file read limit for tools (default: 2MB)
777
+ # config.max_file_size = 2_000_000
778
+
779
+ # Test file read limit (default: 500KB)
780
+ # config.max_test_file_size = 500_000
781
+
782
+ # schema.rb / structure.sql parse limit (default: 10MB)
783
+ # config.max_schema_file_size = 10_000_000
784
+
785
+ # Max search results per call (default: 100)
786
+ # config.max_search_results = 100
787
+
788
+ # Max files per validate call (default: 20)
789
+ # config.max_validate_files = 20
790
+
791
+ # --- Search and file discovery ---
792
+
793
+ # File extensions for Ruby fallback search
794
+ # config.search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
795
+
796
+ # Where to look for concern source files
797
+ # config.concern_paths = %w[app/models/concerns app/controllers/concerns]
798
+
639
799
  # --- Live reload ---
640
800
 
641
801
  # Auto-invalidate MCP tool caches on file changes
@@ -667,6 +827,7 @@ end
667
827
  | `cache_ttl` | Integer | `30` | Cache TTL in seconds for introspection results |
668
828
  | `excluded_models` | Array | internal Rails models | Models to skip |
669
829
  | `excluded_paths` | Array | `node_modules tmp log vendor .git` | Paths excluded from code search |
830
+ | `sensitive_patterns` | Array | `.env`, `.key`, `.pem`, credentials | File patterns blocked from search and read tools |
670
831
  | `output_dir` | String | `nil` (Rails.root) | Where to write context files |
671
832
  | `auto_mount` | Boolean | `false` | Auto-mount HTTP MCP endpoint |
672
833
  | `http_path` | String | `"/mcp"` | HTTP endpoint path |
@@ -675,7 +836,22 @@ end
675
836
  | `live_reload` | Symbol/Boolean | `:auto` | `:auto`, `true`, or `false` — enable MCP live reload |
676
837
  | `live_reload_debounce` | Float | `1.5` | Debounce interval in seconds for live reload |
677
838
  | `server_name` | String | `"rails-ai-context"` | MCP server name |
839
+ | `server_version` | String | gem version | MCP server version |
678
840
  | `generate_root_files` | Boolean | `true` | Generate root files (CLAUDE.md, .windsurfrules, etc.) — set `false` for split rules only |
841
+ | `max_file_size` | Integer | `2_000_000` | Per-file read limit for tools (2MB) |
842
+ | `max_test_file_size` | Integer | `500_000` | Test file read limit (500KB) |
843
+ | `max_schema_file_size` | Integer | `10_000_000` | schema.rb / structure.sql parse limit (10MB) |
844
+ | `max_view_total_size` | Integer | `5_000_000` | Total aggregated view content for UI patterns (5MB) |
845
+ | `max_view_file_size` | Integer | `500_000` | Per-view file during aggregation (500KB) |
846
+ | `max_search_results` | Integer | `100` | Max search results per call |
847
+ | `max_validate_files` | Integer | `20` | Max files per validate call |
848
+ | `excluded_controllers` | Array | `DeviseController`, etc. | Controller classes hidden from listings |
849
+ | `excluded_route_prefixes` | Array | `action_mailbox/`, `active_storage/`, etc. | Route controller prefixes hidden with `app_only` |
850
+ | `excluded_concerns` | Array | framework regex patterns | Regex patterns for concerns to hide from model output |
851
+ | `excluded_filters` | Array | `verify_authenticity_token`, etc. | Framework filter names hidden from controller output |
852
+ | `excluded_middleware` | Array | standard Rails middleware | Default middleware hidden from config output |
853
+ | `search_extensions` | Array | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
854
+ | `concern_paths` | Array | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
679
855
 
680
856
  ### Root file generation
681
857
 
@@ -750,7 +926,7 @@ config.preset = :full
750
926
 
751
927
  ```ruby
752
928
  # Start with standard, add specific ones
753
- config.introspectors += %i[views turbo auth api stimulus]
929
+ config.introspectors += %i[views turbo auth api]
754
930
 
755
931
  # Or build from scratch
756
932
  config.introspectors = %i[schema models routes gems auth api]
@@ -60,6 +60,26 @@ module RailsAiContext
60
60
  # When false, only generates split rule files (.claude/rules/, .cursor/rules/, etc.)
61
61
  attr_accessor :generate_root_files
62
62
 
63
+ # File size limits (bytes) — increase for larger projects
64
+ attr_accessor :max_file_size # Per-file read limit for tools (default: 2MB)
65
+ attr_accessor :max_test_file_size # Test file read limit (default: 500KB)
66
+ attr_accessor :max_schema_file_size # schema.rb / structure.sql parse limit (default: 10MB)
67
+ attr_accessor :max_view_total_size # Total aggregated view content for UI patterns (default: 5MB)
68
+ attr_accessor :max_view_file_size # Per-view file during aggregation (default: 500KB)
69
+ attr_accessor :max_search_results # Max search results per call (default: 100)
70
+ attr_accessor :max_validate_files # Max files per validate call (default: 20)
71
+
72
+ # Filtering — customize what's hidden from AI output
73
+ attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
74
+ attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
75
+ attr_accessor :excluded_concerns # Regex patterns for concerns to hide (e.g. /Devise::Models/)
76
+ attr_accessor :excluded_filters # Framework filter names hidden from controller output
77
+ attr_accessor :excluded_middleware # Default middleware hidden from config output
78
+
79
+ # Search and file discovery
80
+ attr_accessor :search_extensions # File extensions for Ruby fallback search (default: rb,js,erb,yml,yaml,json)
81
+ attr_accessor :concern_paths # Where to look for concern source files (default: app/models/concerns)
82
+
63
83
  def initialize
64
84
  @server_name = "rails-ai-context"
65
85
  @server_version = RailsAiContext::VERSION
@@ -87,6 +107,41 @@ module RailsAiContext
87
107
  @live_reload = :auto
88
108
  @live_reload_debounce = 1.5
89
109
  @generate_root_files = true
110
+ @max_file_size = 2_000_000
111
+ @max_test_file_size = 500_000
112
+ @max_schema_file_size = 10_000_000
113
+ @max_view_total_size = 5_000_000
114
+ @max_view_file_size = 500_000
115
+ @max_search_results = 100
116
+ @max_validate_files = 20
117
+ @excluded_controllers = %w[DeviseController Devise::OmniauthCallbacksController]
118
+ @excluded_route_prefixes = %w[action_mailbox/ active_storage/ rails/ conductor/ devise/ turbo/]
119
+ @excluded_concerns = [
120
+ /::Generated/,
121
+ /\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage)/,
122
+ /\A(ActionDispatch|ActionController|ActionView|AbstractController)/,
123
+ /\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/
124
+ ]
125
+ @excluded_filters = %w[
126
+ verify_authenticity_token verify_same_origin_request
127
+ turbo_tracking_request_id handle_unverified_request
128
+ mark_for_same_origin_verification
129
+ ]
130
+ @excluded_middleware = %w[
131
+ Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
132
+ ActionDispatch::ServerTiming Rack::Runtime
133
+ ActionDispatch::RequestId ActionDispatch::RemoteIp
134
+ Rails::Rack::Logger ActionDispatch::ShowExceptions
135
+ ActionDispatch::DebugExceptions ActionDispatch::Callbacks
136
+ ActionDispatch::Cookies ActionDispatch::Session::CookieStore
137
+ ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
138
+ ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
139
+ Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
140
+ ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
141
+ Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
142
+ ]
143
+ @search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
144
+ @concern_paths = %w[app/models/concerns app/controllers/concerns]
90
145
  end
91
146
 
92
147
  def preset=(name)
@@ -9,12 +9,9 @@ module RailsAiContext
9
9
  class ControllerIntrospector
10
10
  attr_reader :app
11
11
 
12
- # Framework filters inherited from ActionController::Base — suppress to reduce noise
13
- FRAMEWORK_FILTERS = %w[
14
- verify_authenticity_token verify_same_origin_request
15
- turbo_tracking_request_id handle_unverified_request
16
- mark_for_same_origin_verification
17
- ].freeze
12
+ def excluded_filters
13
+ RailsAiContext.configuration.excluded_filters
14
+ end
18
15
 
19
16
  def initialize(app)
20
17
  @app = app
@@ -164,7 +161,7 @@ module RailsAiContext
164
161
 
165
162
  ctrl._process_action_callbacks.filter_map do |cb|
166
163
  next if cb.filter.is_a?(Proc) || cb.filter.to_s.start_with?("_")
167
- next if FRAMEWORK_FILTERS.include?(cb.filter.to_s)
164
+ next if excluded_filters.include?(cb.filter.to_s)
168
165
 
169
166
  filter = { name: cb.filter.to_s, kind: cb.kind.to_s }
170
167
  filter[:only] = cb.instance_variable_get(:@if)&.filter_map { |c| extract_action_condition(c) }&.flatten
@@ -59,6 +59,7 @@ module RailsAiContext
59
59
  table_name: model.table_name,
60
60
  associations: extract_associations(model),
61
61
  validations: extract_validations(model),
62
+ custom_validates: extract_custom_validates(model),
62
63
  scopes: extract_scopes(model),
63
64
  enums: extract_enums(model),
64
65
  callbacks: extract_callbacks(model),
@@ -109,6 +110,17 @@ module RailsAiContext
109
110
  []
110
111
  end
111
112
 
113
+ # Extract custom validate :method_name calls from source
114
+ # These are business-rule validators that model.validators doesn't include
115
+ def extract_custom_validates(model)
116
+ source_path = model_source_path(model)
117
+ return [] unless source_path && File.exist?(source_path)
118
+
119
+ File.read(source_path).scan(/^\s*validate\s+:(\w+)/).flatten
120
+ rescue
121
+ []
122
+ end
123
+
112
124
  def model_source_path(model)
113
125
  root = app.root.to_s
114
126
  underscored = model.name.underscore
@@ -156,11 +168,8 @@ module RailsAiContext
156
168
 
157
169
  def framework_concern?(name)
158
170
  return true if name.nil?
159
- return true if name.include?("::Generated")
160
- return true if name.match?(/\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage|ActionDispatch|ActionController|ActionView|AbstractController)/)
161
- return true if name.match?(/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/)
162
171
  return true if %w[Kernel JSON PP Marshal MessagePack].include?(name)
163
- false
172
+ RailsAiContext.configuration.excluded_concerns.any? { |pattern| name.match?(pattern) }
164
173
  end
165
174
 
166
175
  def extract_public_class_methods(model)
@@ -150,7 +150,9 @@ module RailsAiContext
150
150
  File.join(app.root, "db", "structure.sql")
151
151
  end
152
152
 
153
- MAX_SCHEMA_FILE_SIZE = 10_000_000 # 10MB safety limit for schema files
153
+ def max_schema_file_size
154
+ RailsAiContext.configuration.max_schema_file_size
155
+ end
154
156
 
155
157
  # Fallback: parse schema file as text when DB isn't connected.
156
158
  # Tries db/schema.rb first, then db/structure.sql.
@@ -166,7 +168,7 @@ module RailsAiContext
166
168
  end
167
169
 
168
170
  def parse_schema_rb(path)
169
- return { error: "schema.rb too large (#{File.size(path)} bytes)" } if File.size(path) > MAX_SCHEMA_FILE_SIZE
171
+ return { error: "schema.rb too large (#{File.size(path)} bytes)" } if File.size(path) > max_schema_file_size
170
172
  content = File.read(path)
171
173
  tables = {}
172
174
  current_table = nil
@@ -209,7 +211,7 @@ module RailsAiContext
209
211
  end
210
212
 
211
213
  def parse_structure_sql(path) # rubocop:disable Metrics/MethodLength
212
- return { error: "structure.sql too large (#{File.size(path)} bytes)" } if File.size(path) > MAX_SCHEMA_FILE_SIZE
214
+ return { error: "structure.sql too large (#{File.size(path)} bytes)" } if File.size(path) > max_schema_file_size
213
215
  content = File.read(path)
214
216
  tables = {}
215
217
 
@@ -61,15 +61,14 @@ module RailsAiContext
61
61
  partials
62
62
  end
63
63
 
64
- MAX_TOTAL_VIEW_SIZE = 5_000_000 # 5MB cap for aggregated view content
65
- MAX_SINGLE_VIEW_SIZE = 500_000 # 500KB per file
66
-
67
64
  def collect_all_view_content(views_dir)
65
+ max_total = RailsAiContext.configuration.max_view_total_size
66
+ max_single = RailsAiContext.configuration.max_view_file_size
68
67
  content = +""
69
68
  Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).each do |path|
70
69
  next if File.directory?(path)
71
- next if File.size(path) > MAX_SINGLE_VIEW_SIZE
72
- break if content.bytesize >= MAX_TOTAL_VIEW_SIZE
70
+ next if File.size(path) > max_single
71
+ break if content.bytesize >= max_total
73
72
  content << (File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue "")
74
73
  end
75
74
  content
@@ -6,21 +6,6 @@ module RailsAiContext
6
6
  tool_name "rails_get_config"
7
7
  description "Get Rails application configuration including cache store, session store, timezone, middleware stack, and initializers."
8
8
 
9
- # Default Rails middleware — suppress to only show app-specific middleware
10
- DEFAULT_MIDDLEWARE = %w[
11
- Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
12
- ActionDispatch::ServerTiming Rack::Runtime
13
- ActionDispatch::RequestId ActionDispatch::RemoteIp
14
- Rails::Rack::Logger ActionDispatch::ShowExceptions
15
- ActionDispatch::DebugExceptions ActionDispatch::Callbacks
16
- ActionDispatch::Cookies ActionDispatch::Session::CookieStore
17
- ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
18
- ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
19
- Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
20
- ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
21
- Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
22
- ].freeze
23
-
24
9
  input_schema(properties: {})
25
10
 
26
11
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
@@ -45,7 +30,8 @@ module RailsAiContext
45
30
  Propshaft::Server WebConsole::Middleware ActionDispatch::Reloader
46
31
  Bullet::Rack ActiveSupport::Cache::Strategy::LocalCache
47
32
  ]
48
- custom = data[:middleware_stack].reject { |m| DEFAULT_MIDDLEWARE.include?(m) || dev_middleware.include?(m) }
33
+ excluded_mw = RailsAiContext.configuration.excluded_middleware
34
+ custom = data[:middleware_stack].reject { |m| excluded_mw.include?(m) || dev_middleware.include?(m) }
49
35
  if custom.any?
50
36
  lines << "" << "## Custom Middleware"
51
37
  custom.each { |m| lines << "- #{m}" }
@@ -42,7 +42,7 @@ module RailsAiContext
42
42
  controllers = data[:controllers] || {}
43
43
 
44
44
  # Filter out framework-internal controllers for listings/error messages
45
- framework_controllers = %w[DeviseController Devise::OmniauthCallbacksController]
45
+ framework_controllers = RailsAiContext.configuration.excluded_controllers
46
46
  app_controller_names = controllers.keys.reject { |name| framework_controllers.include?(name) }.sort
47
47
 
48
48
  # Specific controller — always full detail (searches ALL controllers including framework)
@@ -219,11 +219,9 @@ module RailsAiContext
219
219
  lines.join("\n")
220
220
  end
221
221
 
222
- MAX_CONTROLLER_SIZE = 2_000_000 # 2MB safety limit
223
-
224
222
  private_class_method def self.extract_method_with_lines(file_path, method_name)
225
223
  return nil unless File.exist?(file_path)
226
- return nil if File.size(file_path) > MAX_CONTROLLER_SIZE
224
+ return nil if File.size(file_path) > RailsAiContext.configuration.max_file_size
227
225
  source_lines = File.readlines(file_path)
228
226
  start_idx = source_lines.index { |l| l.match?(/^\s*def\s+#{Regexp.escape(method_name.to_s)}\b/) }
229
227
  return nil unless start_idx
@@ -6,7 +6,9 @@ module RailsAiContext
6
6
  tool_name "rails_get_edit_context"
7
7
  description "Get just enough context to make a surgical Edit to a file. Returns the target area with line numbers and surrounding code. Purpose-built to replace Read + Edit workflow with a single call."
8
8
 
9
- MAX_FILE_SIZE = 2_000_000
9
+ def self.max_file_size
10
+ RailsAiContext.configuration.max_file_size
11
+ end
10
12
 
11
13
  input_schema(
12
14
  properties: {
@@ -55,7 +57,7 @@ module RailsAiContext
55
57
  rescue Errno::ENOENT
56
58
  return text_response("File not found: #{file}")
57
59
  end
58
- if File.size(full_path) > MAX_FILE_SIZE
60
+ if File.size(full_path) > max_file_size
59
61
  return text_response("File too large: #{file}")
60
62
  end
61
63
 
@@ -38,7 +38,9 @@ module RailsAiContext
38
38
  lines << "- **#{g[:name]}**: #{g[:note]}"
39
39
  end
40
40
  else
41
- lines << "_No notable gems found#{" in category '#{category}'" unless category == 'all'}._"
41
+ all_cats = (gems[:notable_gems] || []).map { |g| g[:category] }.uniq.sort
42
+ hint = all_cats.any? ? " Available categories: #{all_cats.join(', ')}" : ""
43
+ lines << "_No notable gems found#{" in category '#{category}'" unless category == 'all'}.#{hint}_"
42
44
  end
43
45
 
44
46
  text_response(lines.join("\n"))
@@ -123,6 +123,7 @@ module RailsAiContext
123
123
  detail += " (class: #{a[:class_name]})" if a[:class_name] && a[:class_name] != a[:name].to_s.classify
124
124
  detail += " through: #{a[:through]}" if a[:through]
125
125
  detail += " [polymorphic]" if a[:polymorphic]
126
+ detail += " [optional]" if a[:optional]
126
127
  detail += " dependent: #{a[:dependent]}" if a[:dependent]
127
128
  lines << detail
128
129
  end
@@ -157,6 +158,11 @@ module RailsAiContext
157
158
  end
158
159
  end
159
160
 
161
+ # Custom validate methods (business rules)
162
+ if data[:custom_validates]&.any?
163
+ lines << "- **Custom:** #{data[:custom_validates].map { |v| "`#{v}`" }.join(', ')}"
164
+ end
165
+
160
166
  # Enums
161
167
  if data[:enums]&.any?
162
168
  lines << "" << "## Enums"
@@ -181,18 +187,30 @@ module RailsAiContext
181
187
 
182
188
  # Concerns — filter out framework/gem internal modules
183
189
  if data[:concerns]&.any?
190
+ excluded_patterns = RailsAiContext.configuration.excluded_concerns
184
191
  app_concerns = data[:concerns].reject do |c|
185
- c.include?("::Generated") ||
186
- c.match?(/\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage|ActionDispatch|ActionController|ActionView|AbstractController)/) ||
187
- c.match?(/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/) ||
188
- %w[Kernel JSON PP Marshal MessagePack].any? { |mod| c == mod || c.start_with?("#{mod}::") }
192
+ %w[Kernel JSON PP Marshal MessagePack].include?(c) ||
193
+ excluded_patterns.any? { |pattern| c.match?(pattern) }
189
194
  end
190
195
  if app_concerns.any?
191
196
  lines << "" << "## Concerns"
192
- lines << app_concerns.map { |c| "- #{c}" }.join("\n")
197
+ app_concerns.each do |c|
198
+ methods = extract_concern_methods(c)
199
+ if methods&.any?
200
+ lines << "- **#{c}** — #{methods.join(', ')}"
201
+ else
202
+ lines << "- #{c}"
203
+ end
204
+ end
193
205
  end
194
206
  end
195
207
 
208
+ # Class methods (e.g. Plan.free, Plan.pro)
209
+ if data[:class_methods]&.any?
210
+ lines << "" << "## Class methods"
211
+ lines << data[:class_methods].first(15).map { |m| "- `#{m}`" }.join("\n")
212
+ end
213
+
196
214
  # Key instance methods — include signatures from source if available
197
215
  if data[:instance_methods]&.any?
198
216
  lines << "" << "## Key instance methods"
@@ -220,7 +238,7 @@ module RailsAiContext
220
238
  private_class_method def self.extract_method_signatures(model_name)
221
239
  path = Rails.root.join("app", "models", "#{model_name.underscore}.rb")
222
240
  return nil unless File.exist?(path)
223
- return nil if File.size(path) > MAX_MODEL_SIZE
241
+ return nil if File.size(path) > max_file_size
224
242
 
225
243
  source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
226
244
  signatures = []
@@ -241,13 +259,45 @@ module RailsAiContext
241
259
  nil
242
260
  end
243
261
 
244
- MAX_MODEL_SIZE = 2_000_000 # 2MB safety limit
262
+ # Extract public method names from a concern's source file
263
+ private_class_method def self.extract_concern_methods(concern_name)
264
+ max_size = RailsAiContext.configuration.max_file_size
265
+ underscore = concern_name.underscore
266
+ # Search configurable concern paths
267
+ path = RailsAiContext.configuration.concern_paths
268
+ .map { |dir| Rails.root.join(dir, "#{underscore}.rb") }
269
+ .find { |p| File.exist?(p) }
270
+ return nil unless path
271
+ return nil if File.size(path) > max_size
272
+
273
+ source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
274
+ methods = []
275
+ in_private = false
276
+
277
+ source.each_line do |line|
278
+ in_private = true if line.match?(/\A\s*(private|protected)\s*$/)
279
+ in_private = false if line.match?(/\A\s*public\s*$/)
280
+ next if in_private
281
+
282
+ if (match = line.match(/\A\s*def\s+([\w?!]+)/))
283
+ methods << match[1] unless match[1].start_with?("_")
284
+ end
285
+ end
286
+
287
+ methods.empty? ? nil : methods
288
+ rescue
289
+ nil
290
+ end
291
+
292
+ def self.max_file_size
293
+ RailsAiContext.configuration.max_file_size
294
+ end
245
295
 
246
296
  private_class_method def self.extract_model_structure(model_name)
247
297
  path = "app/models/#{model_name.underscore}.rb"
248
298
  full_path = Rails.root.join(path)
249
299
  return nil unless File.exist?(full_path)
250
- return nil if File.size(full_path) > MAX_MODEL_SIZE
300
+ return nil if File.size(full_path) > max_file_size
251
301
 
252
302
  source_lines = File.readlines(full_path)
253
303
  sections = []
@@ -32,14 +32,9 @@ module RailsAiContext
32
32
  }
33
33
  )
34
34
 
35
- INTERNAL_PREFIXES = %w[
36
- action_mailbox/ active_storage/ rails/ conductor/
37
- ].freeze
38
-
39
- # Framework routes that add noise to summary listings
40
- FRAMEWORK_PREFIXES = %w[
41
- devise/ turbo/
42
- ].freeze
35
+ def self.route_prefixes
36
+ RailsAiContext.configuration.excluded_route_prefixes
37
+ end
43
38
 
44
39
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
45
40
 
@@ -53,12 +48,13 @@ module RailsAiContext
53
48
 
54
49
  # Filter out internal Rails routes by default
55
50
  if app_only
56
- by_controller = by_controller.reject { |k, _| INTERNAL_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
51
+ by_controller = by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
57
52
  end
58
53
 
59
- # Filter by controller
54
+ # Filter by controller — accepts both slash and :: notation
60
55
  if controller
61
- filtered = by_controller.select { |k, _| k.downcase.include?(controller.downcase) }
56
+ normalized = controller.downcase.tr("::", "/").delete_suffix("controller")
57
+ filtered = by_controller.select { |k, _| k.downcase.include?(normalized) }
62
58
  return text_response("No routes for '#{controller}'. Controllers: #{by_controller.keys.sort.join(', ')}") if filtered.empty?
63
59
  by_controller = filtered
64
60
  end
@@ -70,8 +66,8 @@ module RailsAiContext
70
66
  case detail
71
67
  when "summary"
72
68
  # Separate app routes from framework routes for cleaner output
73
- app_routes = controller ? by_controller : by_controller.reject { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
74
- framework_routes = controller ? {} : by_controller.select { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
69
+ app_routes = controller ? by_controller : by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
70
+ framework_routes = controller ? {} : by_controller.select { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
75
71
 
76
72
  lines = [ "# Routes Summary (#{filtered_total} routes)", "" ]
77
73
 
@@ -116,8 +112,8 @@ module RailsAiContext
116
112
  when "standard"
117
113
  limit ||= 100
118
114
  # Separate app vs framework routes (unless user filtered by controller)
119
- app_routes = controller ? by_controller : by_controller.reject { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
120
- framework_routes = controller ? {} : by_controller.select { |k, _| FRAMEWORK_PREFIXES.any? { |p| k.downcase.start_with?(p) } }
115
+ app_routes = controller ? by_controller : by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
116
+ framework_routes = controller ? {} : by_controller.select { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
121
117
 
122
118
  lines = [ "# Routes (#{filtered_total} routes)", "" ]
123
119
  count = 0
@@ -123,23 +123,20 @@ module RailsAiContext
123
123
 
124
124
  private_class_method def self.format_table_markdown(name, data)
125
125
  columns = data[:columns] || []
126
- has_nullable = columns.any? { |c| c[:null] }
127
- has_defaults = columns.any? { |c| c[:default] }
126
+ # Always show Nullable and Default — agents need these for migrations and validations
127
+ has_defaults = columns.any? { |c| c.key?(:default) && !c[:default].nil? }
128
128
 
129
129
  lines = [ "## Table: #{name}", "" ]
130
130
 
131
- # Only show Nullable/Default columns when they have meaningful values
132
- header = "| Column | Type"
133
- sep = "|--------|-----"
134
- header += " | Nullable" if has_nullable
135
- sep += "-|----------" if has_nullable
131
+ header = "| Column | Type | Null"
132
+ sep = "|--------|------|-----"
136
133
  header += " | Default" if has_defaults
137
134
  sep += "-|---------" if has_defaults
138
135
  lines << "#{header} |" << "#{sep}|"
139
136
 
140
137
  columns.each do |col|
141
- line = "| #{col[:name]} | #{col[:type]}"
142
- line += " | #{col[:null] ? 'yes' : 'no'}" if has_nullable
138
+ nullable = col.key?(:null) ? (col[:null] ? "yes" : "**NO**") : "yes"
139
+ line = "| #{col[:name]} | #{col[:type]} | #{nullable}"
143
140
  line += " | #{col[:default]}" if has_defaults
144
141
  lines << "#{line} |"
145
142
  end
@@ -119,7 +119,9 @@ module RailsAiContext
119
119
  end
120
120
  end
121
121
 
122
- MAX_TEST_FILE_SIZE = 500_000 # 500KB safety limit
122
+ def self.max_test_file_size
123
+ RailsAiContext.configuration.max_test_file_size
124
+ end
123
125
 
124
126
  private_class_method def self.find_test_file(name, type, detail = "full")
125
127
  # Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises"
@@ -151,7 +153,7 @@ module RailsAiContext
151
153
  rescue Errno::ENOENT
152
154
  next
153
155
  end
154
- next if File.size(path) > MAX_TEST_FILE_SIZE
156
+ next if File.size(path) > max_test_file_size
155
157
  content = File.read(path)
156
158
 
157
159
  # Summary/standard: return just test names (saves 2000+ tokens vs full source)
@@ -169,7 +171,13 @@ module RailsAiContext
169
171
  return "# #{rel}\n\n```ruby\n#{content}\n```"
170
172
  end
171
173
 
172
- "No test file found for #{name}. Searched: #{candidates.join(', ')}"
174
+ # List nearby test files to help the agent find the right one
175
+ test_dirs = candidates.map { |c| File.dirname(Rails.root.join(c)) }.uniq
176
+ nearby = test_dirs.flat_map do |dir|
177
+ Dir.exist?(dir) ? Dir.glob(File.join(dir, "*")).map { |f| f.sub("#{Rails.root}/", "") }.first(10) : []
178
+ end
179
+ hint = nearby.any? ? "\n\nFiles in test directory: #{nearby.join(', ')}" : ""
180
+ "No test file found for #{name}. Searched: #{candidates.join(', ')}#{hint}"
173
181
  end
174
182
  end
175
183
  end
@@ -161,7 +161,9 @@ module RailsAiContext
161
161
  text_response(lines.join("\n"))
162
162
  end
163
163
 
164
- MAX_FILE_SIZE = 2_000_000 # 2MB safety limit
164
+ def self.max_file_size
165
+ RailsAiContext.configuration.max_file_size
166
+ end
165
167
 
166
168
  private_class_method def self.read_view_file(path)
167
169
  # Reject path traversal attempts before any filesystem operation
@@ -174,7 +176,10 @@ module RailsAiContext
174
176
 
175
177
  # Path traversal protection (resolves symlinks)
176
178
  unless File.exist?(full_path)
177
- return text_response("View not found: #{path}")
179
+ dir = File.dirname(path)
180
+ siblings = Dir.glob(File.join(views_dir, dir, "*")).map { |f| "#{dir}/#{File.basename(f)}" }.sort.first(10)
181
+ hint = siblings.any? ? " Files in #{dir}/: #{siblings.join(', ')}" : ""
182
+ return text_response("View not found: #{path}.#{hint}")
178
183
  end
179
184
  begin
180
185
  unless File.realpath(full_path).start_with?(File.realpath(views_dir))
@@ -183,8 +188,8 @@ module RailsAiContext
183
188
  rescue Errno::ENOENT
184
189
  return text_response("View not found: #{path}")
185
190
  end
186
- if File.size(full_path) > MAX_FILE_SIZE
187
- return text_response("File too large: #{path} (#{File.size(full_path)} bytes)")
191
+ if File.size(full_path) > max_file_size
192
+ return text_response("File too large: #{path} (#{File.size(full_path)} bytes, max: #{max_file_size})")
188
193
  end
189
194
 
190
195
  content = compress_tailwind(strip_svg(File.read(full_path)))
@@ -8,7 +8,9 @@ module RailsAiContext
8
8
  tool_name "rails_search_code"
9
9
  description "Search the Rails codebase for a pattern using ripgrep (rg) or Ruby fallback. Returns matching lines with file paths and line numbers. Useful for finding usages, implementations, and patterns."
10
10
 
11
- MAX_RESULTS_CAP = 100
11
+ def self.max_results_cap
12
+ RailsAiContext.configuration.max_search_results
13
+ end
12
14
 
13
15
  input_schema(
14
16
  properties: {
@@ -59,7 +61,7 @@ module RailsAiContext
59
61
  end
60
62
 
61
63
  # Cap max_results and context_lines
62
- max_results = [ max_results.to_i, MAX_RESULTS_CAP ].min
64
+ max_results = [ max_results.to_i, max_results_cap ].min
63
65
  max_results = 30 if max_results < 1
64
66
  context_lines = [ [ context_lines.to_i, 0 ].max, 5 ].min
65
67
 
@@ -67,7 +69,8 @@ module RailsAiContext
67
69
 
68
70
  # Path traversal protection
69
71
  unless Dir.exist?(search_path)
70
- return text_response("Path not found: #{path}")
72
+ top_dirs = Dir.glob(File.join(root, "*")).select { |f| File.directory?(f) }.map { |f| File.basename(f) }.sort
73
+ return text_response("Path not found: #{path}. Top-level directories: #{top_dirs.first(15).join(', ')}")
71
74
  end
72
75
 
73
76
  begin
@@ -143,7 +146,8 @@ module RailsAiContext
143
146
  rescue RegexpError => e
144
147
  return [ { file: "error", line_number: 0, content: "Invalid regex: #{e.message}" } ]
145
148
  end
146
- glob = file_type ? "**/*.#{file_type}" : "**/*.{rb,js,erb,yml,yaml,json}"
149
+ extensions = RailsAiContext.configuration.search_extensions.join(",")
150
+ glob = file_type ? "**/*.#{file_type}" : "**/*.{#{extensions}}"
147
151
  excluded = RailsAiContext.configuration.excluded_paths
148
152
  sensitive = RailsAiContext.configuration.sensitive_patterns
149
153
 
@@ -8,7 +8,9 @@ module RailsAiContext
8
8
  tool_name "rails_validate"
9
9
  description "Validate syntax of multiple files at once (Ruby, ERB, JavaScript). Replaces separate ruby -c, erb check, and node -c calls. Returns pass/fail for each file with error details."
10
10
 
11
- MAX_FILES = 20
11
+ def self.max_files
12
+ RailsAiContext.configuration.max_validate_files
13
+ end
12
14
 
13
15
  input_schema(
14
16
  properties: {
@@ -28,8 +30,8 @@ module RailsAiContext
28
30
  return text_response("No files provided.")
29
31
  end
30
32
 
31
- if files.size > MAX_FILES
32
- return text_response("Too many files (#{files.size}). Maximum is #{MAX_FILES} per call.")
33
+ if files.size > max_files
34
+ return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.")
33
35
  end
34
36
 
35
37
  results = []
@@ -152,10 +154,8 @@ module RailsAiContext
152
154
 
153
155
  # Basic JavaScript validation when node is not available.
154
156
  # Checks for unmatched braces, brackets, and parentheses.
155
- MAX_VALIDATE_FILE_SIZE = 2_000_000
156
-
157
157
  private_class_method def self.validate_javascript_fallback(full_path)
158
- return [ false, "file too large for basic validation" ] if File.size(full_path) > MAX_VALIDATE_FILE_SIZE
158
+ return [ false, "file too large for basic validation" ] if File.size(full_path) > RailsAiContext.configuration.max_file_size
159
159
  content = File.read(full_path)
160
160
  stack = []
161
161
  openers = { "{" => "}", "[" => "]", "(" => ")" }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "0.15.5"
4
+ VERSION = "0.15.6"
5
5
  end
data/server.json CHANGED
@@ -7,11 +7,11 @@
7
7
  "url": "https://github.com/crisnahine/rails-ai-context",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.15.4",
10
+ "version": "0.15.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
14
- "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.4/rails-ai-context-mcp.mcpb",
14
+ "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.5/rails-ai-context-mcp.mcpb",
15
15
  "fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
16
16
  "transport": {
17
17
  "type": "stdio"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-ai-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.5
4
+ version: 0.15.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine