rails-ai-context 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/CHANGELOG.md +62 -0
  4. data/CLAUDE.md +47 -0
  5. data/README.md +88 -22
  6. data/exe/rails-ai-context +30 -2
  7. data/lib/rails_ai_context/configuration.rb +5 -1
  8. data/lib/rails_ai_context/doctor.rb +149 -0
  9. data/lib/rails_ai_context/engine.rb +8 -0
  10. data/lib/rails_ai_context/fingerprinter.rb +54 -0
  11. data/lib/rails_ai_context/introspector.rb +16 -0
  12. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +50 -0
  13. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +48 -0
  14. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +81 -0
  15. data/lib/rails_ai_context/introspectors/api_introspector.rb +92 -0
  16. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +92 -0
  17. data/lib/rails_ai_context/introspectors/auth_introspector.rb +115 -0
  18. data/lib/rails_ai_context/introspectors/config_introspector.rb +85 -0
  19. data/lib/rails_ai_context/introspectors/controller_introspector.rb +135 -0
  20. data/lib/rails_ai_context/introspectors/convention_detector.rb +20 -1
  21. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +39 -0
  22. data/lib/rails_ai_context/introspectors/devops_introspector.rb +111 -0
  23. data/lib/rails_ai_context/introspectors/gem_introspector.rb +51 -1
  24. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +66 -0
  25. data/lib/rails_ai_context/introspectors/model_introspector.rb +52 -10
  26. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +68 -0
  27. data/lib/rails_ai_context/introspectors/route_introspector.rb +1 -1
  28. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +81 -0
  29. data/lib/rails_ai_context/introspectors/test_introspector.rb +137 -0
  30. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +80 -0
  31. data/lib/rails_ai_context/introspectors/view_introspector.rb +130 -0
  32. data/lib/rails_ai_context/middleware.rb +39 -0
  33. data/lib/rails_ai_context/resources.rb +105 -0
  34. data/lib/rails_ai_context/serializers/claude_serializer.rb +45 -0
  35. data/lib/rails_ai_context/serializers/context_file_serializer.rb +16 -10
  36. data/lib/rails_ai_context/serializers/copilot_serializer.rb +29 -0
  37. data/lib/rails_ai_context/serializers/markdown_serializer.rb +247 -7
  38. data/lib/rails_ai_context/serializers/rules_serializer.rb +26 -0
  39. data/lib/rails_ai_context/server.rb +10 -3
  40. data/lib/rails_ai_context/tasks/rails_ai_context.rake +42 -7
  41. data/lib/rails_ai_context/tools/base_tool.rb +15 -3
  42. data/lib/rails_ai_context/tools/get_config.rb +41 -0
  43. data/lib/rails_ai_context/tools/get_controllers.rb +67 -0
  44. data/lib/rails_ai_context/tools/get_conventions.rb +1 -1
  45. data/lib/rails_ai_context/tools/get_gems.rb +2 -2
  46. data/lib/rails_ai_context/tools/get_model_details.rb +1 -1
  47. data/lib/rails_ai_context/tools/get_routes.rb +2 -2
  48. data/lib/rails_ai_context/tools/get_schema.rb +6 -4
  49. data/lib/rails_ai_context/tools/get_test_info.rb +34 -0
  50. data/lib/rails_ai_context/tools/search_code.rb +42 -9
  51. data/lib/rails_ai_context/version.rb +1 -1
  52. data/lib/rails_ai_context/watcher.rb +75 -0
  53. data/lib/rails_ai_context.rb +31 -1
  54. data/rails-ai-context.gemspec +6 -5
  55. metadata +32 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f762c8f111cca6b60293f4d3eb26ef68aeb9e1d0c97048b46dcf221ebf7d6069
4
- data.tar.gz: 01b9e656715c47aa0a3e67cfb75fb6ac1a5fd7d6444b4ac96837c6202a093487
3
+ metadata.gz: 6f3efeed4bfe44c20ff766c0398b1d8a526a13c053b47d2a93d8faa320abfdda
4
+ data.tar.gz: 2cfe5512db995e2b08a2f9516793f1e68e787e8b76092780370ea6b04e280f69
5
5
  SHA512:
