rails-ai-context 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +2 -2
- data/CONTRIBUTING.md +2 -1
- data/README.md +6 -1
- data/SECURITY.md +2 -2
- data/demo_script.sh +9 -1
- data/docs/GUIDE.md +37 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +1 -0
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +9 -1
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +110 -0
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +282 -0
- data/lib/rails_ai_context/server.rb +12 -9
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +2 -1
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04212baad7453e8371662a226fa50dfa4b65e4b611e94af22c50ab09e314d888
|
|
4
|
+
data.tar.gz: 43140d9b9f6151a4f5e72427efd8fcba0ed4a8256ed3f3c56f6a1c1ff53fd436
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 373de7c5e83e959cc64c96ab3937e9160da315c9dde34e12f2fe506f5ee6b815cbe71a49af3e6015fe0b75b89316810ff067deb0fa839a5dd4cff6836cb33f07
|
|
7
|
+
data.tar.gz: 8c9b16584f464c0603cb04f92ab8d7daf0f8dfe04f15a78e21a6089c344bcd0aedf9a6a59e748e9a90d24a356ad27f0b69cde8512aa3fd7db1995a84641e7a22
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.8.1] - 2026-03-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **OpenCode support** — generates `AGENTS.md` (native OpenCode context file) plus per-directory `app/models/AGENTS.md` and `app/controllers/AGENTS.md` that OpenCode auto-loads when reading files in those directories. Falls back to `CLAUDE.md` when no `AGENTS.md` exists. New command: `rails ai:context:opencode`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Live reload LoadError in HTTP mode** — when `live_reload = true` and the `listen` gem was missing, the `start_http` method's rescue block (for rackup fallback) swallowed the live reload error, producing a confusing rack error instead of the correct "listen gem required" message. The rescue is now scoped to the rackup require only.
|
|
17
|
+
- **Dangling @live_reload reference** — `@live_reload` was assigned before `start` was called. If `start` raised LoadError, the instance variable pointed to a non-functional object. Now only assigned after successful start.
|
|
18
|
+
|
|
8
19
|
## [0.8.0] - 2026-03-19
|
|
9
20
|
|
|
10
21
|
### 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, claude_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON)
|
|
13
|
+
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, 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
|
|
@@ -39,7 +39,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
39
39
|
## Testing
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
bundle exec rspec # Run specs (
|
|
42
|
+
bundle exec rspec # Run specs (373 examples)
|
|
43
43
|
bundle exec rubocop # Lint
|
|
44
44
|
```
|
|
45
45
|
|
data/CONTRIBUTING.md
CHANGED
|
@@ -20,8 +20,9 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
|
|
|
20
20
|
lib/rails_ai_context/
|
|
21
21
|
├── introspectors/ # 27 introspectors (schema, models, routes, etc.)
|
|
22
22
|
├── tools/ # 9 MCP tools with detail levels and pagination
|
|
23
|
-
├── serializers/ # Per-assistant formatters (claude, cursor, windsurf, copilot, JSON)
|
|
23
|
+
├── serializers/ # Per-assistant formatters (claude, opencode, cursor, windsurf, copilot, JSON)
|
|
24
24
|
├── server.rb # MCP server setup (stdio + HTTP)
|
|
25
|
+
├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
|
|
25
26
|
├── engine.rb # Rails Engine for auto-integration
|
|
26
27
|
└── configuration.rb # User-facing config (presets, context_mode, limits)
|
|
27
28
|
```
|
data/README.md
CHANGED
|
@@ -65,6 +65,11 @@ your-rails-app/
|
|
|
65
65
|
│ ├── rails-controllers.mdc globs: app/controllers/**
|
|
66
66
|
│ └── rails-mcp-tools.mdc alwaysApply: true
|
|
67
67
|
│
|
|
68
|
+
├── ⚡ OpenCode
|
|
69
|
+
│ ├── AGENTS.md native OpenCode context
|
|
70
|
+
│ ├── app/models/AGENTS.md auto-loaded when editing models
|
|
71
|
+
│ └── app/controllers/AGENTS.md auto-loaded when editing controllers
|
|
72
|
+
│
|
|
68
73
|
├── 🔵 Windsurf
|
|
69
74
|
│ ├── .windsurfrules ≤5,800 chars (6K limit)
|
|
70
75
|
│ └── .windsurf/rules/
|
|
@@ -347,7 +352,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
|
|
|
347
352
|
```bash
|
|
348
353
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
349
354
|
cd rails-ai-context && bundle install
|
|
350
|
-
bundle exec rspec #
|
|
355
|
+
bundle exec rspec # 373 examples
|
|
351
356
|
bundle exec rubocop # Lint
|
|
352
357
|
```
|
|
353
358
|
|
data/SECURITY.md
CHANGED
data/demo_script.sh
CHANGED
|
@@ -8,7 +8,7 @@ echo 'Fetching gem metadata from https://rubygems.org...'
|
|
|
8
8
|
sleep 0.3
|
|
9
9
|
echo 'Resolving dependencies...'
|
|
10
10
|
sleep 0.3
|
|
11
|
-
echo 'Installing rails-ai-context 0.
|
|
11
|
+
echo 'Installing rails-ai-context 0.8.1'
|
|
12
12
|
echo ''
|
|
13
13
|
sleep 1
|
|
14
14
|
|
|
@@ -36,6 +36,8 @@ sleep 0.15
|
|
|
36
36
|
echo ' ✅ MCP Server Ready (stdio transport)'
|
|
37
37
|
sleep 0.15
|
|
38
38
|
echo ' ✅ Ripgrep Installed (fast code search)'
|
|
39
|
+
sleep 0.15
|
|
40
|
+
echo ' ✅ Live reload `listen` gem available'
|
|
39
41
|
echo ''
|
|
40
42
|
sleep 0.3
|
|
41
43
|
printf ' \033[1;32mAI Readiness Score: 92/100\033[0m\n'
|
|
@@ -53,6 +55,12 @@ echo ' ✅ .claude/rules/rails-models.md'
|
|
|
53
55
|
sleep 0.08
|
|
54
56
|
echo ' ✅ .claude/rules/rails-mcp-tools.md'
|
|
55
57
|
sleep 0.08
|
|
58
|
+
echo ' ✅ AGENTS.md'
|
|
59
|
+
sleep 0.08
|
|
60
|
+
echo ' ✅ app/models/AGENTS.md'
|
|
61
|
+
sleep 0.08
|
|
62
|
+
echo ' ✅ app/controllers/AGENTS.md'
|
|
63
|
+
sleep 0.08
|
|
56
64
|
echo ' ✅ .cursorrules'
|
|
57
65
|
sleep 0.08
|
|
58
66
|
echo ' ✅ .cursor/rules/rails-project.mdc'
|
data/docs/GUIDE.md
CHANGED
|
@@ -125,7 +125,7 @@ end
|
|
|
125
125
|
|
|
126
126
|
## Generated Files
|
|
127
127
|
|
|
128
|
-
`rails ai:context` generates **
|
|
128
|
+
`rails ai:context` generates **20 files** across all AI assistants:
|
|
129
129
|
|
|
130
130
|
### Claude Code (4 files)
|
|
131
131
|
|
|
@@ -136,6 +136,14 @@ end
|
|
|
136
136
|
| `.claude/rules/rails-models.md` | Model listing with associations | Auto-loaded by Claude Code alongside CLAUDE.md. |
|
|
137
137
|
| `.claude/rules/rails-mcp-tools.md` | Full MCP tool reference | Parameters, detail levels, pagination, workflow guide. |
|
|
138
138
|
|
|
139
|
+
### OpenCode (3 files)
|
|
140
|
+
|
|
141
|
+
| File | Purpose | Notes |
|
|
142
|
+
|------|---------|-------|
|
|
143
|
+
| `AGENTS.md` | Main context file | Native OpenCode format. ≤150 lines in compact mode. OpenCode also reads CLAUDE.md as fallback. |
|
|
144
|
+
| `app/models/AGENTS.md` | Model reference | Auto-loaded by OpenCode when reading files in `app/models/`. |
|
|
145
|
+
| `app/controllers/AGENTS.md` | Controller reference | Auto-loaded by OpenCode when reading files in `app/controllers/`. |
|
|
146
|
+
|
|
139
147
|
### Cursor (5 files)
|
|
140
148
|
|
|
141
149
|
| File | Purpose | Notes |
|
|
@@ -181,9 +189,10 @@ Commit **all files except `.ai-context.json`** (which is gitignored). This gives
|
|
|
181
189
|
|
|
182
190
|
| Command | Mode | Format | Description |
|
|
183
191
|
|---------|------|--------|-------------|
|
|
184
|
-
| `rails ai:context` | compact | all | Generate all
|
|
192
|
+
| `rails ai:context` | compact | all | Generate all 18 context files |
|
|
185
193
|
| `rails ai:context:full` | full | all | Generate all files in full mode |
|
|
186
194
|
| `rails ai:context:claude` | compact | Claude | CLAUDE.md + .claude/rules/ |
|
|
195
|
+
| `rails ai:context:opencode` | compact | OpenCode | AGENTS.md + per-directory AGENTS.md |
|
|
187
196
|
| `rails ai:context:cursor` | compact | Cursor | .cursorrules + .cursor/rules/ |
|
|
188
197
|
| `rails ai:context:windsurf` | compact | Windsurf | .windsurfrules + .windsurf/rules/ |
|
|
189
198
|
| `rails ai:context:copilot` | compact | Copilot | copilot-instructions.md + .github/instructions/ |
|
|
@@ -204,7 +213,7 @@ Commit **all files except `.ai-context.json`** (which is gitignored). This gives
|
|
|
204
213
|
|
|
205
214
|
| Command | Description |
|
|
206
215
|
|---------|-------------|
|
|
207
|
-
| `rails ai:doctor` | Run
|
|
216
|
+
| `rails ai:doctor` | Run 13 diagnostic checks. Reports pass/warn/fail with fix suggestions. AI readiness score (0-100). |
|
|
208
217
|
| `rails ai:watch` | Watch for file changes and auto-regenerate context files. Requires `listen` gem. |
|
|
209
218
|
| `rails ai:inspect` | Print introspection summary to stdout. Useful for debugging. |
|
|
210
219
|
|
|
@@ -715,6 +724,31 @@ config.introspectors = %i[schema models routes gems auth api]
|
|
|
715
724
|
| `globs: ["app/models/**/*.rb"]` | When editing files matching the glob pattern |
|
|
716
725
|
| `alwaysApply: false` + `description` | When the AI decides it's relevant based on description |
|
|
717
726
|
|
|
727
|
+
### OpenCode
|
|
728
|
+
|
|
729
|
+
**MCP config:** Add to `opencode.json`:
|
|
730
|
+
|
|
731
|
+
```json
|
|
732
|
+
{
|
|
733
|
+
"mcp": {
|
|
734
|
+
"rails-ai-context": {
|
|
735
|
+
"type": "local",
|
|
736
|
+
"command": ["bundle", "exec", "rails", "ai:serve"]
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Context files loaded:**
|
|
743
|
+
- `AGENTS.md` — project overview + MCP tool guide, read at conversation start
|
|
744
|
+
- `app/models/AGENTS.md` — model listing, auto-loaded when agent reads model files
|
|
745
|
+
- `app/controllers/AGENTS.md` — controller listing, auto-loaded when agent reads controller files
|
|
746
|
+
- Falls back to `CLAUDE.md` if no `AGENTS.md` exists
|
|
747
|
+
|
|
748
|
+
OpenCode uses **per-directory lazy-loading**: when the agent reads a file, it walks up the directory tree and auto-loads any `AGENTS.md` it finds. This is how split rules work — no globs or frontmatter needed.
|
|
749
|
+
|
|
750
|
+
**MCP tools:** Available via `opencode.json` config above.
|
|
751
|
+
|
|
718
752
|
### Windsurf
|
|
719
753
|
|
|
720
754
|
**Context files loaded:**
|
|
@@ -116,6 +116,7 @@ module RailsAiContext
|
|
|
116
116
|
say ""
|
|
117
117
|
say "Generated files per AI tool:", :yellow
|
|
118
118
|
say " Claude Code → CLAUDE.md + .claude/rules/*.md"
|
|
119
|
+
say " OpenCode → AGENTS.md"
|
|
119
120
|
say " Cursor → .cursorrules + .cursor/rules/*.mdc"
|
|
120
121
|
say " Windsurf → .windsurfrules + .windsurf/rules/*.md"
|
|
121
122
|
say " GitHub Copilot → .github/copilot-instructions.md + .github/instructions/*.instructions.md"
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
module Serializers
|
|
5
5
|
# Orchestrates writing context files to disk in various formats.
|
|
6
|
-
# Supports: CLAUDE.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, JSON
|
|
6
|
+
# Supports: CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, JSON
|
|
7
7
|
# Also generates split rule files for AI tools that support them.
|
|
8
8
|
class ContextFileSerializer
|
|
9
9
|
attr_reader :context, :format
|
|
10
10
|
|
|
11
11
|
FORMAT_MAP = {
|
|
12
12
|
claude: "CLAUDE.md",
|
|
13
|
+
opencode: "AGENTS.md",
|
|
13
14
|
cursor: ".cursorrules",
|
|
14
15
|
windsurf: ".windsurfrules",
|
|
15
16
|
copilot: ".github/copilot-instructions.md",
|
|
@@ -63,6 +64,7 @@ module RailsAiContext
|
|
|
63
64
|
case fmt
|
|
64
65
|
when :json then JsonSerializer.new(context).call
|
|
65
66
|
when :claude then ClaudeSerializer.new(context).call
|
|
67
|
+
when :opencode then OpencodeSerializer.new(context).call
|
|
66
68
|
when :cursor then RulesSerializer.new(context).call
|
|
67
69
|
when :windsurf then WindsurfSerializer.new(context).call
|
|
68
70
|
when :copilot then CopilotSerializer.new(context).call
|
|
@@ -89,6 +91,12 @@ module RailsAiContext
|
|
|
89
91
|
skipped.concat(result[:skipped])
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
if formats.include?(:opencode)
|
|
95
|
+
result = OpencodeRulesSerializer.new(context).call(output_dir)
|
|
96
|
+
written.concat(result[:written])
|
|
97
|
+
skipped.concat(result[:skipped])
|
|
98
|
+
end
|
|
99
|
+
|
|
92
100
|
if formats.include?(:copilot)
|
|
93
101
|
result = CopilotInstructionsSerializer.new(context).call(output_dir)
|
|
94
102
|
written.concat(result[:written])
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Serializers
|
|
5
|
+
# Generates per-directory AGENTS.md files for OpenCode's lazy-loading system.
|
|
6
|
+
# When OpenCode's agent reads a file, it walks up the directory tree and
|
|
7
|
+
# auto-loads any AGENTS.md it finds — acting as contextual split rules.
|
|
8
|
+
#
|
|
9
|
+
# Generated files:
|
|
10
|
+
# app/models/AGENTS.md — model listing, loaded when editing models
|
|
11
|
+
# app/controllers/AGENTS.md — controller listing, loaded when editing controllers
|
|
12
|
+
class OpencodeRulesSerializer
|
|
13
|
+
attr_reader :context
|
|
14
|
+
|
|
15
|
+
def initialize(context)
|
|
16
|
+
@context = context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param output_dir [String] Rails root path
|
|
20
|
+
# @return [Hash] { written: [paths], skipped: [paths] }
|
|
21
|
+
def call(output_dir)
|
|
22
|
+
written = []
|
|
23
|
+
skipped = []
|
|
24
|
+
|
|
25
|
+
files = {
|
|
26
|
+
File.join("app", "models", "AGENTS.md") => render_models_reference,
|
|
27
|
+
File.join("app", "controllers", "AGENTS.md") => render_controllers_reference
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
files.each do |relative_path, content|
|
|
31
|
+
next unless content
|
|
32
|
+
|
|
33
|
+
filepath = File.join(output_dir, relative_path)
|
|
34
|
+
dir = File.dirname(filepath)
|
|
35
|
+
next unless Dir.exist?(dir)
|
|
36
|
+
|
|
37
|
+
if File.exist?(filepath) && File.read(filepath) == content
|
|
38
|
+
skipped << filepath
|
|
39
|
+
else
|
|
40
|
+
File.write(filepath, content)
|
|
41
|
+
written << filepath
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
{ written: written, skipped: skipped }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def render_models_reference
|
|
51
|
+
models = context[:models]
|
|
52
|
+
return nil unless models.is_a?(Hash) && !models[:error] && models.any?
|
|
53
|
+
|
|
54
|
+
lines = [
|
|
55
|
+
"# ActiveRecord Models (#{models.size})",
|
|
56
|
+
"",
|
|
57
|
+
"> Auto-generated by rails-ai-context. Do not edit manually.",
|
|
58
|
+
"> Use `rails_get_model_details` MCP tool for full details.",
|
|
59
|
+
""
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
models.keys.sort.first(30).each do |name|
|
|
63
|
+
data = models[name]
|
|
64
|
+
assocs = (data[:associations] || []).first(3).map { |a| "#{a[:type]} :#{a[:name]}" }.join(", ")
|
|
65
|
+
vals = (data[:validations] || []).size
|
|
66
|
+
line = "- **#{name}**"
|
|
67
|
+
line += " (table: #{data[:table_name]})" if data[:table_name]
|
|
68
|
+
line += " — #{assocs}" unless assocs.empty?
|
|
69
|
+
line += " [#{vals}v]" if vals > 0
|
|
70
|
+
lines << line
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
lines << "- _...#{models.size - 30} more_" if models.size > 30
|
|
74
|
+
lines << ""
|
|
75
|
+
lines << "Use `rails_get_model_details(model:\"Name\")` for associations, validations, scopes, enums."
|
|
76
|
+
|
|
77
|
+
lines.join("\n")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_controllers_reference
|
|
81
|
+
data = context[:controllers]
|
|
82
|
+
return nil unless data.is_a?(Hash) && !data[:error]
|
|
83
|
+
controllers = data[:controllers] || {}
|
|
84
|
+
return nil if controllers.empty?
|
|
85
|
+
|
|
86
|
+
lines = [
|
|
87
|
+
"# Controllers (#{controllers.size})",
|
|
88
|
+
"",
|
|
89
|
+
"> Auto-generated by rails-ai-context. Do not edit manually.",
|
|
90
|
+
"> Use `rails_get_controllers` MCP tool for full details.",
|
|
91
|
+
""
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
controllers.keys.sort.first(25).each do |name|
|
|
95
|
+
info = controllers[name]
|
|
96
|
+
actions = (info[:actions] || []).map { |a| a.is_a?(Hash) ? a[:name] : a }.compact.first(6)
|
|
97
|
+
line = "- **#{name}**"
|
|
98
|
+
line += " — #{actions.join(", ")}" unless actions.empty?
|
|
99
|
+
lines << line
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
lines << "- _...#{controllers.size - 25} more_" if controllers.size > 25
|
|
103
|
+
lines << ""
|
|
104
|
+
lines << "Use `rails_get_controllers(controller:\"Name\")` for actions, filters, strong params."
|
|
105
|
+
|
|
106
|
+
lines.join("\n")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Serializers
|
|
5
|
+
# Generates AGENTS.md optimized for OpenCode.
|
|
6
|
+
# In :compact mode (default), produces ≤150 lines with MCP tool references.
|
|
7
|
+
# In :full mode, delegates to MarkdownSerializer with OpenCode header.
|
|
8
|
+
class OpencodeSerializer
|
|
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
|
+
FullOpencodeSerializer.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 # rubocop:disable Metrics/MethodLength
|
|
162
|
+
[
|
|
163
|
+
"## MCP Tool Reference",
|
|
164
|
+
"",
|
|
165
|
+
"This project exposes live MCP tools. **Always start with `detail:\"summary\"`**,",
|
|
166
|
+
"then drill into specifics with a filter or `detail:\"full\"`.",
|
|
167
|
+
"",
|
|
168
|
+
"### Detail levels (schema, routes, models, controllers)",
|
|
169
|
+
"",
|
|
170
|
+
"| Level | Returns | Default limit |",
|
|
171
|
+
"|-------|---------|---------------|",
|
|
172
|
+
"| `summary` | Names + counts | 50 |",
|
|
173
|
+
"| `standard` | Names + key details | 15 (default) |",
|
|
174
|
+
"| `full` | Everything (indexes, FKs, etc.) | 5 |",
|
|
175
|
+
"",
|
|
176
|
+
"### rails_get_schema",
|
|
177
|
+
"Params: `table`, `detail`, `limit`, `offset`, `format`",
|
|
178
|
+
"- `rails_get_schema(detail:\"summary\")` — all tables with column counts",
|
|
179
|
+
"- `rails_get_schema(table:\"users\")` — full detail for one table",
|
|
180
|
+
"- `rails_get_schema(detail:\"summary\", limit:20, offset:40)` — paginate",
|
|
181
|
+
"",
|
|
182
|
+
"### rails_get_model_details",
|
|
183
|
+
"Params: `model`, `detail`",
|
|
184
|
+
"- `rails_get_model_details(detail:\"summary\")` — list all model names",
|
|
185
|
+
"- `rails_get_model_details(model:\"User\")` — associations, validations, scopes, enums, callbacks",
|
|
186
|
+
"- `rails_get_model_details(detail:\"full\")` — all models with full association lists",
|
|
187
|
+
"",
|
|
188
|
+
"### rails_get_routes",
|
|
189
|
+
"Params: `controller`, `detail`, `limit`, `offset`",
|
|
190
|
+
"- `rails_get_routes(detail:\"summary\")` — route counts per controller",
|
|
191
|
+
"- `rails_get_routes(controller:\"users\")` — routes for one controller",
|
|
192
|
+
"- `rails_get_routes(detail:\"full\", limit:50)` — full table with route names",
|
|
193
|
+
"",
|
|
194
|
+
"### rails_get_controllers",
|
|
195
|
+
"Params: `controller`, `detail`",
|
|
196
|
+
"- `rails_get_controllers(detail:\"summary\")` — names + action counts",
|
|
197
|
+
"- `rails_get_controllers(controller:\"UsersController\")` — actions, filters, strong params",
|
|
198
|
+
"",
|
|
199
|
+
"### Other tools (no detail param)",
|
|
200
|
+
"- `rails_get_config` — cache store, session, timezone, middleware, initializers",
|
|
201
|
+
"- `rails_get_test_info` — test framework, factories/fixtures, CI config, coverage",
|
|
202
|
+
"- `rails_get_gems` — notable gems categorized by function (auth, background jobs, etc.)",
|
|
203
|
+
"- `rails_get_conventions` — architecture patterns, directory structure, config files",
|
|
204
|
+
"- `rails_search_code(pattern:\"regex\", file_type:\"rb\", max_results:20)` — ripgrep search",
|
|
205
|
+
""
|
|
206
|
+
]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def render_conventions
|
|
210
|
+
conv = context[:conventions]
|
|
211
|
+
return [] unless conv.is_a?(Hash) && !conv[:error]
|
|
212
|
+
|
|
213
|
+
config_files = conv[:config_files] || []
|
|
214
|
+
return [] if config_files.empty?
|
|
215
|
+
|
|
216
|
+
lines = [ "## Key config files" ]
|
|
217
|
+
config_files.first(5).each { |f| lines << "- `#{f}`" }
|
|
218
|
+
lines << ""
|
|
219
|
+
lines
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def render_commands
|
|
223
|
+
[
|
|
224
|
+
"## Commands",
|
|
225
|
+
"- `bin/dev` — start dev server",
|
|
226
|
+
"- `bundle exec rspec` — run tests",
|
|
227
|
+
"- `rails db:migrate` — run pending migrations",
|
|
228
|
+
""
|
|
229
|
+
]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def render_footer
|
|
233
|
+
[
|
|
234
|
+
"## Rules",
|
|
235
|
+
"- Follow existing patterns and conventions",
|
|
236
|
+
"- Use the MCP tools to check schema before writing migrations",
|
|
237
|
+
"- Match existing code style",
|
|
238
|
+
"- Run tests after changes",
|
|
239
|
+
""
|
|
240
|
+
]
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Internal: full-mode OpenCode serializer (wraps MarkdownSerializer)
|
|
245
|
+
class FullOpencodeSerializer < MarkdownSerializer
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
def header
|
|
249
|
+
<<~MD
|
|
250
|
+
# #{context[:app_name]} — AI Context
|
|
251
|
+
|
|
252
|
+
> Auto-generated by rails-ai-context v#{RailsAiContext::VERSION}
|
|
253
|
+
> Generated: #{context[:generated_at]}
|
|
254
|
+
> Rails #{context[:rails_version]} | Ruby #{context[:ruby_version]}
|
|
255
|
+
|
|
256
|
+
This file gives OpenCode deep context about this Rails application's
|
|
257
|
+
structure, patterns, and conventions.
|
|
258
|
+
MD
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def footer
|
|
262
|
+
rules = []
|
|
263
|
+
rules << "## Behavioral Rules"
|
|
264
|
+
rules << ""
|
|
265
|
+
rules << "When working in this codebase:"
|
|
266
|
+
rules << "- Follow existing patterns and conventions detected above"
|
|
267
|
+
rules << "- Use the database schema as the source of truth for column names and types"
|
|
268
|
+
rules << "- Respect existing associations and validations when modifying models"
|
|
269
|
+
rules << "- Match the project's architecture style (#{architecture_summary})" if architecture_summary
|
|
270
|
+
rules << "- Run `bundle exec rspec` after making changes to verify correctness"
|
|
271
|
+
rules << ""
|
|
272
|
+
rules << super
|
|
273
|
+
rules.join("\n")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def architecture_summary
|
|
277
|
+
arch = context.dig(:conventions, :architecture)
|
|
278
|
+
arch&.any? ? arch.join(", ") : nil
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -76,12 +76,14 @@ module RailsAiContext
|
|
|
76
76
|
$stderr.puts "[rails-ai-context] Tools: #{TOOLS.map { |t| t.tool_name }.join(', ')}"
|
|
77
77
|
maybe_start_live_reload(server)
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
begin
|
|
80
|
+
require "rackup"
|
|
81
|
+
Rackup::Handler.default.run(rack_app, Host: config.http_bind, Port: config.http_port)
|
|
82
|
+
rescue LoadError
|
|
83
|
+
# Fallback for older rack without rackup gem
|
|
84
|
+
require "rack/handler"
|
|
85
|
+
Rack::Handler.default.run(rack_app, Host: config.http_bind, Port: config.http_port)
|
|
86
|
+
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
# Conditionally start live reload based on configuration.
|
|
@@ -94,9 +96,10 @@ module RailsAiContext
|
|
|
94
96
|
return if mode == false
|
|
95
97
|
|
|
96
98
|
begin
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
live_reload = LiveReload.new(app, mcp_server)
|
|
100
|
+
live_reload.start
|
|
101
|
+
@live_reload = live_reload
|
|
102
|
+
rescue LoadError
|
|
100
103
|
if mode == true
|
|
101
104
|
raise LoadError, "Live reload requires the `listen` gem. Add to your Gemfile: gem 'listen', group: :development"
|
|
102
105
|
end
|
|
@@ -4,6 +4,7 @@ ASSISTANT_TABLE = <<~TABLE
|
|
|
4
4
|
AI Assistant Context File Command
|
|
5
5
|
-- -- --
|
|
6
6
|
Claude Code CLAUDE.md + .claude/rules/ rails ai:context:claude
|
|
7
|
+
OpenCode AGENTS.md rails ai:context:opencode
|
|
7
8
|
Cursor .cursorrules + .cursor/rules/ rails ai:context:cursor
|
|
8
9
|
Windsurf .windsurfrules + .windsurf/rules/ rails ai:context:windsurf
|
|
9
10
|
GitHub Copilot .github/copilot-instructions.md rails ai:context:copilot
|
|
@@ -59,7 +60,7 @@ namespace :ai do
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
namespace :context do
|
|
62
|
-
{ claude: "CLAUDE.md", cursor: ".cursorrules", windsurf: ".windsurfrules",
|
|
63
|
+
{ claude: "CLAUDE.md", opencode: "AGENTS.md", cursor: ".cursorrules", windsurf: ".windsurfrules",
|
|
63
64
|
copilot: ".github/copilot-instructions.md", json: ".ai-context.json" }.each do |fmt, file|
|
|
64
65
|
desc "Generate #{file} context file"
|
|
65
66
|
task fmt => :environment do
|
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.8.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -237,6 +237,8 @@ files:
|
|
|
237
237
|
- lib/rails_ai_context/serializers/cursor_rules_serializer.rb
|
|
238
238
|
- lib/rails_ai_context/serializers/json_serializer.rb
|
|
239
239
|
- lib/rails_ai_context/serializers/markdown_serializer.rb
|
|
240
|
+
- lib/rails_ai_context/serializers/opencode_rules_serializer.rb
|
|
241
|
+
- lib/rails_ai_context/serializers/opencode_serializer.rb
|
|
240
242
|
- lib/rails_ai_context/serializers/rules_serializer.rb
|
|
241
243
|
- lib/rails_ai_context/serializers/windsurf_rules_serializer.rb
|
|
242
244
|
- lib/rails_ai_context/serializers/windsurf_serializer.rb
|