rails-ai-context 4.4.0 → 4.5.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 +1 -1
- data/CLAUDE.md +6 -2
- data/README.md +29 -12
- data/docs/GUIDE.md +56 -18
- data/exe/rails-ai-context +311 -39
- data/lib/generators/rails_ai_context/install/install_generator.rb +21 -3
- data/lib/rails_ai_context/configuration.rb +66 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +140 -6
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +5 -0
- data/server.json +3 -3
- metadata +13 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41d06a29e3165804283c9dcef76e2c1ee5625ef5ac236931fa492d76172062b6
|
|
4
|
+
data.tar.gz: 8f2f81c3186b2d16fa9c4ea2b84721fd519d437b4cbd8fb99a3978e45723a189
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94ee0d646375d50942d8d3ea186a3f5faea8795f1fee1588f63304a06ab283db79e8dd07a1460284279b9edeac9bc3604e9208b847dda6ef144f9d5e68c5df2b
|
|
7
|
+
data.tar.gz: 838c87226b6027f62aa045c3eebd2842f82434db0b6a9fa2f20234bde75a9e0f4d6407fe4d510e129c0e192141b15a75fec5c9d0d3620a2991c2860db7ad5e42
|
data/CHANGELOG.md
CHANGED
|
@@ -386,7 +386,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
386
386
|
- **Phase 1 improvements** — scope definitions include lambda body, controller actions show instance variables + private methods called inline, Stimulus shows HTML data-attributes + reverse view lookup.
|
|
387
387
|
- **3 new validation rules** — instance variable consistency (view uses @foo but controller never sets it), Turbo Stream channel matching (broadcast without subscriber), respond_to template existence.
|
|
388
388
|
- **`rails_security_scan` tool** — Brakeman static security analysis via MCP. Detects SQL injection, XSS, mass assignment, and more. Optional dependency — returns install instructions if Brakeman isn't present. Supports file filtering, confidence levels (high/medium/weak), specific check selection, and three detail levels (summary/standard/full).
|
|
389
|
-
- **`config.skip_tools`** — users can now exclude specific built-in tools: `config.skip_tools = %w[rails_security_scan]`. Defaults to empty (all
|
|
389
|
+
- **`config.skip_tools`** — users can now exclude specific built-in tools: `config.skip_tools = %w[rails_security_scan]`. Defaults to empty (all 39 tools active).
|
|
390
390
|
- **Schema index hints** — `get_schema` standard detail now shows `[indexed]`/`[unique]` on columns, saving a round-trip to full detail.
|
|
391
391
|
- **Enum backing types** — `get_model_details` now shows integer vs string backing: `status: pending(0), active(1) [integer]`.
|
|
392
392
|
- **Search context lines default 2** — `search_code` now returns 2 lines of context by default (was 0). Eliminates follow-up calls for context.
|
data/CLAUDE.md
CHANGED
|
@@ -21,12 +21,12 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
21
21
|
- `lib/rails_ai_context/watcher.rb` — File watcher for auto-regenerating context files
|
|
22
22
|
- `lib/rails_ai_context/engine.rb` — Rails Engine for auto-integration
|
|
23
23
|
- `lib/generators/rails_ai_context/install/` — Install generator (creates .mcp.json, initializer, context files)
|
|
24
|
-
- `exe/rails-ai-context` — Standalone Thor CLI (serve, context, inspect, watch, doctor, tool, version)
|
|
24
|
+
- `exe/rails-ai-context` — Standalone Thor CLI (init, serve, context, inspect, watch, doctor, tool, version) — works without Gemfile entry via `gem install`
|
|
25
25
|
|
|
26
26
|
## Key Design Decisions
|
|
27
27
|
|
|
28
28
|
1. **Built on official mcp SDK** — not a custom protocol implementation
|
|
29
|
-
2. **
|
|
29
|
+
2. **Sensible defaults** — works standalone (`gem install` + `rails-ai-context init`) or in-Gemfile with Railtie auto-registration
|
|
30
30
|
3. **Graceful degradation** — works without DB by parsing schema.rb as text
|
|
31
31
|
4. **Read-only tools only** — all MCP tools are annotated as non-destructive
|
|
32
32
|
5. **Sensitive pattern blocking** — search/read tools reject `.env`, `*.key`, `*.pem` and other secret files via `sensitive_patterns`
|
|
@@ -58,6 +58,10 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
58
58
|
31. **Log reading** — reverse file tail with level filtering and sensitive data redaction
|
|
59
59
|
32. **Config validation** — `http_port`, `cache_ttl`, `max_tool_response_chars`, `query_row_limit` validated on assignment with clear error messages
|
|
60
60
|
33. **Sensitive column redaction** — query tool redacts columns by name AND by suffix pattern (password, secret, token, key, digest, hash) to prevent alias bypass
|
|
61
|
+
34. **Standalone mode** — `gem install rails-ai-context && rails-ai-context init` works without Gemfile entry. CLI pre-loads gem before Rails boot, restores `$LOAD_PATH` entries stripped by `Bundler.setup`. Config from `.rails-ai-context.yml`.
|
|
62
|
+
35. **YAML config** — `.rails-ai-context.yml` as alternative to initializer. Supports all config options except `custom_tools` (Ruby classes) and `excluded_concerns` (regex). Precedence: initializer > YAML > defaults.
|
|
63
|
+
36. **Config auto-loading** — `Configuration.auto_load!` checks `configured_via_block?` flag. If initializer ran, YAML is skipped. Corrupted YAML degrades gracefully with a warning.
|
|
64
|
+
37. **Three install paths** — In-Gemfile (`rails generate rails_ai_context:install`), Standalone (`rails-ai-context init`), Zero config (just run `rails-ai-context serve` with defaults). Users can switch between paths freely; `.mcp.json` command is updated on re-init/re-install.
|
|
61
65
|
|
|
62
66
|
## Testing
|
|
63
67
|
|
data/README.md
CHANGED
|
@@ -51,6 +51,15 @@ gem "rails-ai-context", group: :development
|
|
|
51
51
|
rails generate rails_ai_context:install
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
### Or standalone — no Gemfile needed
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
gem install rails-ai-context
|
|
58
|
+
cd your-rails-app
|
|
59
|
+
rails-ai-context init # interactive setup
|
|
60
|
+
rails-ai-context serve # start MCP server
|
|
61
|
+
```
|
|
62
|
+
|
|
54
63
|
<div align="center">
|
|
55
64
|
|
|
56
65
|

|
|
@@ -341,7 +350,7 @@ Every tool is **read-only** and returns structured, token-efficient context.
|
|
|
341
350
|
▼
|
|
342
351
|
┌─────────────────────────────────────────────────────────┐
|
|
343
352
|
│ rails-ai-context │
|
|
344
|
-
│ Parses everything. Caches results.
|
|
353
|
+
│ Parses everything. Caches results. Sensible defaults. │
|
|
345
354
|
└────────┬──────────────────┬──────────────┬──────────────┘
|
|
346
355
|
│ │ │
|
|
347
356
|
▼ ▼ ▼
|
|
@@ -357,27 +366,35 @@ Every tool is **read-only** and returns structured, token-efficient context.
|
|
|
357
366
|
|
|
358
367
|
## Install
|
|
359
368
|
|
|
369
|
+
**Option A — In Gemfile:**
|
|
370
|
+
|
|
360
371
|
```bash
|
|
361
372
|
gem "rails-ai-context", group: :development
|
|
362
373
|
rails generate rails_ai_context:install
|
|
363
374
|
```
|
|
364
375
|
|
|
365
|
-
|
|
376
|
+
**Option B — Standalone (no Gemfile entry needed):**
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
gem install rails-ai-context
|
|
380
|
+
cd your-rails-app
|
|
381
|
+
rails-ai-context init
|
|
382
|
+
```
|
|
366
383
|
|
|
367
|
-
`.mcp.json` is auto-detected by Claude Code and Cursor
|
|
384
|
+
Both paths ask which AI tools you use and whether you want MCP or CLI mode. `.mcp.json` is auto-detected by Claude Code and Cursor.
|
|
368
385
|
|
|
369
386
|
<br>
|
|
370
387
|
|
|
371
388
|
## Commands
|
|
372
389
|
|
|
373
|
-
|
|
|
374
|
-
|
|
375
|
-
| `rails ai:context` |
|
|
376
|
-
| `rails 'ai:tool[NAME]'` | Run any of the 39 tools
|
|
377
|
-
| `rails ai:tool` | List all available tools |
|
|
378
|
-
| `rails ai:serve` | Start MCP server (stdio) |
|
|
379
|
-
| `rails ai:doctor` | Diagnostics + AI readiness score |
|
|
380
|
-
| `rails ai:watch` | Auto-regenerate on file changes |
|
|
390
|
+
| In-Gemfile | Standalone | What it does |
|
|
391
|
+
|:-----------|:-----------|:------------|
|
|
392
|
+
| `rails ai:context` | `rails-ai-context context` | Generate context files |
|
|
393
|
+
| `rails 'ai:tool[NAME]'` | `rails-ai-context tool NAME` | Run any of the 39 tools |
|
|
394
|
+
| `rails ai:tool` | `rails-ai-context tool --list` | List all available tools |
|
|
395
|
+
| `rails ai:serve` | `rails-ai-context serve` | Start MCP server (stdio) |
|
|
396
|
+
| `rails ai:doctor` | `rails-ai-context doctor` | Diagnostics + AI readiness score |
|
|
397
|
+
| `rails ai:watch` | `rails-ai-context watch` | Auto-regenerate on file changes |
|
|
381
398
|
|
|
382
399
|
<br>
|
|
383
400
|
|
|
@@ -427,7 +444,7 @@ end
|
|
|
427
444
|
## About
|
|
428
445
|
|
|
429
446
|
Built by a Rails developer with 10+ years of production experience.<br>
|
|
430
|
-
1529 tests. 39 tools. 33 introspectors.
|
|
447
|
+
1529 tests. 39 tools. 33 introspectors. Standalone or in-Gemfile.<br>
|
|
431
448
|
MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
|
|
432
449
|
|
|
433
450
|
<br>
|
data/docs/GUIDE.md
CHANGED
|
@@ -29,35 +29,35 @@
|
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
32
|
-
###
|
|
32
|
+
### Option A: In Gemfile
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
35
|
+
gem "rails-ai-context", group: :development
|
|
36
|
+
bundle install
|
|
36
37
|
rails generate rails_ai_context:install
|
|
37
38
|
rails ai:context
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
This creates:
|
|
41
42
|
1. `config/initializers/rails_ai_context.rb` — configuration file
|
|
42
|
-
2. `.
|
|
43
|
-
3.
|
|
43
|
+
2. `.rails-ai-context.yml` — standalone config (enables switching later)
|
|
44
|
+
3. `.mcp.json` — MCP auto-discovery for Claude Code and Cursor
|
|
45
|
+
4. Context files — tailored for each AI assistant
|
|
44
46
|
|
|
45
|
-
###
|
|
47
|
+
### Option B: Standalone (no Gemfile entry needed)
|
|
46
48
|
|
|
47
49
|
```bash
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
bundle install
|
|
53
|
-
rails generate rails_ai_context:install
|
|
50
|
+
gem install rails-ai-context
|
|
51
|
+
cd your-rails-app
|
|
52
|
+
rails-ai-context init
|
|
53
|
+
```
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
rails
|
|
55
|
+
This creates:
|
|
56
|
+
1. `.rails-ai-context.yml` — configuration file
|
|
57
|
+
2. `.mcp.json` — MCP auto-discovery (if MCP mode selected)
|
|
58
|
+
3. Context files — tailored for each AI assistant
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
rails ai:doctor
|
|
60
|
-
```
|
|
60
|
+
No Gemfile entry, no initializer, no files in your project besides config and context.
|
|
61
61
|
|
|
62
62
|
### What the install generator does
|
|
63
63
|
|
|
@@ -220,9 +220,10 @@ Commit **all files except `.ai-context.json`** (which is gitignored). This gives
|
|
|
220
220
|
|
|
221
221
|
### Standalone CLI
|
|
222
222
|
|
|
223
|
-
The gem ships a `rails-ai-context` executable
|
|
223
|
+
The gem ships a `rails-ai-context` executable that works **without adding the gem to your Gemfile**. Install globally with `gem install rails-ai-context`, then run from any Rails app directory.
|
|
224
224
|
|
|
225
225
|
```bash
|
|
226
|
+
rails-ai-context init # Interactive setup (creates .rails-ai-context.yml + .mcp.json)
|
|
226
227
|
rails-ai-context serve # Start MCP server (stdio)
|
|
227
228
|
rails-ai-context serve --transport http # Start MCP server (HTTP, port 6029)
|
|
228
229
|
rails-ai-context serve --transport http --port 8080 # Custom port
|
|
@@ -241,6 +242,8 @@ rails-ai-context help # Show all commands
|
|
|
241
242
|
|
|
242
243
|
Must be run from your Rails app root directory (requires `config/environment.rb`).
|
|
243
244
|
|
|
245
|
+
**Config:** Standalone mode reads from `.rails-ai-context.yml` (created by `init`). If no config file exists, defaults are used. If the gem is also in the Gemfile, the initializer takes precedence over the YAML file.
|
|
246
|
+
|
|
244
247
|
### Legacy command
|
|
245
248
|
|
|
246
249
|
```bash
|
|
@@ -1042,8 +1045,9 @@ curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=rails-ai-cont
|
|
|
1042
1045
|
|
|
1043
1046
|
### Auto-discovery (recommended)
|
|
1044
1047
|
|
|
1045
|
-
The install generator creates `.mcp.json` in your project root:
|
|
1048
|
+
The install generator (or `rails-ai-context init`) creates `.mcp.json` in your project root:
|
|
1046
1049
|
|
|
1050
|
+
**In-Gemfile:**
|
|
1047
1051
|
```json
|
|
1048
1052
|
{
|
|
1049
1053
|
"mcpServers": {
|
|
@@ -1055,6 +1059,18 @@ The install generator creates `.mcp.json` in your project root:
|
|
|
1055
1059
|
}
|
|
1056
1060
|
```
|
|
1057
1061
|
|
|
1062
|
+
**Standalone:**
|
|
1063
|
+
```json
|
|
1064
|
+
{
|
|
1065
|
+
"mcpServers": {
|
|
1066
|
+
"rails-ai-context": {
|
|
1067
|
+
"command": "rails-ai-context",
|
|
1068
|
+
"args": ["serve"]
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1058
1074
|
**Claude Code** and **Cursor** auto-detect this file. No manual config needed — just open your project.
|
|
1059
1075
|
|
|
1060
1076
|
### Claude Code
|
|
@@ -1062,7 +1078,11 @@ The install generator creates `.mcp.json` in your project root:
|
|
|
1062
1078
|
Auto-discovered via `.mcp.json`. Or add manually:
|
|
1063
1079
|
|
|
1064
1080
|
```bash
|
|
1081
|
+
# In-Gemfile
|
|
1065
1082
|
claude mcp add rails-ai-context -- bundle exec rails ai:serve
|
|
1083
|
+
|
|
1084
|
+
# Standalone
|
|
1085
|
+
claude mcp add rails-ai-context -- rails-ai-context serve
|
|
1066
1086
|
```
|
|
1067
1087
|
|
|
1068
1088
|
### Claude Desktop
|
|
@@ -1081,6 +1101,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
1081
1101
|
}
|
|
1082
1102
|
```
|
|
1083
1103
|
|
|
1104
|
+
Or for standalone: replace `"command": "bundle"` / `"args": ["exec", "rails", "ai:serve"]` with `"command": "rails-ai-context"` / `"args": ["serve"]`.
|
|
1105
|
+
|
|
1084
1106
|
### Cursor
|
|
1085
1107
|
|
|
1086
1108
|
Auto-discovered via `.mcp.json`. Or add manually in **Cursor Settings > MCP**:
|
|
@@ -1097,6 +1119,9 @@ Auto-discovered via `.mcp.json`. Or add manually in **Cursor Settings > MCP**:
|
|
|
1097
1119
|
}
|
|
1098
1120
|
```
|
|
1099
1121
|
|
|
1122
|
+
For standalone: use `"command": "rails-ai-context"` / `"args": ["serve"]` instead.
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1100
1125
|
### HTTP transport
|
|
1101
1126
|
|
|
1102
1127
|
For browser-based or remote AI clients:
|
|
@@ -1407,6 +1432,19 @@ config.introspectors = %i[schema models routes gems auth api]
|
|
|
1407
1432
|
}
|
|
1408
1433
|
```
|
|
1409
1434
|
|
|
1435
|
+
For standalone: use `"command": ["rails-ai-context", "serve"]` instead.
|
|
1436
|
+
|
|
1437
|
+
```json
|
|
1438
|
+
{
|
|
1439
|
+
"mcp": {
|
|
1440
|
+
"rails-ai-context": {
|
|
1441
|
+
"type": "local",
|
|
1442
|
+
"command": ["rails-ai-context", "serve"]
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1410
1448
|
**Context files loaded:**
|
|
1411
1449
|
- `AGENTS.md` — project overview + MCP tool guide, read at conversation start
|
|
1412
1450
|
- `app/models/AGENTS.md` — model listing, auto-loaded when agent reads model files
|
data/exe/rails-ai-context
CHANGED
|
@@ -2,43 +2,39 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "thor"
|
|
5
|
+
require "yaml"
|
|
5
6
|
|
|
6
|
-
module
|
|
7
|
-
|
|
7
|
+
# Top-level class to avoid conflict with RailsAiContext::CLI module (which contains ToolRunner).
|
|
8
|
+
# The library uses RailsAiContext::CLI as a module namespace; this exe needs a class for Thor.
|
|
9
|
+
class RailsAiContextCLI < Thor
|
|
8
10
|
# Let Thor pass unknown options through to ToolRunner for the tool command
|
|
9
11
|
stop_on_unknown_option! :tool
|
|
10
12
|
|
|
11
|
-
desc "tool [NAME]", "Run an MCP tool (
|
|
13
|
+
desc "tool [NAME]", "Run an MCP tool (39 available). Use --list to see all."
|
|
12
14
|
option :json, type: :boolean, default: false, desc: "Output as JSON envelope"
|
|
13
15
|
option :list, type: :boolean, default: false, desc: "List available tools"
|
|
14
16
|
def tool(name = nil, *args)
|
|
15
17
|
if options[:list] || name.nil?
|
|
16
18
|
boot_rails!
|
|
17
|
-
|
|
18
|
-
puts RailsAiContext::CLI::ToolRunner.tool_list
|
|
19
|
+
puts ::RailsAiContext::CLI::ToolRunner.tool_list
|
|
19
20
|
return
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
boot_rails!
|
|
23
|
-
require "rails_ai_context"
|
|
24
24
|
|
|
25
25
|
if args.include?("--help") || args.include?("-h")
|
|
26
|
-
tool_class = RailsAiContext::CLI::ToolRunner.new(name, []).tool_class
|
|
27
|
-
puts RailsAiContext::CLI::ToolRunner.tool_help(tool_class)
|
|
26
|
+
tool_class = ::RailsAiContext::CLI::ToolRunner.new(name, []).tool_class
|
|
27
|
+
puts ::RailsAiContext::CLI::ToolRunner.tool_help(tool_class)
|
|
28
28
|
return
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
runner = RailsAiContext::CLI::ToolRunner.new(name, args, json_mode: options[:json])
|
|
31
|
+
runner = ::RailsAiContext::CLI::ToolRunner.new(name, args, json_mode: options[:json])
|
|
32
32
|
puts runner.run
|
|
33
|
-
rescue
|
|
34
|
-
|
|
35
|
-
exit 1
|
|
36
|
-
rescue RailsAiContext::CLI::ToolRunner::InvalidArgumentError => e
|
|
37
|
-
$stderr.puts "Error: #{e.message}"
|
|
38
|
-
exit 3
|
|
33
|
+
rescue SystemExit
|
|
34
|
+
raise # let exit() propagate (from boot_rails!)
|
|
39
35
|
rescue => e
|
|
40
36
|
$stderr.puts "Error: #{e.message}"
|
|
41
|
-
exit
|
|
37
|
+
exit 1
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
desc "serve", "Start MCP server (stdio transport)"
|
|
@@ -46,53 +42,70 @@ module RailsAiContext
|
|
|
46
42
|
option :port, type: :numeric, default: 6029, desc: "HTTP port (only for http transport)"
|
|
47
43
|
def serve
|
|
48
44
|
boot_rails!
|
|
49
|
-
require "rails_ai_context"
|
|
50
45
|
|
|
51
46
|
transport = options[:transport].to_sym
|
|
52
47
|
if transport == :http
|
|
53
|
-
RailsAiContext.configuration.http_port = options[:port]
|
|
48
|
+
::RailsAiContext.configuration.http_port = options[:port]
|
|
54
49
|
end
|
|
55
50
|
|
|
56
|
-
RailsAiContext.start_mcp_server(transport: transport)
|
|
51
|
+
::RailsAiContext.start_mcp_server(transport: transport)
|
|
52
|
+
rescue SystemExit
|
|
53
|
+
raise
|
|
54
|
+
rescue => e
|
|
55
|
+
$stderr.puts "Error: #{e.message}"
|
|
56
|
+
exit 1
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
desc "context", "Generate AI context files"
|
|
60
|
-
option :format, type: :string, default:
|
|
60
|
+
option :format, type: :string, default: nil, desc: "Format: claude, cursor, copilot, json, all (default: from config)"
|
|
61
61
|
def context
|
|
62
62
|
boot_rails!
|
|
63
|
-
require "rails_ai_context"
|
|
64
63
|
|
|
65
|
-
format = options[:format].to_sym
|
|
66
64
|
$stderr.puts "Introspecting Rails app..."
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
|
|
66
|
+
if options[:format]
|
|
67
|
+
# Explicit --format flag overrides config
|
|
68
|
+
result = ::RailsAiContext.generate_context(format: options[:format].to_sym)
|
|
69
|
+
print_context_result(result)
|
|
70
|
+
else
|
|
71
|
+
# Use ai_tools from config (.rails-ai-context.yml or initializer)
|
|
72
|
+
ai_tools = ::RailsAiContext.configuration.ai_tools
|
|
73
|
+
if ai_tools.nil? || ai_tools.empty?
|
|
74
|
+
result = ::RailsAiContext.generate_context(format: :all)
|
|
75
|
+
print_context_result(result)
|
|
76
|
+
else
|
|
77
|
+
ai_tools.each do |fmt|
|
|
78
|
+
result = ::RailsAiContext.generate_context(format: fmt)
|
|
79
|
+
print_context_result(result)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
rescue SystemExit
|
|
84
|
+
raise
|
|
85
|
+
rescue => e
|
|
86
|
+
$stderr.puts "Error: #{e.message}"
|
|
87
|
+
exit 1
|
|
70
88
|
end
|
|
71
89
|
|
|
72
90
|
desc "inspect", "Print introspection summary"
|
|
73
91
|
def inspect_app
|
|
74
92
|
boot_rails!
|
|
75
|
-
require "rails_ai_context"
|
|
76
93
|
require "json"
|
|
77
94
|
|
|
78
|
-
context = RailsAiContext.introspect
|
|
95
|
+
context = ::RailsAiContext.introspect
|
|
79
96
|
puts JSON.pretty_generate(context)
|
|
80
97
|
end
|
|
81
98
|
|
|
82
99
|
desc "watch", "Watch for changes and auto-regenerate context files"
|
|
83
100
|
def watch
|
|
84
101
|
boot_rails!
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
RailsAiContext::Watcher.new.start
|
|
102
|
+
::RailsAiContext::Watcher.new.start
|
|
88
103
|
end
|
|
89
104
|
|
|
90
105
|
desc "doctor", "Run diagnostic checks and report AI readiness score"
|
|
91
106
|
def doctor
|
|
92
107
|
boot_rails!
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
result = RailsAiContext::Doctor.new.run
|
|
108
|
+
result = ::RailsAiContext::Doctor.new.run
|
|
96
109
|
result[:checks].each do |check|
|
|
97
110
|
icon = case check.status
|
|
98
111
|
when :pass then "PASS"
|
|
@@ -106,6 +119,49 @@ module RailsAiContext
|
|
|
106
119
|
$stderr.puts "AI Readiness Score: #{result[:score]}/100"
|
|
107
120
|
end
|
|
108
121
|
|
|
122
|
+
desc "init", "Set up rails-ai-context for standalone use (creates .rails-ai-context.yml and .mcp.json)"
|
|
123
|
+
def init
|
|
124
|
+
config_path = File.join(Dir.pwd, "config", "environment.rb")
|
|
125
|
+
unless File.exist?(config_path)
|
|
126
|
+
$stderr.puts "Error: No Rails app found in #{Dir.pwd}"
|
|
127
|
+
$stderr.puts "Run this command from your Rails app root directory."
|
|
128
|
+
exit 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# --- Read previous selection (before prompts) ---
|
|
132
|
+
previous_tools = read_previous_ai_tools
|
|
133
|
+
|
|
134
|
+
# --- Prompts (no Rails needed) ---
|
|
135
|
+
ai_tools = prompt_ai_tools
|
|
136
|
+
tool_mode = prompt_tool_mode
|
|
137
|
+
|
|
138
|
+
# --- Cleanup removed tools ---
|
|
139
|
+
cleanup_removed_tools(previous_tools, ai_tools) if previous_tools&.any?
|
|
140
|
+
|
|
141
|
+
# --- Write .rails-ai-context.yml ---
|
|
142
|
+
write_yaml_config(ai_tools, tool_mode)
|
|
143
|
+
|
|
144
|
+
# --- Write .mcp.json (MCP mode only) ---
|
|
145
|
+
write_standalone_mcp_json if tool_mode == :mcp
|
|
146
|
+
|
|
147
|
+
# --- Add .ai-context.json to .gitignore ---
|
|
148
|
+
add_to_gitignore
|
|
149
|
+
|
|
150
|
+
# --- Boot Rails, load config, generate context ---
|
|
151
|
+
boot_rails!
|
|
152
|
+
|
|
153
|
+
$stderr.puts ""
|
|
154
|
+
$stderr.puts "Generating AI context files..."
|
|
155
|
+
ai_tools.each do |fmt|
|
|
156
|
+
result = ::RailsAiContext.generate_context(format: fmt)
|
|
157
|
+
(result[:written] || []).each { |f| $stderr.puts " Written: #{f}" }
|
|
158
|
+
(result[:skipped] || []).each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# --- Instructions ---
|
|
162
|
+
show_standalone_instructions(ai_tools, tool_mode)
|
|
163
|
+
end
|
|
164
|
+
|
|
109
165
|
desc "version", "Print version"
|
|
110
166
|
def version
|
|
111
167
|
require_relative "../lib/rails_ai_context/version"
|
|
@@ -114,18 +170,234 @@ module RailsAiContext
|
|
|
114
170
|
|
|
115
171
|
private
|
|
116
172
|
|
|
173
|
+
AI_TOOL_OPTIONS = {
|
|
174
|
+
"1" => { key: :claude, name: "Claude Code", files: "CLAUDE.md + .claude/rules/" },
|
|
175
|
+
"2" => { key: :cursor, name: "Cursor", files: ".cursor/rules/" },
|
|
176
|
+
"3" => { key: :copilot, name: "GitHub Copilot", files: ".github/copilot-instructions.md + .github/instructions/" },
|
|
177
|
+
"4" => { key: :opencode, name: "OpenCode", files: "AGENTS.md" }
|
|
178
|
+
}.freeze
|
|
179
|
+
|
|
180
|
+
def prompt_ai_tools
|
|
181
|
+
$stderr.puts ""
|
|
182
|
+
$stderr.puts "Which AI tools do you use? (select all that apply)"
|
|
183
|
+
$stderr.puts ""
|
|
184
|
+
AI_TOOL_OPTIONS.each { |num, info| $stderr.puts " #{num}. #{info[:name].ljust(16)} -> #{info[:files]}" }
|
|
185
|
+
$stderr.puts " a. All of the above"
|
|
186
|
+
$stderr.puts ""
|
|
187
|
+
$stderr.print "Enter numbers separated by commas (e.g. 1,2) or 'a' for all: "
|
|
188
|
+
input = $stdin.gets&.strip&.downcase || "a"
|
|
189
|
+
|
|
190
|
+
selected = if input == "a" || input == "all" || input.empty?
|
|
191
|
+
AI_TOOL_OPTIONS.values.map { |t| t[:key] }
|
|
192
|
+
else
|
|
193
|
+
input.split(/[\s,]+/).filter_map { |n| AI_TOOL_OPTIONS[n]&.dig(:key) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if selected.empty?
|
|
197
|
+
$stderr.puts "No tools selected — using all."
|
|
198
|
+
selected = AI_TOOL_OPTIONS.values.map { |t| t[:key] }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
names = AI_TOOL_OPTIONS.values.select { |t| selected.include?(t[:key]) }.map { |t| t[:name] }
|
|
202
|
+
$stderr.puts "Selected: #{names.join(', ')}"
|
|
203
|
+
selected
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def prompt_tool_mode
|
|
207
|
+
$stderr.puts ""
|
|
208
|
+
$stderr.puts "Do you also want MCP server support?"
|
|
209
|
+
$stderr.puts ""
|
|
210
|
+
$stderr.puts " 1. Yes — MCP server (generates .mcp.json)"
|
|
211
|
+
$stderr.puts " 2. No — CLI only (no server needed)"
|
|
212
|
+
$stderr.puts ""
|
|
213
|
+
$stderr.print "Enter number (default: 1): "
|
|
214
|
+
input = $stdin.gets&.strip || "1"
|
|
215
|
+
|
|
216
|
+
mode = input == "2" ? :cli : :mcp
|
|
217
|
+
label = mode == :mcp ? "MCP server" : "CLI only"
|
|
218
|
+
$stderr.puts "Selected: #{label}"
|
|
219
|
+
mode
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Files/dirs generated per AI tool format — used for cleanup on tool removal
|
|
223
|
+
FORMAT_PATHS = {
|
|
224
|
+
claude: %w[CLAUDE.md .claude/rules],
|
|
225
|
+
cursor: %w[.cursor/rules],
|
|
226
|
+
copilot: %w[.github/copilot-instructions.md .github/instructions],
|
|
227
|
+
opencode: %w[AGENTS.md app/models/AGENTS.md app/controllers/AGENTS.md]
|
|
228
|
+
}.freeze
|
|
229
|
+
|
|
230
|
+
def read_previous_ai_tools
|
|
231
|
+
yaml_path = File.join(Dir.pwd, ".rails-ai-context.yml")
|
|
232
|
+
return nil unless File.exist?(yaml_path)
|
|
233
|
+
|
|
234
|
+
data = YAML.safe_load_file(yaml_path, permitted_classes: [ Symbol ]) || {}
|
|
235
|
+
tools = data["ai_tools"]
|
|
236
|
+
return nil unless tools.is_a?(Array) && tools.any?
|
|
237
|
+
|
|
238
|
+
tools.map(&:to_sym)
|
|
239
|
+
rescue => e
|
|
240
|
+
$stderr.puts "[rails-ai-context] Could not read previous config: #{e.message}" if ENV["DEBUG"]
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def cleanup_removed_tools(previous, current)
|
|
245
|
+
removed = previous.map(&:to_sym) - current.map(&:to_sym)
|
|
246
|
+
return if removed.empty?
|
|
247
|
+
|
|
248
|
+
$stderr.puts ""
|
|
249
|
+
$stderr.puts "These AI tools were removed from your selection:"
|
|
250
|
+
removed.each_with_index do |fmt, idx|
|
|
251
|
+
tool = AI_TOOL_OPTIONS.values.find { |t| t[:key] == fmt }
|
|
252
|
+
$stderr.puts " #{idx + 1}. #{tool[:name]} (#{tool[:files]})" if tool
|
|
253
|
+
end
|
|
254
|
+
$stderr.puts ""
|
|
255
|
+
$stderr.puts "Remove their generated files?"
|
|
256
|
+
$stderr.puts " y — remove all listed above"
|
|
257
|
+
$stderr.puts " n — keep all (default)"
|
|
258
|
+
$stderr.puts " 1,2 — remove only specific ones by number"
|
|
259
|
+
$stderr.puts ""
|
|
260
|
+
$stderr.print "Enter choice: "
|
|
261
|
+
input = $stdin.gets&.strip&.downcase || "n"
|
|
262
|
+
return if input.empty? || input == "n" || input == "no"
|
|
263
|
+
|
|
264
|
+
to_remove = if input == "y" || input == "yes" || input == "a"
|
|
265
|
+
removed
|
|
266
|
+
else
|
|
267
|
+
nums = input.split(/[\s,]+/).filter_map { |n| n.to_i - 1 }
|
|
268
|
+
nums.filter_map { |i| removed[i] if i >= 0 && i < removed.size }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
return if to_remove.empty?
|
|
272
|
+
|
|
273
|
+
require "fileutils"
|
|
274
|
+
to_remove.each do |fmt|
|
|
275
|
+
tool = AI_TOOL_OPTIONS.values.find { |t| t[:key] == fmt }
|
|
276
|
+
paths = FORMAT_PATHS[fmt] || []
|
|
277
|
+
paths.each do |rel_path|
|
|
278
|
+
full = File.join(Dir.pwd, rel_path)
|
|
279
|
+
if File.directory?(full)
|
|
280
|
+
FileUtils.rm_rf(full)
|
|
281
|
+
$stderr.puts " Removed #{rel_path}/"
|
|
282
|
+
elsif File.exist?(full)
|
|
283
|
+
FileUtils.rm_f(full)
|
|
284
|
+
$stderr.puts " Removed #{rel_path}"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
$stderr.puts " #{tool[:name]} files removed" if tool
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def add_to_gitignore
|
|
292
|
+
gitignore = File.join(Dir.pwd, ".gitignore")
|
|
293
|
+
return unless File.exist?(gitignore)
|
|
294
|
+
|
|
295
|
+
content = File.read(gitignore)
|
|
296
|
+
return if content.include?(".ai-context.json")
|
|
297
|
+
|
|
298
|
+
File.open(gitignore, "a") do |f|
|
|
299
|
+
f.puts ""
|
|
300
|
+
f.puts "# rails-ai-context (JSON cache — markdown files should be committed)"
|
|
301
|
+
f.puts ".ai-context.json"
|
|
302
|
+
end
|
|
303
|
+
$stderr.puts "Updated .gitignore"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def write_yaml_config(ai_tools, tool_mode)
|
|
307
|
+
yaml_path = File.join(Dir.pwd, ".rails-ai-context.yml")
|
|
308
|
+
content = {
|
|
309
|
+
"ai_tools" => ai_tools.map(&:to_s),
|
|
310
|
+
"tool_mode" => tool_mode.to_s
|
|
311
|
+
}
|
|
312
|
+
File.write(yaml_path, YAML.dump(content))
|
|
313
|
+
$stderr.puts "Created .rails-ai-context.yml"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def write_standalone_mcp_json
|
|
317
|
+
require "json"
|
|
318
|
+
mcp_path = File.join(Dir.pwd, ".mcp.json")
|
|
319
|
+
server_entry = { "command" => "rails-ai-context", "args" => [ "serve" ] }
|
|
320
|
+
|
|
321
|
+
if File.exist?(mcp_path)
|
|
322
|
+
existing = JSON.parse(File.read(mcp_path)) rescue {}
|
|
323
|
+
existing["mcpServers"] ||= {}
|
|
324
|
+
if existing["mcpServers"]["rails-ai-context"] == server_entry
|
|
325
|
+
$stderr.puts ".mcp.json already up to date — skipped"
|
|
326
|
+
else
|
|
327
|
+
existing["mcpServers"]["rails-ai-context"] = server_entry
|
|
328
|
+
File.write(mcp_path, JSON.pretty_generate(existing) + "\n")
|
|
329
|
+
$stderr.puts "Updated rails-ai-context in .mcp.json"
|
|
330
|
+
end
|
|
331
|
+
else
|
|
332
|
+
content = JSON.pretty_generate({ mcpServers: { "rails-ai-context" => server_entry } }) + "\n"
|
|
333
|
+
File.write(mcp_path, content)
|
|
334
|
+
$stderr.puts "Created .mcp.json (auto-discovered by Claude Code, Cursor, etc.)"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def show_standalone_instructions(ai_tools, tool_mode)
|
|
339
|
+
$stderr.puts ""
|
|
340
|
+
$stderr.puts "=" * 50
|
|
341
|
+
$stderr.puts " rails-ai-context initialized!"
|
|
342
|
+
$stderr.puts "=" * 50
|
|
343
|
+
$stderr.puts ""
|
|
344
|
+
$stderr.puts "Your setup:"
|
|
345
|
+
AI_TOOL_OPTIONS.each_value do |info|
|
|
346
|
+
next unless ai_tools.include?(info[:key])
|
|
347
|
+
$stderr.puts " #{info[:name].ljust(16)} -> #{info[:files]}"
|
|
348
|
+
end
|
|
349
|
+
$stderr.puts ""
|
|
350
|
+
$stderr.puts "Commands:"
|
|
351
|
+
$stderr.puts " rails-ai-context context # Regenerate context files"
|
|
352
|
+
$stderr.puts " rails-ai-context tool NAME # Run any of the 39 tools"
|
|
353
|
+
if tool_mode == :mcp
|
|
354
|
+
$stderr.puts " rails-ai-context serve # Start MCP server"
|
|
355
|
+
end
|
|
356
|
+
$stderr.puts " rails-ai-context doctor # Check AI readiness"
|
|
357
|
+
$stderr.puts ""
|
|
358
|
+
if tool_mode == :mcp
|
|
359
|
+
$stderr.puts "MCP auto-discovery:"
|
|
360
|
+
$stderr.puts " .mcp.json is auto-detected by Claude Code and Cursor."
|
|
361
|
+
$stderr.puts " No manual config needed — just open your project."
|
|
362
|
+
else
|
|
363
|
+
$stderr.puts "CLI tools:"
|
|
364
|
+
$stderr.puts " AI agents can run `rails-ai-context tool schema table=users` directly."
|
|
365
|
+
end
|
|
366
|
+
$stderr.puts ""
|
|
367
|
+
$stderr.puts "Config: .rails-ai-context.yml (edit anytime, no restart needed)"
|
|
368
|
+
$stderr.puts ""
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def print_context_result(result)
|
|
372
|
+
(result[:written] || []).each { |f| $stderr.puts " Written: #{f}" }
|
|
373
|
+
(result[:skipped] || []).each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
|
|
374
|
+
end
|
|
375
|
+
|
|
117
376
|
def boot_rails!
|
|
118
|
-
# Try to find and boot the Rails app
|
|
119
377
|
config_path = File.join(Dir.pwd, "config", "environment.rb")
|
|
120
|
-
|
|
121
|
-
require config_path
|
|
122
|
-
else
|
|
378
|
+
unless File.exist?(config_path)
|
|
123
379
|
$stderr.puts "Error: No Rails app found in #{Dir.pwd}"
|
|
124
380
|
$stderr.puts "Run this command from your Rails app root directory."
|
|
125
381
|
exit 1
|
|
126
382
|
end
|
|
383
|
+
|
|
384
|
+
# Load gem + dependencies BEFORE Rails boot.
|
|
385
|
+
# Bundler.setup (in config/boot.rb) strips $LOAD_PATH to Gemfile-only gems.
|
|
386
|
+
# In standalone mode (gem not in Gemfile), this would remove our gem and mcp.
|
|
387
|
+
# Loading first ensures Zeitwerk's autoload paths (absolute, not $LOAD_PATH-
|
|
388
|
+
# dependent) are configured, and we can restore stripped paths after boot.
|
|
389
|
+
require "mcp"
|
|
390
|
+
require "rails_ai_context"
|
|
391
|
+
pre_boot_paths = $LOAD_PATH.dup
|
|
392
|
+
|
|
393
|
+
require config_path
|
|
394
|
+
|
|
395
|
+
# Restore dependency paths that Bundler.setup stripped (standalone mode).
|
|
396
|
+
# For Gemfile users, this is a no-op — no paths were removed.
|
|
397
|
+
(pre_boot_paths - $LOAD_PATH).each { |p| $LOAD_PATH << p }
|
|
398
|
+
|
|
399
|
+
RailsAiContext::Configuration.auto_load!
|
|
127
400
|
end
|
|
128
|
-
end
|
|
129
401
|
end
|
|
130
402
|
|
|
131
|
-
|
|
403
|
+
RailsAiContextCLI.start(ARGV)
|
|
@@ -138,12 +138,13 @@ module RailsAiContext
|
|
|
138
138
|
existing = JSON.parse(File.read(mcp_path)) rescue {}
|
|
139
139
|
existing["mcpServers"] ||= {}
|
|
140
140
|
|
|
141
|
-
if existing["mcpServers"]["rails-ai-context"]
|
|
142
|
-
say ".mcp.json already
|
|
141
|
+
if existing["mcpServers"]["rails-ai-context"] == server_entry
|
|
142
|
+
say ".mcp.json already up to date — skipped", :yellow
|
|
143
143
|
else
|
|
144
144
|
existing["mcpServers"]["rails-ai-context"] = server_entry
|
|
145
145
|
File.write(mcp_path, JSON.pretty_generate(existing) + "\n")
|
|
146
|
-
|
|
146
|
+
verb = existing["mcpServers"].key?("rails-ai-context") ? "Updated" : "Added"
|
|
147
|
+
say "#{verb} rails-ai-context in .mcp.json", :green
|
|
147
148
|
end
|
|
148
149
|
else
|
|
149
150
|
create_file ".mcp.json", JSON.pretty_generate({
|
|
@@ -383,6 +384,18 @@ module RailsAiContext
|
|
|
383
384
|
end
|
|
384
385
|
end # no_tasks
|
|
385
386
|
|
|
387
|
+
def create_yaml_config
|
|
388
|
+
yaml_path = Rails.root.join(".rails-ai-context.yml")
|
|
389
|
+
content = {
|
|
390
|
+
"ai_tools" => @selected_formats.map(&:to_s),
|
|
391
|
+
"tool_mode" => @tool_mode.to_s
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
require "yaml"
|
|
395
|
+
File.write(yaml_path, YAML.dump(content))
|
|
396
|
+
say "Created .rails-ai-context.yml (standalone config)", :green
|
|
397
|
+
end
|
|
398
|
+
|
|
386
399
|
def add_to_gitignore
|
|
387
400
|
gitignore = Rails.root.join(".gitignore")
|
|
388
401
|
return unless File.exist?(gitignore)
|
|
@@ -459,6 +472,11 @@ module RailsAiContext
|
|
|
459
472
|
say " rails ai:context:copilot # Generate for Copilot"
|
|
460
473
|
say " rails generate rails_ai_context:install # Re-run to pick tools"
|
|
461
474
|
say ""
|
|
475
|
+
say "Standalone (no Gemfile needed):", :yellow
|
|
476
|
+
say " gem install rails-ai-context"
|
|
477
|
+
say " rails-ai-context init # interactive setup"
|
|
478
|
+
say " rails-ai-context serve # start MCP server"
|
|
479
|
+
say ""
|
|
462
480
|
say "Commit context files and .mcp.json so your team benefits!", :green
|
|
463
481
|
end
|
|
464
482
|
end
|
|
@@ -1,7 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
3
5
|
module RailsAiContext
|
|
4
6
|
class Configuration
|
|
7
|
+
CONFIG_FILENAME = ".rails-ai-context.yml"
|
|
8
|
+
|
|
9
|
+
# Keys that require symbol conversion (string → symbol or array of symbols)
|
|
10
|
+
SYMBOL_KEYS = %i[tool_mode preset context_mode live_reload].freeze
|
|
11
|
+
SYMBOL_ARRAY_KEYS = %i[ai_tools introspectors].freeze
|
|
12
|
+
|
|
13
|
+
# All YAML-supported keys (explicit allowlist for safety)
|
|
14
|
+
YAML_KEYS = %i[
|
|
15
|
+
ai_tools tool_mode preset context_mode generate_root_files claude_max_lines
|
|
16
|
+
server_name server_version cache_ttl max_tool_response_chars
|
|
17
|
+
live_reload live_reload_debounce auto_mount http_path http_bind http_port
|
|
18
|
+
output_dir skip_tools excluded_models excluded_controllers
|
|
19
|
+
excluded_route_prefixes excluded_filters excluded_middleware excluded_paths
|
|
20
|
+
sensitive_patterns search_extensions concern_paths frontend_paths mobile_paths
|
|
21
|
+
max_file_size max_test_file_size max_schema_file_size max_view_total_size
|
|
22
|
+
max_view_file_size max_search_results max_validate_files
|
|
23
|
+
query_timeout query_row_limit query_redacted_columns allow_query_in_production
|
|
24
|
+
log_lines introspectors
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# Load configuration from a YAML file, applying values to the current config instance.
|
|
28
|
+
# Only keys present in the YAML are set; absent keys keep their defaults.
|
|
29
|
+
def self.load_from_yaml(path)
|
|
30
|
+
return unless File.exist?(path)
|
|
31
|
+
|
|
32
|
+
data = YAML.safe_load_file(path, permitted_classes: [ Symbol ]) || {}
|
|
33
|
+
config = RailsAiContext.configuration
|
|
34
|
+
|
|
35
|
+
data.each do |key, value|
|
|
36
|
+
key_sym = key.to_sym
|
|
37
|
+
next unless YAML_KEYS.include?(key_sym)
|
|
38
|
+
next if value.nil?
|
|
39
|
+
|
|
40
|
+
value = coerce_value(key_sym, value)
|
|
41
|
+
config.public_send(:"#{key_sym}=", value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
config
|
|
45
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
46
|
+
$stderr.puts "[rails-ai-context] WARNING: #{path} has invalid YAML (#{e.message}). Using defaults."
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Auto-load config from .rails-ai-context.yml if no initializer configure block ran.
|
|
51
|
+
# Safe to call multiple times (idempotent).
|
|
52
|
+
def self.auto_load!(dir = nil)
|
|
53
|
+
return if RailsAiContext.configured_via_block?
|
|
54
|
+
|
|
55
|
+
dir ||= defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.to_s : Dir.pwd
|
|
56
|
+
yaml_path = File.join(dir, CONFIG_FILENAME)
|
|
57
|
+
load_from_yaml(yaml_path) if File.exist?(yaml_path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.coerce_value(key, value)
|
|
61
|
+
if SYMBOL_KEYS.include?(key)
|
|
62
|
+
value.respond_to?(:to_sym) ? value.to_sym : value
|
|
63
|
+
elsif SYMBOL_ARRAY_KEYS.include?(key)
|
|
64
|
+
Array(value).map(&:to_sym)
|
|
65
|
+
else
|
|
66
|
+
value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
private_class_method :coerce_value
|
|
70
|
+
|
|
5
71
|
PRESETS = {
|
|
6
72
|
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus
|
|
7
73
|
view_templates design_tokens config components
|
|
@@ -97,13 +97,24 @@ rescue => e
|
|
|
97
97
|
end unless defined?(save_tool_mode_to_initializer)
|
|
98
98
|
|
|
99
99
|
def ensure_mcp_json
|
|
100
|
+
require "json"
|
|
100
101
|
mcp_path = Rails.root.join(".mcp.json")
|
|
101
|
-
return if File.exist?(mcp_path)
|
|
102
|
-
|
|
103
102
|
server_entry = { "command" => "bundle", "args" => [ "exec", "rails", "ai:serve" ] }
|
|
104
|
-
|
|
105
|
-
File.
|
|
106
|
-
|
|
103
|
+
|
|
104
|
+
if File.exist?(mcp_path)
|
|
105
|
+
existing = JSON.parse(File.read(mcp_path)) rescue {}
|
|
106
|
+
existing["mcpServers"] ||= {}
|
|
107
|
+
if existing["mcpServers"]["rails-ai-context"] == server_entry
|
|
108
|
+
return # already up to date
|
|
109
|
+
end
|
|
110
|
+
existing["mcpServers"]["rails-ai-context"] = server_entry
|
|
111
|
+
File.write(mcp_path, JSON.pretty_generate(existing) + "\n")
|
|
112
|
+
puts "✅ Updated rails-ai-context in .mcp.json"
|
|
113
|
+
else
|
|
114
|
+
content = JSON.pretty_generate({ mcpServers: { "rails-ai-context" => server_entry } }) + "\n"
|
|
115
|
+
File.write(mcp_path, content)
|
|
116
|
+
puts "✅ Created .mcp.json (MCP auto-discovery for Claude Code, Cursor, etc.)"
|
|
117
|
+
end
|
|
107
118
|
rescue => e
|
|
108
119
|
puts "⚠️ Could not create .mcp.json: #{e.message}"
|
|
109
120
|
end unless defined?(ensure_mcp_json)
|
|
@@ -143,6 +154,113 @@ rescue => e
|
|
|
143
154
|
nil
|
|
144
155
|
end unless defined?(save_ai_tools_to_initializer)
|
|
145
156
|
|
|
157
|
+
def save_yaml_config(ai_tools, tool_mode)
|
|
158
|
+
require "yaml"
|
|
159
|
+
yaml_path = Rails.root.join(".rails-ai-context.yml")
|
|
160
|
+
content = {
|
|
161
|
+
"ai_tools" => Array(ai_tools).map(&:to_s),
|
|
162
|
+
"tool_mode" => tool_mode.to_s
|
|
163
|
+
}
|
|
164
|
+
File.write(yaml_path, YAML.dump(content))
|
|
165
|
+
puts "💾 Saved .rails-ai-context.yml (standalone config)"
|
|
166
|
+
rescue => e
|
|
167
|
+
$stderr.puts "[rails-ai-context] save_yaml_config failed: #{e.message}" if ENV["DEBUG"]
|
|
168
|
+
nil
|
|
169
|
+
end unless defined?(save_yaml_config)
|
|
170
|
+
|
|
171
|
+
FORMAT_PATHS = {
|
|
172
|
+
claude: %w[CLAUDE.md .claude/rules],
|
|
173
|
+
cursor: %w[.cursor/rules],
|
|
174
|
+
copilot: %w[.github/copilot-instructions.md .github/instructions],
|
|
175
|
+
opencode: %w[AGENTS.md app/models/AGENTS.md app/controllers/AGENTS.md]
|
|
176
|
+
}.freeze unless defined?(FORMAT_PATHS)
|
|
177
|
+
|
|
178
|
+
def read_previous_ai_tools_from_config
|
|
179
|
+
# Try initializer first
|
|
180
|
+
init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
|
|
181
|
+
if File.exist?(init_path)
|
|
182
|
+
content = File.read(init_path)
|
|
183
|
+
match = content.match(/^\s*config\.ai_tools\s*=\s*%i\[([^\]]*)\]/)
|
|
184
|
+
return match[1].split.map(&:to_sym) if match
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Fall back to YAML
|
|
188
|
+
yaml_path = Rails.root.join(".rails-ai-context.yml")
|
|
189
|
+
if File.exist?(yaml_path)
|
|
190
|
+
require "yaml"
|
|
191
|
+
data = YAML.safe_load_file(yaml_path, permitted_classes: [ Symbol ]) || {}
|
|
192
|
+
tools = data["ai_tools"]
|
|
193
|
+
return tools.map(&:to_sym) if tools.is_a?(Array) && tools.any?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
nil
|
|
197
|
+
rescue => e
|
|
198
|
+
$stderr.puts "[rails-ai-context] read_previous_ai_tools_from_config failed: #{e.message}" if ENV["DEBUG"]
|
|
199
|
+
nil
|
|
200
|
+
end unless defined?(read_previous_ai_tools_from_config)
|
|
201
|
+
|
|
202
|
+
def cleanup_removed_ai_tools(previous, current)
|
|
203
|
+
removed = previous.map(&:to_sym) - current.map(&:to_sym)
|
|
204
|
+
return if removed.empty?
|
|
205
|
+
|
|
206
|
+
puts ""
|
|
207
|
+
puts "These AI tools were removed from your selection:"
|
|
208
|
+
removed.each_with_index do |fmt, idx|
|
|
209
|
+
tool = AI_TOOL_OPTIONS.values.find { |t| t[:key] == fmt }
|
|
210
|
+
puts " #{idx + 1}. #{tool[:name]}" if tool
|
|
211
|
+
end
|
|
212
|
+
puts ""
|
|
213
|
+
puts "Remove their generated files?"
|
|
214
|
+
puts " y — remove all listed above"
|
|
215
|
+
puts " n — keep all (default)"
|
|
216
|
+
puts " 1,2 — remove only specific ones by number"
|
|
217
|
+
puts ""
|
|
218
|
+
print "Enter choice: "
|
|
219
|
+
input = $stdin.gets&.strip&.downcase || "n"
|
|
220
|
+
return if input.empty? || input == "n" || input == "no"
|
|
221
|
+
|
|
222
|
+
to_remove = if input == "y" || input == "yes" || input == "a"
|
|
223
|
+
removed
|
|
224
|
+
else
|
|
225
|
+
nums = input.split(/[\s,]+/).filter_map { |n| n.to_i - 1 }
|
|
226
|
+
nums.filter_map { |i| removed[i] if i >= 0 && i < removed.size }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
return if to_remove.empty?
|
|
230
|
+
|
|
231
|
+
require "fileutils"
|
|
232
|
+
to_remove.each do |fmt|
|
|
233
|
+
tool = AI_TOOL_OPTIONS.values.find { |t| t[:key] == fmt }
|
|
234
|
+
paths = FORMAT_PATHS[fmt] || []
|
|
235
|
+
paths.each do |rel_path|
|
|
236
|
+
full = Rails.root.join(rel_path)
|
|
237
|
+
if File.directory?(full)
|
|
238
|
+
FileUtils.rm_rf(full)
|
|
239
|
+
puts " Removed #{rel_path}/"
|
|
240
|
+
elsif File.exist?(full)
|
|
241
|
+
FileUtils.rm_f(full)
|
|
242
|
+
puts " Removed #{rel_path}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
puts " ✅ #{tool[:name]} files removed" if tool
|
|
246
|
+
end
|
|
247
|
+
end unless defined?(cleanup_removed_ai_tools)
|
|
248
|
+
|
|
249
|
+
def add_ai_context_to_gitignore
|
|
250
|
+
gitignore = Rails.root.join(".gitignore")
|
|
251
|
+
return unless File.exist?(gitignore)
|
|
252
|
+
|
|
253
|
+
content = File.read(gitignore)
|
|
254
|
+
return if content.include?(".ai-context.json")
|
|
255
|
+
|
|
256
|
+
File.open(gitignore, "a") do |f|
|
|
257
|
+
f.puts ""
|
|
258
|
+
f.puts "# rails-ai-context (JSON cache — markdown files should be committed)"
|
|
259
|
+
f.puts ".ai-context.json"
|
|
260
|
+
end
|
|
261
|
+
puts "✅ Updated .gitignore"
|
|
262
|
+
end unless defined?(add_ai_context_to_gitignore)
|
|
263
|
+
|
|
146
264
|
def add_ai_tool_to_initializer(format)
|
|
147
265
|
init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
|
|
148
266
|
return unless File.exist?(init_path)
|
|
@@ -223,6 +341,7 @@ namespace :ai do
|
|
|
223
341
|
apply_context_mode_override
|
|
224
342
|
|
|
225
343
|
ai_tools = RailsAiContext.configuration.ai_tools
|
|
344
|
+
previous_tools = read_previous_ai_tools_from_config
|
|
226
345
|
|
|
227
346
|
# First time — no tools configured, ask the user
|
|
228
347
|
if ai_tools.nil?
|
|
@@ -237,9 +356,19 @@ namespace :ai do
|
|
|
237
356
|
save_tool_mode_to_initializer(tool_mode)
|
|
238
357
|
end
|
|
239
358
|
|
|
240
|
-
#
|
|
359
|
+
# Cleanup removed tools (only when re-running with different selections)
|
|
360
|
+
cleanup_removed_ai_tools(previous_tools, ai_tools) if previous_tools&.any? && ai_tools
|
|
361
|
+
|
|
362
|
+
# Write .rails-ai-context.yml alongside initializer (enables standalone mode)
|
|
363
|
+
save_yaml_config(ai_tools || RailsAiContext.configuration.ai_tools,
|
|
364
|
+
RailsAiContext.configuration.tool_mode)
|
|
365
|
+
|
|
366
|
+
# Auto-create/update .mcp.json when tool_mode is :mcp
|
|
241
367
|
ensure_mcp_json if RailsAiContext.configuration.tool_mode == :mcp
|
|
242
368
|
|
|
369
|
+
# Add .ai-context.json to .gitignore
|
|
370
|
+
add_ai_context_to_gitignore
|
|
371
|
+
|
|
243
372
|
puts "🔍 Introspecting #{Rails.application.class.module_parent_name}..."
|
|
244
373
|
|
|
245
374
|
if ai_tools.nil? || ai_tools.empty?
|
|
@@ -257,6 +386,11 @@ namespace :ai do
|
|
|
257
386
|
puts ""
|
|
258
387
|
puts "Done! Commit these files so your team benefits."
|
|
259
388
|
puts "Change AI tools: config/initializers/rails_ai_context.rb (config.ai_tools)"
|
|
389
|
+
puts ""
|
|
390
|
+
puts "Standalone (no Gemfile needed):"
|
|
391
|
+
puts " gem install rails-ai-context"
|
|
392
|
+
puts " rails-ai-context init # interactive setup"
|
|
393
|
+
puts " rails-ai-context serve # start MCP server"
|
|
260
394
|
end
|
|
261
395
|
|
|
262
396
|
desc "Generate AI context in a specific format (claude, cursor, copilot, json)"
|
data/lib/rails_ai_context.rb
CHANGED
|
@@ -22,9 +22,14 @@ module RailsAiContext
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def configure
|
|
25
|
+
@configured_via_block = true
|
|
25
26
|
yield(configuration)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def configured_via_block?
|
|
30
|
+
@configured_via_block || false
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# Quick access to introspect the current Rails app
|
|
29
34
|
# Returns a hash of all discovered context
|
|
30
35
|
def introspect(app = nil)
|
data/server.json
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.crisnahine/rails-ai-context",
|
|
4
4
|
"title": "Rails AI Context",
|
|
5
|
-
"description": "Auto-expose Rails app structure to AI via MCP or CLI.
|
|
5
|
+
"description": "Auto-expose Rails app structure to AI via MCP or CLI. 39 read-only tools. Standalone or in-Gemfile.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "
|
|
10
|
+
"version": "4.5.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v4.5.0/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -174,7 +174,7 @@ description: |
|
|
|
174
174
|
structure on demand with semantic validation that catches cross-file errors
|
|
175
175
|
(wrong columns, missing partials, broken routes) before code runs.
|
|
176
176
|
Auto-generates context files for Claude Code, Cursor, GitHub Copilot, and
|
|
177
|
-
OpenCode.
|
|
177
|
+
OpenCode. Works standalone or in-Gemfile.
|
|
178
178
|
email:
|
|
179
179
|
- crisjosephnahine@gmail.com
|
|
180
180
|
executables:
|
|
@@ -318,11 +318,17 @@ metadata:
|
|
|
318
318
|
funding_uri: https://github.com/sponsors/crisnahine
|
|
319
319
|
rubygems_mfa_required: 'true'
|
|
320
320
|
post_install_message: |
|
|
321
|
-
rails-ai-context installed!
|
|
321
|
+
rails-ai-context installed!
|
|
322
|
+
|
|
323
|
+
Standalone (no Gemfile entry needed):
|
|
324
|
+
cd your-rails-app
|
|
325
|
+
rails-ai-context init # interactive setup
|
|
326
|
+
rails-ai-context serve # start MCP server
|
|
327
|
+
|
|
328
|
+
Or add to Gemfile:
|
|
329
|
+
gem "rails-ai-context", group: :development
|
|
322
330
|
rails generate rails_ai_context:install
|
|
323
|
-
rails ai:
|
|
324
|
-
rails 'ai:tool[schema]' # run any of the 39 tools from CLI
|
|
325
|
-
rails ai:serve # start MCP server (optional)
|
|
331
|
+
rails ai:serve
|
|
326
332
|
rdoc_options: []
|
|
327
333
|
require_paths:
|
|
328
334
|
- lib
|
|
@@ -340,5 +346,5 @@ requirements: []
|
|
|
340
346
|
rubygems_version: 3.6.9
|
|
341
347
|
specification_version: 4
|
|
342
348
|
summary: Give AI agents a complete mental model of your Rails app — 39 tools via MCP
|
|
343
|
-
or CLI.
|
|
349
|
+
or CLI. Standalone or in-Gemfile.
|
|
344
350
|
test_files: []
|