6
- metadata.gz: a5daa689b56aeee3225f0bfa983580adeabbc195c2184b669f5a3689d1cd2760d115afefcc2c925e14eefafea2b28729893b260f141acd00f5e89a6bcb4fc665
7
- data.tar.gz: 7fba10086525e15044867dc4c1b5c5a44fd8eaf5f66c08ffbbd6468dfb1f2831d5b03231ec3e0ca02e2fec2a30a4558cd5567262642edf92c750a4bd8286fdc4
6
+ metadata.gz: 4ccbff85f3a231a4c207522834387662af03240511c2bbbf491023e2b6361b68ea1e109cc3d8b383f02c9989db5542e555b6579b01037c25757acb3d84cc4525
7
+ data.tar.gz: 62a53606b4e98f331e716a44a8e424479e2cd61649de82814778feb1f4b3de23607f6a1f35fb6664c465fc130c9f2a140d928c6ce8864c4d4038f7ed27ddbcbc
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
- require:
2
- - rubocop-rails-omakase
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 3.2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,68 @@ 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.4.0] - 2026-03-18
9
+
10
+ ### Added
11
+
12
+ - **14 new introspectors** — Controllers, Views, Turbo/Hotwire, I18n, Config, Active Storage, Action Text, Auth, API, Tests, Rake Tasks, Asset Pipeline, DevOps, Action Mailbox
13
+ - **3 new MCP tools** — `rails_get_controllers`, `rails_get_config`, `rails_get_test_info`
14
+ - **3 new MCP resources** — `rails://controllers`, `rails://config`, `rails://tests`
15
+ - **Model introspector enhancements** — Extracts `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts_to` via source parsing
16
+ - **Stimulus introspector enhancements** — Extracts `outlets` and `classes` from controllers
17
+ - **Gem introspector enhancements** — 30+ new notable gems: monitoring (Sentry, Datadog, New Relic, Skylight), admin (ActiveAdmin, Administrate, Avo), pagination (Pagy, Kaminari), search (Ransack, pg_search, Searchkick), forms (SimpleForm), utilities (Faraday, Flipper, Bullet, Rack::Attack), and more
18
+ - **Convention detector enhancements** — Detects concerns, validators, policies, serializers, notifiers, Phlex, PWA, encrypted attributes, normalizations
19
+ - **Markdown serializer sections** — All 14 new introspector sections rendered in generated context files
20
+ - **Doctor enhancements** — 4 new checks: controllers, views, i18n, tests (11 total)
21
+ - **Fingerprinter expansion** — Watches `app/controllers`, `app/views`, `app/jobs`, `app/mailers`, `app/channels`, `app/javascript/controllers`, `config/initializers`, `lib/tasks`; glob now covers `.rb`, `.rake`, `.js`, `.ts`, `.erb`, `.haml`, `.slim`, `.yml`
22
+
23
+ ### Fixed
24
+
25
+ - **YAML parsing** — `YAML.load_file` calls now pass `permitted_classes: [Symbol], aliases: true` for Psych 4 (Ruby 3.1+) compatibility
26
+ - **Rake task parser** — Fixed `@last_desc` instance variable leaking between files; fixed namespace tracking with indent-based stack
27
+ - **Vite detection** — Changed `File.exist?("vite.config")` to `Dir.glob("vite.config.*")` to match `.js`/`.ts`/`.mjs` extensions
28
+ - **Health check regex** — Added word boundaries to avoid false positives on substrings (e.g. "groups" matching "up")
29
+ - **Multi-attribute macros** — `normalizes :email, :name` now captures all attributes, not just the first
30
+ - **Stimulus action regex** — Requires `method(args) {` pattern to avoid matching control flow keywords
31
+ - **Controller respond_to** — Simplified format extraction to avoid nested `end` keyword issues
32
+ - **GetRoutes nil guard** — Added `|| {}` fallback for `by_controller` to prevent crash on partial introspection data
33
+ - **GetSchema nil guard** — Added `|| {}` fallback for `schema[:tables]` to prevent crash on partial schema data
34
+ - **View layout discovery** — Added `File.file?` filter to exclude directories from layout listing
35
+ - **Fingerprinter glob** — Changed from `**/*.rb` to multi-extension glob to detect changes in `.rake`, `.js`, `.ts`, `.erb` files
36
+
37
+ ### Changed
38
+
39
+ - Default introspectors expanded from 7 to 21
40
+ - MCP tools expanded from 6 to 9
41
+ - Static MCP resources expanded from 4 to 7
42
+ - Doctor checks expanded from 7 to 11
43
+ - Test suite expanded from 149 to 247 examples with exact value assertions
44
+
45
+ ## [0.3.0] - 2026-03-18
46
+
47
+ ### Added
48
+
49
+ - **Cache invalidation** — TTL + file fingerprinting for MCP tool cache (replaces permanent `||=` cache)
50
+ - **MCP Resources** — Static resources (`rails://schema`, `rails://routes`, `rails://conventions`, `rails://gems`) and resource template (`rails://models/{name}`)
51
+ - **Per-assistant serializers** — Claude gets behavioral rules, Cursor/Windsurf get compact rules, Copilot gets task-oriented GFM
52
+ - **Stimulus introspector** — Extracts Stimulus controller targets, values, and actions from JS/TS files
53
+ - **Database stats introspector** — Opt-in PostgreSQL approximate row counts via `pg_stat_user_tables`
54
+ - **Auto-mount HTTP middleware** — Rack middleware for MCP endpoint when `config.auto_mount = true`
55
+ - **Diff-aware regeneration** — Context file generation skips unchanged files
56
+ - **`rails ai:doctor`** — Diagnostic command with AI readiness score (0-100)
57
+ - **`rails ai:watch`** — File watcher that auto-regenerates context files on change (requires `listen` gem)
58
+
59
+ ### Fixed
60
+
61
+ - **Shell injection in SearchCode** — Replaced backtick execution with `Open3.capture2` array form; added file_type validation, max_results cap, and path traversal protection
62
+ - **Scope extraction** — Fixed broken `model.methods.grep(/^_scope_/)` by parsing source files for `scope :name` declarations
63
+ - **Route introspector** — Fixed `route.internal?` compatibility with Rails 8.1
64
+
65
+ ### Changed
66
+
67
+ - `generate_context` now returns `{ written: [], skipped: [] }` instead of flat array
68
+ - Default introspectors now include `:stimulus`
69
+
8
70
  ## [0.2.0] - 2026-03-18
