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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e6efb0468b245ebcc4945086d0091ccdf7a5df5395af4cfde7a928ec2861c93
4
- data.tar.gz: f956c81dc980e2622e04d6957ad59c054e33f416f5e37eb11c8d4466c255fa00
3
+ metadata.gz: 04212baad7453e8371662a226fa50dfa4b65e4b611e94af22c50ab09e314d888
4
+ data.tar.gz: 43140d9b9f6151a4f5e72427efd8fcba0ed4a8256ed3f3c56f6a1c1ff53fd436
5
5
  SHA512:
6
- metadata.gz: 4b2eeb539cff753960b5f6718c7d3e7ec476dd56440785369ed7af6d40af9aa20ea4d213ed09e1d3cfa7f94e244f7a9a15ad57385fb6ddc88e1a338bc1aedbfe
7
- data.tar.gz: '099e4dee7daefb3bddad34d9b10c3a25c8d6ff906b797f1acd35f5ae17ae169b89cf954b0cc73d7ae7fc557af6e3a3c08535d2d348ce364c08647050ff9eb19b'
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 (348 examples)
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 # 350 examples
355
+ bundle exec rspec # 373 examples
351
356
  bundle exec rubocop # Lint
352
357
  ```
353
358
 
data/SECURITY.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---------|--------------------|
7
+ | 0.8.x | :white_check_mark: |
7
8
  | 0.7.x | :white_check_mark: |
8
- | 0.6.x | :white_check_mark: |
9
- | < 0.6 | :x: |
9
+ | < 0.7 | :x: |
10
10
 
11
11
  ## Reporting a Vulnerability
12
12
 
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.7.1'
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 **17 files** across all AI assistants:
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 17 context files |
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 12 diagnostic checks. Reports pass/warn/fail with fix suggestions. AI readiness score (0-100). |
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
- require "rackup"
80
- Rackup::Handler.default.run(rack_app, Host: config.http_bind, Port: config.http_port)
81
- rescue LoadError
82
- # Fallback for older rack without rackup gem
83
- require "rack/handler"
84
- Rack::Handler.default.run(rack_app, Host: config.http_bind, Port: config.http_port)
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
- @live_reload = LiveReload.new(app, mcp_server)
98
- @live_reload.start
99
- rescue LoadError => e
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  end
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.0
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