rails-ai-context 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +4 -2
- data/README.md +93 -6
- data/lib/generators/rails_ai_context/install/install_generator.rb +21 -6
- data/lib/rails_ai_context/configuration.rb +15 -1
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +97 -0
- data/lib/rails_ai_context/serializers/claude_serializer.rb +213 -3
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +32 -1
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +94 -0
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +108 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +157 -0
- data/lib/rails_ai_context/serializers/rules_serializer.rb +100 -3
- data/lib/rails_ai_context/serializers/windsurf_rules_serializer.rb +52 -0
- data/lib/rails_ai_context/serializers/windsurf_serializer.rb +94 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +31 -3
- data/lib/rails_ai_context/tools/base_tool.rb +9 -2
- data/lib/rails_ai_context/tools/get_controllers.rb +53 -10
- data/lib/rails_ai_context/tools/get_model_details.rb +51 -11
- data/lib/rails_ai_context/tools/get_routes.rb +74 -16
- data/lib/rails_ai_context/tools/get_schema.rb +70 -10
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4aecabdd89f6a8bb9cb32189de791a98ebe632e3dcdb7406c4ae334eda9cb71
|
|
4
|
+
data.tar.gz: 546be87729c99383fd4eed6cb94df487f106fac6035e7a683902bf764694297b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cf2ebe640b6dcf2d98b48dac91efb7fde81804f26d3f2b71e4f6ee59de23ed10d15e4e8b0ae5a4fe6ffc5dff2d4fb6904a99e269153da9a54909050b23d7c123
|
|
7
|
+
data.tar.gz: 9f62a8d2e6a9e73fad88bf5bbf86881976f80aa687cee579b2116d26c3d3719d4bbb82e4226d6193531efe6299abeff8272eee82190fe4703efd8e427865e133
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.7.0] - 2026-03-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Detail levels on MCP tools** — `detail:"summary"`, `detail:"standard"` (default), `detail:"full"` on `rails_get_schema`, `rails_get_routes`, `rails_get_model_details`, `rails_get_controllers`. AI calls summary first, then drills down. Based on Anthropic's recommended MCP pattern.
|
|
13
|
+
- **Pagination** — `limit` and `offset` parameters on schema and routes tools for apps with hundreds of tables/routes.
|
|
14
|
+
- **Response size safety net** — Configurable hard cap (`max_tool_response_chars`, default 120K) on tool responses. Truncated responses include hints to use filters.
|
|
15
|
+
- **Compact CLAUDE.md** — New `:compact` context mode (default) generates ≤150 lines per Claude Code's official recommendation. Contains stack overview, key models, and MCP tool usage guide.
|
|
16
|
+
- **Full mode preserved** — `config.context_mode = :full` retains the existing full-dump behavior. Also available via `rails ai:context:full` or `CONTEXT_MODE=full`.
|
|
17
|
+
- **`.claude/rules/` generation** — Generates quick-reference files in `.claude/rules/` for schema and models. Auto-loaded by Claude Code alongside CLAUDE.md.
|
|
18
|
+
- **Cursor MDC rules** — Generates `.cursor/rules/*.mdc` files with YAML frontmatter (globs, alwaysApply). Project overview is always-on; model/controller rules auto-attach when working in matching directories. Legacy `.cursorrules` kept for backward compatibility.
|
|
19
|
+
- **Windsurf 6K compliance** — `.windsurfrules` is now hard-capped at 5,800 characters (within Windsurf's 6,000 char limit). Generates `.windsurf/rules/*.md` for the new rules format.
|
|
20
|
+
- **Copilot path-specific instructions** — Generates `.github/instructions/*.instructions.md` with `applyTo` frontmatter for model and controller contexts. Main `copilot-instructions.md` respects compact mode (≤500 lines).
|
|
21
|
+
- **`rails ai:context:full` task** — Dedicated rake task for full context dump.
|
|
22
|
+
- **Configurable limits** — `claude_max_lines` (default: 150), `max_tool_response_chars` (default: 120K).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Default `context_mode` is now `:compact` (was implicitly `:full`). Existing behavior available via `config.context_mode = :full`.
|
|
27
|
+
- Tools default to `detail:"standard"` which returns bounded results, not unlimited.
|
|
28
|
+
- All tools return pagination hints when results are truncated.
|
|
29
|
+
- `.windsurfrules` now uses dedicated `WindsurfSerializer` instead of sharing `RulesSerializer` with Cursor.
|
|
30
|
+
|
|
8
31
|
## [0.6.0] - 2026-03-18
|
|
9
32
|
|
|
10
33
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
10
10
|
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
11
11
|
- `lib/rails_ai_context/introspectors/` — 27 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, migrations, seeds, middleware, engines, multi_database)
|
|
12
12
|
- `lib/rails_ai_context/tools/` — 9 MCP tools using the official mcp SDK
|
|
13
|
-
- `lib/rails_ai_context/serializers/` — Output formatters (claude,
|
|
13
|
+
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON)
|
|
14
14
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
16
16
|
- `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
|
|
@@ -32,11 +32,13 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
32
32
|
8. **Zeitwerk autoloading** — files loaded on-demand, not all upfront
|
|
33
33
|
9. **Introspector presets** — `:standard` (9 core) default, `:full` (26) for power users
|
|
34
34
|
10. **MCP auto-discovery** — `.mcp.json` generated by install generator
|
|
35
|
+
11. **Compact by default** — context files ≤150 lines, MCP tools use `detail` parameter (summary/standard/full)
|
|
36
|
+
12. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
|
|
35
37
|
|
|
36
38
|
## Testing
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
bundle exec rspec # Run specs (
|
|
41
|
+
bundle exec rspec # Run specs (348 examples)
|
|
40
42
|
bundle exec rubocop # Lint
|
|
41
43
|
```
|
|
42
44
|
|
data/README.md
CHANGED
|
@@ -69,22 +69,66 @@ This creates:
|
|
|
69
69
|
|
|
70
70
|
Each file is tailored to the AI assistant's preferred format. **Commit these files.** Your entire team gets smarter AI assistance.
|
|
71
71
|
|
|
72
|
-
### 3. MCP Server
|
|
72
|
+
### 3. MCP Server
|
|
73
73
|
|
|
74
|
-
The
|
|
74
|
+
The gem provides a live MCP server that AI clients can query on-demand for always-up-to-date introspection. Two transports are available:
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
#### Auto-discovery (recommended)
|
|
77
|
+
|
|
78
|
+
The install generator creates a `.mcp.json` file in your project root:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"rails-ai-context": {
|
|
84
|
+
"command": "bundle",
|
|
85
|
+
"args": ["exec", "rails", "ai:serve"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Claude Code** and **Cursor** auto-detect this file — no manual config needed. Just open your project and the MCP tools are available.
|
|
92
|
+
|
|
93
|
+
#### Manual setup per AI client
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary><strong>Claude Code</strong></summary>
|
|
97
|
+
|
|
98
|
+
Already handled by `.mcp.json` auto-discovery. Or add manually:
|
|
77
99
|
|
|
78
100
|
```bash
|
|
79
|
-
rails ai:serve
|
|
101
|
+
claude mcp add rails-ai-context -- bundle exec rails ai:serve
|
|
102
|
+
```
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>Claude Desktop</strong></summary>
|
|
107
|
+
|
|
108
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"rails-ai-context": {
|
|
114
|
+
"command": "bundle",
|
|
115
|
+
"args": ["exec", "rails", "ai:serve"],
|
|
116
|
+
"cwd": "/path/to/your/rails/app"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
80
120
|
```
|
|
121
|
+
</details>
|
|
81
122
|
|
|
82
|
-
|
|
123
|
+
<details>
|
|
124
|
+
<summary><strong>Cursor</strong></summary>
|
|
125
|
+
|
|
126
|
+
Already handled by `.mcp.json` auto-discovery. Or add manually in Cursor Settings > MCP:
|
|
83
127
|
|
|
84
128
|
```json
|
|
85
129
|
{
|
|
86
130
|
"mcpServers": {
|
|
87
|
-
"
|
|
131
|
+
"rails-ai-context": {
|
|
88
132
|
"command": "bundle",
|
|
89
133
|
"args": ["exec", "rails", "ai:serve"],
|
|
90
134
|
"cwd": "/path/to/your/rails/app"
|
|
@@ -92,6 +136,31 @@ Or configure manually in `~/.claude/claude_desktop_config.json`:
|
|
|
92
136
|
}
|
|
93
137
|
}
|
|
94
138
|
```
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
#### HTTP transport (for remote clients)
|
|
142
|
+
|
|
143
|
+
For browser-based tools or remote AI clients, use the HTTP transport:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
rails ai:serve_http
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This starts a standalone HTTP server at `http://127.0.0.1:6029/mcp` using the Streamable HTTP transport.
|
|
150
|
+
|
|
151
|
+
Or auto-mount it inside your Rails app (no separate process needed):
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# config/initializers/rails_ai_context.rb
|
|
155
|
+
RailsAiContext.configure do |config|
|
|
156
|
+
config.auto_mount = true
|
|
157
|
+
config.http_path = "/mcp" # default
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This inserts Rack middleware that handles MCP requests at `/mcp` and passes everything else through to your Rails app.
|
|
162
|
+
|
|
163
|
+
> **Note:** Both transports are **read-only** — they expose the same 9 introspection tools and never modify your application or database. The stdio transport (`ai:serve`) is recommended for local development; HTTP is for remote or programmatic access.
|
|
95
164
|
|
|
96
165
|
---
|
|
97
166
|
|
|
@@ -230,6 +299,24 @@ This gives AI assistants context about your frontend JavaScript alongside your b
|
|
|
230
299
|
|
|
231
300
|
---
|
|
232
301
|
|
|
302
|
+
## Stack Compatibility
|
|
303
|
+
|
|
304
|
+
Works with every Rails architecture — the gem auto-detects what's relevant:
|
|
305
|
+
|
|
306
|
+
| Setup | Coverage | Notes |
|
|
307
|
+
|-------|----------|-------|
|
|
308
|
+
| Rails full-stack (ERB + Hotwire) | 27/27 | All introspectors relevant |
|
|
309
|
+
| Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
|
|
310
|
+
| Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
|
|
311
|
+
| Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
|
|
312
|
+
| Rails engine (mountable gem) | ~15/27 | Core introspectors (schema, models, routes, gems) work |
|
|
313
|
+
|
|
314
|
+
Introspectors that target frontend concerns (views, Turbo, Stimulus, assets) are less relevant for API-only apps, but they degrade gracefully — they simply report nothing when those features aren't present.
|
|
315
|
+
|
|
316
|
+
> **Tip:** API-only apps can use the `:standard` preset (9 core introspectors) for faster introspection, or cherry-pick with `config.introspectors += %i[auth api]`. See [Configuration](#configuration).
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
233
320
|
## Rake Tasks
|
|
234
321
|
|
|
235
322
|
| Command | Description |
|
|
@@ -41,6 +41,17 @@ module RailsAiContext
|
|
|
41
41
|
# Paths to exclude from code search
|
|
42
42
|
# config.excluded_paths += %w[vendor/bundle]
|
|
43
43
|
|
|
44
|
+
# Context mode for generated files (CLAUDE.md, .cursorrules, etc.)
|
|
45
|
+
# :compact — smart, ≤150 lines, references MCP tools for details (default)
|
|
46
|
+
# :full — dumps everything into context files (good for small apps <30 models)
|
|
47
|
+
# config.context_mode = :compact
|
|
48
|
+
|
|
49
|
+
# Max lines for CLAUDE.md in compact mode
|
|
50
|
+
# config.claude_max_lines = 150
|
|
51
|
+
|
|
52
|
+
# Max response size for MCP tool results (chars). Safety net for large apps.
|
|
53
|
+
# config.max_tool_response_chars = 120_000
|
|
54
|
+
|
|
44
55
|
# Auto-mount HTTP MCP endpoint at /mcp
|
|
45
56
|
# config.auto_mount = false
|
|
46
57
|
# config.http_path = "/mcp"
|
|
@@ -96,17 +107,21 @@ module RailsAiContext
|
|
|
96
107
|
say " rails ai:serve # Start MCP server (stdio)"
|
|
97
108
|
say " rails ai:inspect # Print introspection summary"
|
|
98
109
|
say ""
|
|
99
|
-
say "
|
|
100
|
-
say " Claude Code
|
|
101
|
-
say " Cursor
|
|
102
|
-
say " Windsurf
|
|
103
|
-
say " GitHub Copilot
|
|
110
|
+
say "Generated files per AI tool:", :yellow
|
|
111
|
+
say " Claude Code → CLAUDE.md + .claude/rules/*.md"
|
|
112
|
+
say " Cursor → .cursorrules + .cursor/rules/*.mdc"
|
|
113
|
+
say " Windsurf → .windsurfrules + .windsurf/rules/*.md"
|
|
114
|
+
say " GitHub Copilot → .github/copilot-instructions.md + .github/instructions/*.instructions.md"
|
|
104
115
|
say ""
|
|
105
116
|
say "MCP auto-discovery:", :yellow
|
|
106
117
|
say " .mcp.json is auto-detected by Claude Code and Cursor."
|
|
107
118
|
say " No manual MCP config needed — just open your project."
|
|
108
119
|
say ""
|
|
109
|
-
say "
|
|
120
|
+
say "Context modes:", :yellow
|
|
121
|
+
say " rails ai:context # compact mode (default, smart for any app size)"
|
|
122
|
+
say " rails ai:context:full # full dump (good for small apps)"
|
|
123
|
+
say ""
|
|
124
|
+
say "Commit context files and .mcp.json so your team benefits!", :green
|
|
110
125
|
end
|
|
111
126
|
end
|
|
112
127
|
end
|
|
@@ -33,6 +33,17 @@ module RailsAiContext
|
|
|
33
33
|
# TTL in seconds for cached introspection (default: 30)
|
|
34
34
|
attr_accessor :cache_ttl
|
|
35
35
|
|
|
36
|
+
# Context file generation mode
|
|
37
|
+
# :compact — ≤150 lines CLAUDE.md, references MCP tools for details (default)
|
|
38
|
+
# :full — current behavior, dumps everything into context files
|
|
39
|
+
attr_accessor :context_mode
|
|
40
|
+
|
|
41
|
+
# Max lines for generated CLAUDE.md (only applies in :compact mode)
|
|
42
|
+
attr_accessor :claude_max_lines
|
|
43
|
+
|
|
44
|
+
# Max characters for any single MCP tool response (safety net)
|
|
45
|
+
attr_accessor :max_tool_response_chars
|
|
46
|
+
|
|
36
47
|
def initialize
|
|
37
48
|
@server_name = "rails-ai-context"
|
|
38
49
|
@server_version = RailsAiContext::VERSION
|
|
@@ -49,7 +60,10 @@ module RailsAiContext
|
|
|
49
60
|
ActionText::RichText ActionText::EncryptedRichText
|
|
50
61
|
ActionMailbox::InboundEmail ActionMailbox::Record
|
|
51
62
|
]
|
|
52
|
-
@cache_ttl
|
|
63
|
+
@cache_ttl = 30
|
|
64
|
+
@context_mode = :compact
|
|
65
|
+
@claude_max_lines = 150
|
|
66
|
+
@max_tool_response_chars = 120_000
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
def preset=(name)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Serializers
|
|
5
|
+
# Generates .claude/rules/ files for Claude Code auto-discovery.
|
|
6
|
+
# These provide quick-reference lists without bloating CLAUDE.md.
|
|
7
|
+
class ClaudeRulesSerializer
|
|
8
|
+
attr_reader :context
|
|
9
|
+
|
|
10
|
+
def initialize(context)
|
|
11
|
+
@context = context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param output_dir [String] Rails root path
|
|
15
|
+
# @return [Hash] { written: [paths], skipped: [paths] }
|
|
16
|
+
def call(output_dir)
|
|
17
|
+
rules_dir = File.join(output_dir, ".claude", "rules")
|
|
18
|
+
FileUtils.mkdir_p(rules_dir)
|
|
19
|
+
|
|
20
|
+
written = []
|
|
21
|
+
skipped = []
|
|
22
|
+
|
|
23
|
+
files = {
|
|
24
|
+
"rails-schema.md" => render_schema_reference,
|
|
25
|
+
"rails-models.md" => render_models_reference
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
files.each do |filename, content|
|
|
29
|
+
next unless content
|
|
30
|
+
|
|
31
|
+
filepath = File.join(rules_dir, filename)
|
|
32
|
+
if File.exist?(filepath) && File.read(filepath) == content
|
|
33
|
+
skipped << filepath
|
|
34
|
+
else
|
|
35
|
+
File.write(filepath, content)
|
|
36
|
+
written << filepath
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
{ written: written, skipped: skipped }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def render_schema_reference
|
|
46
|
+
schema = context[:schema]
|
|
47
|
+
return nil unless schema.is_a?(Hash) && !schema[:error]
|
|
48
|
+
tables = schema[:tables] || {}
|
|
49
|
+
return nil if tables.empty?
|
|
50
|
+
|
|
51
|
+
lines = [
|
|
52
|
+
"# Database Tables (#{tables.size})",
|
|
53
|
+
"",
|
|
54
|
+
"For full column details, use the `rails_get_schema` MCP tool.",
|
|
55
|
+
"Call with `detail:\"summary\"` first, then `table:\"name\"` for specifics.",
|
|
56
|
+
""
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
tables.keys.sort.each do |name|
|
|
60
|
+
data = tables[name]
|
|
61
|
+
col_count = data[:columns]&.size || 0
|
|
62
|
+
pk = data[:primary_key] || "id"
|
|
63
|
+
lines << "- #{name} (#{col_count} cols, pk: #{pk})"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_models_reference
|
|
70
|
+
models = context[:models]
|
|
71
|
+
return nil unless models.is_a?(Hash) && !models[:error]
|
|
72
|
+
return nil if models.empty?
|
|
73
|
+
|
|
74
|
+
lines = [
|
|
75
|
+
"# ActiveRecord Models (#{models.size})",
|
|
76
|
+
"",
|
|
77
|
+
"For full details, use `rails_get_model_details` MCP tool.",
|
|
78
|
+
"Call with no args to list all, then `model:\"Name\"` for specifics.",
|
|
79
|
+
""
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
models.keys.sort.each do |name|
|
|
83
|
+
data = models[name]
|
|
84
|
+
assocs = (data[:associations] || []).size
|
|
85
|
+
vals = (data[:validations] || []).size
|
|
86
|
+
table = data[:table_name]
|
|
87
|
+
line = "- #{name}"
|
|
88
|
+
line += " (table: #{table})" if table
|
|
89
|
+
line += " — #{assocs} assocs, #{vals} validations"
|
|
90
|
+
lines << line
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
lines.join("\n")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -2,9 +2,219 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
module Serializers
|
|
5
|
-
# Generates
|
|
6
|
-
#
|
|
7
|
-
|
|
5
|
+
# Generates CLAUDE.md optimized for Claude Code.
|
|
6
|
+
# In :compact mode (default), produces ≤150 lines with MCP tool references.
|
|
7
|
+
# In :full mode, delegates to MarkdownSerializer with behavioral rules.
|
|
8
|
+
class ClaudeSerializer
|
|
9
|
+
attr_reader :context
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
if RailsAiContext.configuration.context_mode == :full
|
|
17
|
+
FullClaudeSerializer.new(context).call
|
|
18
|
+
else
|
|
19
|
+
render_compact
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_compact
|
|
26
|
+
lines = []
|
|
27
|
+
lines.concat(render_header)
|
|
28
|
+
lines.concat(render_stack_overview)
|
|
29
|
+
lines.concat(render_key_models)
|
|
30
|
+
lines.concat(render_notable_gems)
|
|
31
|
+
lines.concat(render_architecture)
|
|
32
|
+
lines.concat(render_mcp_guide)
|
|
33
|
+
lines.concat(render_conventions)
|
|
34
|
+
lines.concat(render_commands)
|
|
35
|
+
lines.concat(render_footer)
|
|
36
|
+
|
|
37
|
+
# Enforce max lines
|
|
38
|
+
max = RailsAiContext.configuration.claude_max_lines
|
|
39
|
+
if lines.size > max
|
|
40
|
+
lines = lines.first(max - 2)
|
|
41
|
+
lines << ""
|
|
42
|
+
lines << "_Context trimmed. Use MCP tools for full details._"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_header
|
|
49
|
+
[
|
|
50
|
+
"# #{context[:app_name]} — AI Context",
|
|
51
|
+
"",
|
|
52
|
+
"> Rails #{context[:rails_version]} | Ruby #{context[:ruby_version]}",
|
|
53
|
+
"> Generated by rails-ai-context v#{RailsAiContext::VERSION}",
|
|
54
|
+
""
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_stack_overview
|
|
59
|
+
lines = [ "## Stack" ]
|
|
60
|
+
|
|
61
|
+
schema = context[:schema]
|
|
62
|
+
if schema && !schema[:error]
|
|
63
|
+
lines << "- Database: #{schema[:adapter]} — #{schema[:total_tables]} tables"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
models = context[:models]
|
|
67
|
+
if models.is_a?(Hash) && !models[:error]
|
|
68
|
+
lines << "- Models: #{models.size}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
routes = context[:routes]
|
|
72
|
+
if routes && !routes[:error]
|
|
73
|
+
ctrl_count = (routes[:by_controller] || {}).keys.size
|
|
74
|
+
lines << "- Routes: #{routes[:total_routes]} across #{ctrl_count} controllers"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
auth = context[:auth]
|
|
78
|
+
if auth.is_a?(Hash) && !auth[:error]
|
|
79
|
+
parts = []
|
|
80
|
+
parts << "Devise" if auth.dig(:authentication, :devise)&.any?
|
|
81
|
+
parts << "Rails 8 auth" if auth.dig(:authentication, :rails_auth)
|
|
82
|
+
parts << "Pundit" if auth.dig(:authorization, :pundit)&.any?
|
|
83
|
+
parts << "CanCanCan" if auth.dig(:authorization, :cancancan)
|
|
84
|
+
lines << "- Auth: #{parts.join(' + ')}" if parts.any?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
jobs = context[:jobs]
|
|
88
|
+
if jobs.is_a?(Hash) && !jobs[:error]
|
|
89
|
+
job_count = jobs[:jobs]&.size || 0
|
|
90
|
+
mailer_count = jobs[:mailers]&.size || 0
|
|
91
|
+
channel_count = jobs[:channels]&.size || 0
|
|
92
|
+
parts = []
|
|
93
|
+
parts << "#{job_count} jobs" if job_count > 0
|
|
94
|
+
parts << "#{mailer_count} mailers" if mailer_count > 0
|
|
95
|
+
parts << "#{channel_count} channels" if channel_count > 0
|
|
96
|
+
lines << "- Async: #{parts.join(', ')}" if parts.any?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
migrations = context[:migrations]
|
|
100
|
+
if migrations.is_a?(Hash) && !migrations[:error]
|
|
101
|
+
pending = migrations[:pending]
|
|
102
|
+
lines << "- Migrations: #{migrations[:total]} total, #{pending&.size || 0} pending"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
lines << ""
|
|
106
|
+
lines
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_key_models
|
|
110
|
+
models = context[:models]
|
|
111
|
+
return [] unless models.is_a?(Hash) && !models[:error] && models.any?
|
|
112
|
+
|
|
113
|
+
max_show = 15
|
|
114
|
+
lines = [ "## Key models (#{models.size} total)" ]
|
|
115
|
+
models.keys.sort.first(max_show).each do |name|
|
|
116
|
+
data = models[name]
|
|
117
|
+
assoc_count = (data[:associations] || []).size
|
|
118
|
+
val_count = (data[:validations] || []).size
|
|
119
|
+
top_assocs = (data[:associations] || []).first(3).map { |a| "#{a[:type]} :#{a[:name]}" }.join(", ")
|
|
120
|
+
line = "- **#{name}**"
|
|
121
|
+
line += " (#{assoc_count}a, #{val_count}v)" if assoc_count > 0 || val_count > 0
|
|
122
|
+
line += " — #{top_assocs}" if top_assocs && !top_assocs.empty?
|
|
123
|
+
lines << line
|
|
124
|
+
end
|
|
125
|
+
lines << "- _...#{models.size - max_show} more (use `rails_get_model_details` tool)_" if models.size > max_show
|
|
126
|
+
lines << ""
|
|
127
|
+
lines
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_notable_gems
|
|
131
|
+
gems = context[:gems]
|
|
132
|
+
return [] unless gems.is_a?(Hash) && !gems[:error]
|
|
133
|
+
notable = gems[:notable_gems] || gems[:notable] || gems[:detected] || []
|
|
134
|
+
return [] if notable.empty?
|
|
135
|
+
|
|
136
|
+
lines = [ "## Gems" ]
|
|
137
|
+
grouped = notable.group_by { |g| g[:category]&.to_s || "other" }
|
|
138
|
+
grouped.each do |category, gem_list|
|
|
139
|
+
names = gem_list.map { |g| g[:name] }.join(", ")
|
|
140
|
+
lines << "- **#{category}**: #{names}"
|
|
141
|
+
end
|
|
142
|
+
lines << ""
|
|
143
|
+
lines
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render_architecture
|
|
147
|
+
conv = context[:conventions]
|
|
148
|
+
return [] unless conv.is_a?(Hash) && !conv[:error]
|
|
149
|
+
|
|
150
|
+
arch = conv[:architecture] || []
|
|
151
|
+
patterns = conv[:patterns] || []
|
|
152
|
+
return [] if arch.empty? && patterns.empty?
|
|
153
|
+
|
|
154
|
+
lines = [ "## Architecture" ]
|
|
155
|
+
arch.each { |p| lines << "- #{p}" }
|
|
156
|
+
patterns.first(8).each { |p| lines << "- #{p}" }
|
|
157
|
+
lines << ""
|
|
158
|
+
lines
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_mcp_guide
|
|
162
|
+
[
|
|
163
|
+
"## MCP tools (live introspection)",
|
|
164
|
+
"",
|
|
165
|
+
"This project exposes MCP tools. Always use `detail:\"summary\"` first,",
|
|
166
|
+
"then drill into specifics with `detail:\"full\"` + a filter.",
|
|
167
|
+
"",
|
|
168
|
+
"- `rails_get_schema` — tables, columns, indexes, foreign keys",
|
|
169
|
+
"- `rails_get_model_details` — associations, validations, scopes, enums",
|
|
170
|
+
"- `rails_get_routes` — HTTP verbs, paths, controller actions",
|
|
171
|
+
"- `rails_get_controllers` — actions, filters, strong params",
|
|
172
|
+
"- `rails_get_config` — cache, session, timezone, middleware",
|
|
173
|
+
"- `rails_get_test_info` — framework, factories, CI config",
|
|
174
|
+
"- `rails_get_gems` — categorized gem analysis",
|
|
175
|
+
"- `rails_get_conventions` — architecture and design patterns",
|
|
176
|
+
"- `rails_search_code` — regex search across the codebase",
|
|
177
|
+
""
|
|
178
|
+
]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def render_conventions
|
|
182
|
+
conv = context[:conventions]
|
|
183
|
+
return [] unless conv.is_a?(Hash) && !conv[:error]
|
|
184
|
+
|
|
185
|
+
config_files = conv[:config_files] || []
|
|
186
|
+
return [] if config_files.empty?
|
|
187
|
+
|
|
188
|
+
lines = [ "## Key config files" ]
|
|
189
|
+
config_files.first(5).each { |f| lines << "- `#{f}`" }
|
|
190
|
+
lines << ""
|
|
191
|
+
lines
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def render_commands
|
|
195
|
+
[
|
|
196
|
+
"## Commands",
|
|
197
|
+
"- `bin/dev` — start dev server",
|
|
198
|
+
"- `bundle exec rspec` — run tests",
|
|
199
|
+
"- `rails db:migrate` — run pending migrations",
|
|
200
|
+
""
|
|
201
|
+
]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def render_footer
|
|
205
|
+
[
|
|
206
|
+
"## Rules",
|
|
207
|
+
"- Follow existing patterns and conventions",
|
|
208
|
+
"- Use the MCP tools to check schema before writing migrations",
|
|
209
|
+
"- Match existing code style",
|
|
210
|
+
"- Run tests after changes",
|
|
211
|
+
""
|
|
212
|
+
]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Internal: full-mode Claude serializer (wraps MarkdownSerializer with behavioral rules)
|
|
217
|
+
class FullClaudeSerializer < MarkdownSerializer
|
|
8
218
|
private
|
|
9
219
|
|
|
10
220
|
def header
|
|
@@ -4,6 +4,7 @@ module RailsAiContext
|
|
|
4
4
|
module Serializers
|
|
5
5
|
# Orchestrates writing context files to disk in various formats.
|
|
6
6
|
# Supports: CLAUDE.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, JSON
|
|
7
|
+
# Also generates split rule files for AI tools that support them.
|
|
7
8
|
class ContextFileSerializer
|
|
8
9
|
attr_reader :context, :format
|
|
9
10
|
|
|
@@ -50,6 +51,9 @@ module RailsAiContext
|
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
# Generate split rule files for all AI tools that support them
|
|
55
|
+
generate_split_rules(formats, output_dir, written, skipped)
|
|
56
|
+
|
|
53
57
|
{ written: written, skipped: skipped }
|
|
54
58
|
end
|
|
55
59
|
|
|
@@ -59,11 +63,38 @@ module RailsAiContext
|
|
|
59
63
|
case fmt
|
|
60
64
|
when :json then JsonSerializer.new(context).call
|
|
61
65
|
when :claude then ClaudeSerializer.new(context).call
|
|
62
|
-
when :cursor
|
|
66
|
+
when :cursor then RulesSerializer.new(context).call
|
|
67
|
+
when :windsurf then WindsurfSerializer.new(context).call
|
|
63
68
|
when :copilot then CopilotSerializer.new(context).call
|
|
64
69
|
else MarkdownSerializer.new(context).call
|
|
65
70
|
end
|
|
66
71
|
end
|
|
72
|
+
|
|
73
|
+
def generate_split_rules(formats, output_dir, written, skipped)
|
|
74
|
+
if formats.include?(:claude)
|
|
75
|
+
result = ClaudeRulesSerializer.new(context).call(output_dir)
|
|
76
|
+
written.concat(result[:written])
|
|
77
|
+
skipped.concat(result[:skipped])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if formats.include?(:cursor)
|
|
81
|
+
result = CursorRulesSerializer.new(context).call(output_dir)
|
|
82
|
+
written.concat(result[:written])
|
|
83
|
+
skipped.concat(result[:skipped])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if formats.include?(:windsurf)
|
|
87
|
+
result = WindsurfRulesSerializer.new(context).call(output_dir)
|
|
88
|
+
written.concat(result[:written])
|
|
89
|
+
skipped.concat(result[:skipped])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if formats.include?(:copilot)
|
|
93
|
+
result = CopilotInstructionsSerializer.new(context).call(output_dir)
|
|
94
|
+
written.concat(result[:written])
|
|
95
|
+
skipped.concat(result[:skipped])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
67
98
|
end
|
|
68
99
|
end
|
|
69
100
|
end
|