9
71
 
10
72
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,47 @@
1
+ # CLAUDE.md — rails-ai-context development guide
2
+
3
+ This is a Ruby gem that auto-introspects Rails applications and exposes their
4
+ structure to AI assistants via the Model Context Protocol (MCP).
5
+
6
+ ## Architecture
7
+
8
+ - `lib/rails_ai_context.rb` — Main entry point, public API
9
+ - `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
10
+ - `lib/rails_ai_context/introspectors/` — Individual introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox)
11
+ - `lib/rails_ai_context/tools/` — MCP tools using the official mcp SDK
12
+ - `lib/rails_ai_context/serializers/` — Output formatters (claude, rules, copilot, markdown, JSON)
13
+ - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
14
+ - `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
15
+ - `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
16
+ - `lib/rails_ai_context/fingerprinter.rb` — SHA256 file fingerprinting for cache invalidation
17
+ - `lib/rails_ai_context/doctor.rb` — Diagnostic checks and AI readiness scoring
18
+ - `lib/rails_ai_context/watcher.rb` — File watcher for auto-regenerating context files
19
+ - `lib/rails_ai_context/engine.rb` — Rails Engine for auto-integration
20
+
21
+ ## Key Design Decisions
22
+
23
+ 1. **Built on official mcp SDK** — not a custom protocol implementation
24
+ 2. **Zero-config** — Railtie auto-registers at boot, introspects without setup
25
+ 3. **Graceful degradation** — works without DB by parsing schema.rb as text
26
+ 4. **Read-only tools only** — all MCP tools are annotated as non-destructive
27
+ 5. **Dual output** — static files (CLAUDE.md) + live MCP server (stdio/HTTP)
28
+ 6. **Diff-aware** — context regeneration skips unchanged files
29
+ 7. **Per-assistant serializers** — each AI tool gets tailored output format
30
+
31
+ ## Testing
32
+
33
+ ```bash
34
+ bundle exec rspec # Run specs
35
+ bundle exec rubocop # Lint
36
+ ```
37
+
38
+ Uses combustion gem for testing Rails engine behavior in isolation.
39
+
40
+ ## Conventions
41
+
42
+ - Ruby 3.2+ features OK (pattern matching, etc.)
43
+ - Follow rubocop-rails-omakase style
44
+ - Every introspector returns a Hash, never raises (wraps errors in `{ error: msg }`)
45
+ - MCP tools return `MCP::Tool::Response` objects per SDK convention
46
+ - All tools prefixed with `rails_` per MCP naming best practices
47
+ - `generate_context` returns `{ written: [], skipped: [] }` hash
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![CI](https://github.com/crisnahine/rails-ai-context/actions/workflows/ci.yml/badge.svg)](https://github.com/crisnahine/rails-ai-context/actions)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
 
9
- `rails-ai-context` automatically introspects your Rails application and exposes your models, routes, schema, jobs, gems, and conventions to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
9
+ `rails-ai-context` automatically introspects your Rails application and exposes your models, routes, schema, controllers, views, jobs, gems, auth, API, tests, config, and conventions to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
10
10
 
11
11
  **Your AI assistant instantly understands your entire Rails app. No configuration. No manual tool definitions. Just `bundle add` and go.**
12
12
 
@@ -26,12 +26,22 @@ bundle add rails-ai-context
26
26
 
27
27
  That's it. Now your AI assistant knows:
28
28
 
29
- - 📦 **Every table, column, index, and foreign key** in your database
30
- - 🏗️ **Every model** with its associations, validations, scopes, enums, and callbacks
31
- - 🛤️ **Every route** with HTTP verbs, paths, and controller actions
32
- - **Every background job**, mailer, and Action Cable channel
33
- - 💎 **Every notable gem** and what it means (Devise → auth, Sidekiq → jobs, Turbo Hotwire)
34
- - 🏛️ **Your architecture patterns**: service objects, STI, polymorphism, state machines, multi-tenancy
29
+ - **Every table, column, index, and foreign key** in your database
30
+ - **Every model** with associations, validations, scopes, enums, callbacks, and macros (has_secure_password, encrypts, normalizes, etc.)
31
+ - **Every controller** with actions, filters, strong params, and concerns
32
+ - **Every view** with layouts, templates, partials, helpers, and template engines
33
+ - **Every route** with HTTP verbs, paths, and controller actions
34
+ - **Every background job**, mailer, and Action Cable channel
35
+ - **Hotwire/Turbo** — Turbo Frames, Turbo Streams, model broadcasts
36
+ - **Every notable gem** (70+) and what it means (Devise = auth, Sidekiq = jobs, Turbo = Hotwire)
37
+ - **Auth & security** — Devise modules, Pundit policies, CanCanCan, CORS, CSP
38
+ - **API layer** — serializers, GraphQL, versioning, rate limiting
39
+ - **Test infrastructure** — framework, factories/fixtures, CI config, coverage
40
+ - **Configuration** — cache store, session store, middleware, initializers
41
+ - **Asset pipeline** — Propshaft/Sprockets, importmaps, CSS framework, JS bundler
42
+ - **DevOps** — Puma config, Procfile, Docker, deployment tools
43
+ - **Your architecture patterns**: service objects, STI, polymorphism, state machines, multi-tenancy
44
+ - **Stimulus controllers** with targets, values, actions, outlets, and classes
35
45
 
36
46
  ---
37
47
 
@@ -51,12 +61,12 @@ rails ai:context
51
61
  ```
52
62
 
53
63
  This creates:
54
- - `CLAUDE.md` — for Claude Code
55
- - `.cursorrules` — for Cursor
56
- - `.windsurfrules` — for Windsurf
57
- - `.github/copilot-instructions.md` — for GitHub Copilot
64
+ - `CLAUDE.md` — for Claude Code (with behavioral rules)
65
+ - `.cursorrules` — for Cursor (compact rules format)
66
+ - `.windsurfrules` — for Windsurf (compact rules format)
67
+ - `.github/copilot-instructions.md` — for GitHub Copilot (task-oriented)
58
68
 
59
- **Commit these files.** Your entire team gets smarter AI assistance.
69
+ Each file is tailored to the AI assistant's preferred format. **Commit these files.** Your entire team gets smarter AI assistance.
60
70
 
61
71
  ### 3. Start the MCP Server
62
72
 
@@ -84,7 +94,7 @@ Or add to your Claude Code config (`~/.claude/claude_desktop_config.json`):
84
94
 
85
95
  ## MCP Tools
86
96
 
87
- The gem exposes 6 tools via MCP that AI clients can call:
97
+ The gem exposes 9 tools via MCP that AI clients can call:
88
98
 
89
99
  | Tool | Description | Annotations |
90
100
  |------|-------------|-------------|
@@ -94,9 +104,27 @@ The gem exposes 6 tools via MCP that AI clients can call:
94
104
  | `rails_get_gems` | Notable gems categorized by function with explanations | read-only, idempotent |
95
105
  | `rails_search_code` | Ripgrep-powered code search across the codebase | read-only, idempotent |
96
106
  | `rails_get_conventions` | Architecture patterns, directory structure, config files | read-only, idempotent |
107
+ | `rails_get_controllers` | Controller actions, filters, strong params, concerns | read-only, idempotent |
108
+ | `rails_get_config` | App configuration: cache, sessions, middleware, initializers | read-only, idempotent |
109
+ | `rails_get_test_info` | Test framework, factories, CI config, coverage | read-only, idempotent |
97
110
 
98
111
  All tools are **read-only** — they never modify your application or database.
99
112
 
113
+ ## MCP Resources
114
+
115
+ In addition to tools, the gem registers MCP resources that AI clients can read directly:
116
+
117
+ | Resource | Description |
118
+ |----------|-------------|
119
+ | `rails://schema` | Full database schema (JSON) |
120
+ | `rails://routes` | All routes (JSON) |
121
+ | `rails://conventions` | Detected patterns and architecture (JSON) |
122
+ | `rails://gems` | Notable gems with categories (JSON) |
123
+ | `rails://controllers` | All controllers with actions and filters (JSON) |
124
+ | `rails://config` | Application configuration (JSON) |
125
+ | `rails://tests` | Test infrastructure details (JSON) |
126
+ | `rails://models/{name}` | Per-model details (resource template) |
127
+
100
128
  ---
101
129
 
102
130
  ## How It Works
@@ -124,9 +152,10 @@ All tools are **read-only** — they never modify your application or database.
124
152
  (reads file) any MCP client
125
153
  ```
126
154
 
127
- **Two modes:**
155
+ **Three modes:**
128
156
  1. **Static files** (`rails ai:context`) — generates markdown files that AI tools read as project context. Zero runtime cost. Works everywhere.
129
157
  2. **MCP server** (`rails ai:serve`) — live introspection tools that AI clients call on-demand. Richer, always up-to-date.
158
+ 3. **Watch mode** (`rails ai:watch`) — auto-regenerates context files when your code changes.
130
159
 
131
160
  ---
132
161
 
@@ -145,20 +174,49 @@ RailsAiContext.configure do |config|
145
174
  config.auto_mount = true
146
175
  config.http_path = "/mcp"
147
176
  config.http_port = 6029
177
+
178
+ # Cache TTL for MCP tool responses (seconds)
179
+ config.cache_ttl = 30
180
+
181
+ # Enable Postgres row count stats (opt-in)
182
+ # config.introspectors += [:database_stats]
148
183
  end
149
184
  ```
150
185
 
151
186
  ---
152
187
 
188
+ ## Diagnostics
189
+
190
+ Check your app's AI readiness:
191
+
192
+ ```bash
193
+ rails ai:doctor
194
+ ```
195
+
196
+ Reports pass/warn/fail for schema, models, routes, gems, controllers, views, i18n, tests, context files, MCP server, and ripgrep. Includes fix suggestions and an AI readiness score (0-100).
197
+
198
+ ---
199
+
200
+ ## Stimulus Support
201
+
202
+ The gem automatically detects Stimulus controllers and extracts:
203
+ - Controller names (derived from filenames)
204
+ - Static targets, values, outlets, and classes
205
+ - Action methods
206
+
207
+ This gives AI assistants context about your frontend JavaScript alongside your backend Ruby.
208
+
209
+ ---
210
+
153
211
  ## Supported AI Assistants
154
212
 
155
- | AI Assistant | Context File | Command |
156
- |--------------|-------------|---------|
157
- | Claude Code | `CLAUDE.md` | `rails ai:context:claude` |
158
- | Cursor | `.cursorrules` | `rails ai:context:cursor` |
159
- | Windsurf | `.windsurfrules` | `rails ai:context:windsurf` |
160
- | GitHub Copilot | `.github/copilot-instructions.md` | `rails ai:context:copilot` |
161
- | JSON (generic) | `.ai-context.json` | `rails ai:context:json` |
213
+ | AI Assistant | Context File | Format | Command |
214
+ |--------------|-------------|--------|---------|
215
+ | Claude Code | `CLAUDE.md` | Verbose + behavioral rules | `rails ai:context:claude` |
216
+ | Cursor | `.cursorrules` | Compact imperative rules | `rails ai:context:cursor` |
217
+ | Windsurf | `.windsurfrules` | Compact imperative rules | `rails ai:context:windsurf` |
218
+ | GitHub Copilot | `.github/copilot-instructions.md` | Task-oriented GFM | `rails ai:context:copilot` |
219
+ | JSON (generic) | `.ai-context.json` | Structured JSON | `rails ai:context:json` |
162
220
 
163
221
  ---
164
222
 
@@ -166,7 +224,7 @@ end
166
224
 
167
225
  | Command | Description |
168
226
  |---------|-------------|
169
- | `rails ai:context` | Generate all context files (CLAUDE.md, .cursorrules, etc.) |
227
+ | `rails ai:context` | Generate all context files (skips unchanged) |
170
228
  | `rails ai:context:claude` | Generate CLAUDE.md only |
171
229
  | `rails ai:context:cursor` | Generate .cursorrules only |
172
230
  | `rails ai:context:windsurf` | Generate .windsurfrules only |
@@ -175,6 +233,8 @@ end
175
233
  | `rails ai:serve` | Start MCP server (stdio, for Claude Code) |
176
234
  | `rails ai:serve_http` | Start MCP server (HTTP, for remote clients) |
177
235
  | `rails ai:inspect` | Print introspection summary to stdout |
236
+ | `rails ai:doctor` | Run diagnostics and report AI readiness score |
237
+ | `rails ai:watch` | Watch for changes and auto-regenerate context files |
178
238
 
179
239
  > **zsh users:** The bracket syntax `rails ai:context_for[claude]` requires quoting in zsh (`rails 'ai:context_for[claude]'`). The named tasks above (`rails ai:context:claude`) work without quoting in any shell.
180
240
 
@@ -196,6 +256,8 @@ The gem gracefully degrades when no database is connected — it parses `db/sche
196
256
  - Ruby >= 3.2
197
257
  - Rails >= 7.1
198
258
  - [mcp](https://github.com/modelcontextprotocol/ruby-sdk) (official MCP SDK, installed automatically)
259
+ - Optional: `listen` gem for watch mode
260
+ - Optional: `ripgrep` for fast code search (falls back to Ruby)
199
261
 
200
262
  ---
201
263
 
@@ -225,6 +287,10 @@ bundle exec rspec
225
287
 
226
288
  Bug reports and pull requests welcome at https://github.com/crisnahine/rails-ai-context.
227
289
 
290
+ ## Sponsorship
291
+
292
+ If rails-ai-context helps your workflow, consider supporting the project — [become a monthly sponsor or buy me a coffee](https://github.com/sponsors/crisnahine).
293
+
228
294
  ## License
229
295
 
230
296
  [MIT License](LICENSE)
data/exe/rails-ai-context CHANGED
@@ -28,8 +28,9 @@ module RailsAiContext
28
28
 
29
29
  format = options[:format].to_sym
30
30
  $stderr.puts "Introspecting Rails app..."
31
- files = RailsAiContext.generate_context(format: format)
32
- files.each { |f| $stderr.puts " Written: #{f}" }
31
+ result = RailsAiContext.generate_context(format: format)
32
+ result[:written].each { |f| $stderr.puts " Written: #{f}" }
33
+ result[:skipped].each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
33
34
  end
34
35
 
35
36
  desc "inspect", "Print introspection summary"
@@ -42,6 +43,33 @@ module RailsAiContext
42
43
  puts JSON.pretty_generate(context)
43
44
  end
44
45
 
46
+ desc "watch", "Watch for changes and auto-regenerate context files"
47
+ def watch
48
+ boot_rails!
49
+ require "rails_ai_context"
50
+
51
+ RailsAiContext::Watcher.new.start
52
+ end
53
+
54
+ desc "doctor", "Run diagnostic checks and report AI readiness score"
55
+ def doctor
56
+ boot_rails!
57
+ require "rails_ai_context"
58
+
59
+ result = RailsAiContext::Doctor.new.run
60
+ result[:checks].each do |check|
61
+ icon = case check.status
62
+ when :pass then "PASS"
63
+ when :warn then "WARN"
64
+ when :fail then "FAIL"
65
+ end
66
+ $stderr.puts " [#{icon}] #{check.name}: #{check.message}"
67
+ $stderr.puts " Fix: #{check.fix}" if check.fix
68
+ end
69
+ $stderr.puts ""
70
+ $stderr.puts "AI Readiness Score: #{result[:score]}/100"
71
+ end
72
+
45
73
  desc "version", "Print version"
46
74
  def version
47
75
  require_relative "../rails_ai_context/version"
@@ -26,10 +26,13 @@ module RailsAiContext
26
26
  # Maximum depth for association traversal
27
27
  attr_accessor :max_association_depth
28
28
 
29
+ # TTL in seconds for cached introspection (default: 30)
30
+ attr_accessor :cache_ttl
31
+
29
32
  def initialize
30
33
  @server_name = "rails-ai-context"
31
34
  @server_version = RailsAiContext::VERSION
32
- @introspectors = %i[schema models routes jobs gems conventions]
35
+ @introspectors = %i[schema models routes jobs gems conventions stimulus controllers views turbo i18n config active_storage action_text auth api tests rake_tasks assets devops action_mailbox]
33
36
  @excluded_paths = %w[node_modules tmp log vendor .git]
34
37
  @auto_mount = false
35
38
  @http_path = "/mcp"
@@ -43,6 +46,7 @@ module RailsAiContext
43
46
  ActionMailbox::InboundEmail ActionMailbox::Record
44
47
  ]
45
48
  @max_association_depth = 2
49
+ @cache_ttl = 30
46
50
  end
47
51
 
48
52
  def output_dir_for(app)
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ # Diagnostic checker that validates the environment and reports
5
+ # AI readiness with pass/warn/fail checks and a readiness score.
6
+ class Doctor
7
+ Check = Data.define(:name, :status, :message, :fix)
8
+
9
+ CHECKS = %i[
10
+ check_schema
11
+ check_models
12
+ check_routes
13
+ check_gems
14
+ check_controllers
15
+ check_views
16
+ check_i18n
17
+ check_tests
18
+ check_context_files
19
+ check_mcp_buildable
20
+ check_ripgrep
21
+ ].freeze
22
+
23
+ attr_reader :app
24
+
25
+ def initialize(app = nil)
26
+ @app = app || Rails.application
27
+ end
28
+
29
+ def run
30
+ results = CHECKS.map { |check| send(check) }
31
+ score = compute_score(results)
32
+ { checks: results, score: score }
33
+ end
34
+
35
+ private
36
+
37
+ def check_schema
38
+ schema_path = File.join(app.root, "db/schema.rb")
39
+ if File.exist?(schema_path)
40
+ Check.new(name: "Schema", status: :pass, message: "db/schema.rb found", fix: nil)
41
+ else
42
+ Check.new(name: "Schema", status: :warn, message: "db/schema.rb not found", fix: "Run `rails db:schema:dump` to generate it")
43
+ end
44
+ end
45
+
46
+ def check_models
47
+ models_dir = File.join(app.root, "app/models")
48
+ if Dir.exist?(models_dir) && Dir.glob(File.join(models_dir, "**/*.rb")).any?
49
+ count = Dir.glob(File.join(models_dir, "**/*.rb")).size
50
+ Check.new(name: "Models", status: :pass, message: "#{count} model files found", fix: nil)
51
+ else
52
+ Check.new(name: "Models", status: :warn, message: "No model files found in app/models/", fix: "Generate models with `rails generate model`")
53
+ end
54
+ end
55
+
56
+ def check_routes
57
+ routes_path = File.join(app.root, "config/routes.rb")
58
+ if File.exist?(routes_path)
59
+ Check.new(name: "Routes", status: :pass, message: "config/routes.rb found", fix: nil)
60
+ else
61
+ Check.new(name: "Routes", status: :fail, message: "config/routes.rb not found", fix: "Ensure you're in a Rails app root directory")
62
+ end
63
+ end
64
+
65
+ def check_gems
66
+ lock_path = File.join(app.root, "Gemfile.lock")
67
+ if File.exist?(lock_path)
68
+ Check.new(name: "Gems", status: :pass, message: "Gemfile.lock found", fix: nil)
69
+ else
70
+ Check.new(name: "Gems", status: :warn, message: "Gemfile.lock not found", fix: "Run `bundle install` to generate it")
71
+ end
72
+ end
73
+
74
+ def check_controllers
75
+ dir = File.join(app.root, "app/controllers")
76
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*.rb")).any?
77
+ count = Dir.glob(File.join(dir, "**/*.rb")).size
78
+ Check.new(name: "Controllers", status: :pass, message: "#{count} controller files found", fix: nil)
79
+ else
80
+ Check.new(name: "Controllers", status: :warn, message: "No controller files found in app/controllers/", fix: "Generate controllers with `rails generate controller`")
81
+ end
82
+ end
83
+
84
+ def check_views
85
+ dir = File.join(app.root, "app/views")
86
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.any?
87
+ count = Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.size
88
+ Check.new(name: "Views", status: :pass, message: "#{count} view files found", fix: nil)
89
+ else
90
+ Check.new(name: "Views", status: :warn, message: "No view files found in app/views/", fix: "Views are generated alongside controllers")
91
+ end
92
+ end
93
+
94
+ def check_i18n
95
+ dir = File.join(app.root, "config/locales")
96
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*.{yml,yaml}")).any?
97
+ count = Dir.glob(File.join(dir, "**/*.{yml,yaml}")).size
98
+ Check.new(name: "I18n", status: :pass, message: "#{count} locale files found", fix: nil)
99
+ else
100
+ Check.new(name: "I18n", status: :warn, message: "No locale files found in config/locales/", fix: "Add locale files for internationalization support")
101
+ end
102
+ end
103
+
104
+ def check_tests
105
+ if Dir.exist?(File.join(app.root, "spec")) || Dir.exist?(File.join(app.root, "test"))
106
+ framework = Dir.exist?(File.join(app.root, "spec")) ? "RSpec" : "Minitest"
107
+ Check.new(name: "Tests", status: :pass, message: "#{framework} test directory found", fix: nil)
108
+ else
109
+ Check.new(name: "Tests", status: :warn, message: "No test directory found", fix: "Set up tests with `rails generate rspec:install` or use default Minitest")
110
+ end
111
+ end
112
+
113
+ def check_context_files
114
+ claude_path = File.join(app.root, "CLAUDE.md")
115
+ if File.exist?(claude_path)
116
+ Check.new(name: "Context files", status: :pass, message: "CLAUDE.md exists", fix: nil)
117
+ else
118
+ Check.new(name: "Context files", status: :warn, message: "No context files generated yet", fix: "Run `rails ai:context` to generate them")
119
+ end
120
+ end
121
+
122
+ def check_mcp_buildable
123
+ Server.new(app).build
124
+ Check.new(name: "MCP server", status: :pass, message: "MCP server builds successfully", fix: nil)
125
+ rescue => e
126
+ Check.new(name: "MCP server", status: :fail, message: "MCP server failed to build: #{e.message}", fix: "Check mcp gem installation: `bundle info mcp`")
127
+ end
128
+
129
+ def check_ripgrep
130
+ if system("which rg > /dev/null 2>&1")
131
+ Check.new(name: "ripgrep", status: :pass, message: "rg available for code search", fix: nil)
132
+ else
133
+ Check.new(name: "ripgrep", status: :warn, message: "ripgrep not installed (code search will use slower Ruby fallback)", fix: "Install with `brew install ripgrep` or `apt install ripgrep`")
134
+ end
135
+ end
136
+
137
+ def compute_score(results)
138
+ total = results.size * 10
139
+ earned = results.sum do |check|
140
+ case check.status
141
+ when :pass then 10
142
+ when :warn then 5
143
+ else 0
144
+ end
145
+ end
146
+ ((earned.to_f / total) * 100).round
147
+ end
148
+ end
149
+ end
@@ -8,6 +8,14 @@ module RailsAiContext
8
8
  Rails.application.config.rails_ai_context = RailsAiContext.configuration
9
9
  end
10
10
 
11
+ # Auto-mount MCP HTTP middleware when configured
12
+ initializer "rails_ai_context.middleware" do |app|
13
+ if RailsAiContext.configuration.auto_mount
14
+ require_relative "middleware"
15
+ app.middleware.use RailsAiContext::Middleware
16
+ end
17
+ end
18
+
11
19
  # Register Rake tasks
12
20
  rake_tasks do
13
21
  load File.expand_path("tasks/rails_ai_context.rake", __dir__)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module RailsAiContext
6
+ # Computes a SHA256 fingerprint of key application files to detect changes.
7
+ # Used by BaseTool to invalidate cached introspection when files change.
8
+ class Fingerprinter
9
+ WATCHED_FILES = %w[
10
+ db/schema.rb
11
+ config/routes.rb
12
+ Gemfile.lock
13
+ ].freeze
14
+
15
+ WATCHED_DIRS = %w[
16
+ app/models
17
+ app/controllers
18
+ app/views
19
+ app/jobs
20
+ app/mailers
21
+ app/channels
22
+ app/javascript/controllers
23
+ config/initializers
24
+ lib/tasks
25
+ ].freeze
26
+
27
+ class << self
28
+ def compute(app)
29
+ root = app.root.to_s
30
+ digest = Digest::SHA256.new
31
+
32
+ WATCHED_FILES.each do |file|
33
+ path = File.join(root, file)
34
+ digest.update(File.mtime(path).to_f.to_s) if File.exist?(path)
35
+ end
36
+
37
+ WATCHED_DIRS.each do |dir|
38
+ full_dir = File.join(root, dir)
39
+ next unless Dir.exist?(full_dir)
40
+
41
+ Dir.glob(File.join(full_dir, "**/*.{rb,rake,js,ts,erb,haml,slim,yml}")).sort.each do |path|
42
+ digest.update(File.mtime(path).to_f.to_s)
43
+ end
44
+ end
45
+
46
+ digest.hexdigest
47
+ end
48
+
49
+ def changed?(app, previous)
50
+ compute(app) != previous
51
+ end
52
+ end
53
+ end
54
+ end
@@ -53,6 +53,22 @@ module RailsAiContext
53
53
  when :jobs then Introspectors::JobIntrospector.new(app)
54
54
  when :gems then Introspectors::GemIntrospector.new(app)
55
55
  when :conventions then Introspectors::ConventionDetector.new(app)
56
+ when :stimulus then Introspectors::StimulusIntrospector.new(app)
57
+ when :database_stats then Introspectors::DatabaseStatsIntrospector.new(app)
58
+ when :controllers then Introspectors::ControllerIntrospector.new(app)
59
+ when :views then Introspectors::ViewIntrospector.new(app)
60
+ when :turbo then Introspectors::TurboIntrospector.new(app)
61
+ when :i18n then Introspectors::I18nIntrospector.new(app)
62
+ when :config then Introspectors::ConfigIntrospector.new(app)
63
+ when :active_storage then Introspectors::ActiveStorageIntrospector.new(app)
64
+ when :action_text then Introspectors::ActionTextIntrospector.new(app)
65
+ when :auth then Introspectors::AuthIntrospector.new(app)
66
+ when :api then Introspectors::ApiIntrospector.new(app)
67
+ when :tests then Introspectors::TestIntrospector.new(app)
68
+ when :rake_tasks then Introspectors::RakeTaskIntrospector.new(app)
69
+ when :assets then Introspectors::AssetPipelineIntrospector.new(app)
70
+ when :devops then Introspectors::DevOpsIntrospector.new(app)
71
+ when :action_mailbox then Introspectors::ActionMailboxIntrospector.new(app)
56
72
  else
57
73
  raise ConfigurationError, "Unknown introspector: #{name}"
58
74
  end