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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fa4865cb6081dbc1a243371d270db6f80304fd5c3d7ccd8052d2c7c0d672dc6
4
- data.tar.gz: 055a0fc1462f6a698e004090fda64703e98478af1d8bd1ca3426d85a5e9b6f69
3
+ metadata.gz: 41d06a29e3165804283c9dcef76e2c1ee5625ef5ac236931fa492d76172062b6
4
+ data.tar.gz: 8f2f81c3186b2d16fa9c4ea2b84721fd519d437b4cbd8fb99a3978e45723a189
5
5
  SHA512:
6
- metadata.gz: ac7a84cb7f54d6106c2bf33c94208f75871b9b8d25d304e0ddde0e36f73d3bd8e973dbf5f81b0665545548fd740fbdf60726a79195889cfbb9683cc733c80307
7
- data.tar.gz: 440e13668f46d1578933bd5f053ea15cbae494938097f55380a0ab78c3a30a0f92dd74b20ad37edf7aed67f4aade37810e02b141e9d25e8963d40e3d63a650b9
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 25 tools active).
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. **Zero-config** — Railtie auto-registers at boot, introspects without setup
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
  ![Install demo](demo.gif)
@@ -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. Zero config.
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
- The installer asks which AI tools you use and whether you want MCP or CLI mode. That's it.
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 — no manual config.
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
- | Command | What it does |
374
- |:--------|:------------|
375
- | `rails ai:context` | Generate context files for your AI tools |
376
- | `rails 'ai:tool[NAME]'` | Run any of the 39 tools from CLI |
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. Zero config.<br>
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
- ### New project
32
+ ### Option A: In Gemfile
33
33
 
34
34
  ```bash
35
- bundle add rails-ai-context
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. `.mcp.json` — MCP auto-discovery for Claude Code and Cursor
43
- 3. 29 context files tailored for each AI assistant
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
- ### Existing project
47
+ ### Option B: Standalone (no Gemfile entry needed)
46
48
 
47
49
  ```bash
48
- # Add to Gemfile
49
- gem "rails-ai-context"
50
-
51
- # Install
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
- # Generate context files
56
- rails ai:context
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
- # Verify everything works
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 as an alternative to rake tasks. Useful for `.mcp.json` configs or when you prefer a shorter command.
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 RailsAiContext
7
- class CLI < Thor
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 (25 available). Use --list to see all."
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
- require "rails_ai_context"
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 RailsAiContext::CLI::ToolRunner::ToolNotFoundError => e
34
- $stderr.puts "Error: #{e.message}"
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 2
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: "all", desc: "Format: claude, cursor, copilot, json, all"
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
- result = RailsAiContext.generate_context(format: format)
68
- result[:written].each { |f| $stderr.puts " Written: #{f}" }
69
- result[:skipped].each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
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
- require "rails_ai_context"
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
- require "rails_ai_context"
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
- if File.exist?(config_path)
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
- RailsAiContext::CLI.start(ARGV)
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 has rails-ai-context — skipped", :yellow
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
- say "Added rails-ai-context to existing .mcp.json", :green
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
- content = JSON.pretty_generate({ mcpServers: { "rails-ai-context" => server_entry } }) + "\n"
105
- File.write(mcp_path, content)
106
- puts "✅ Created .mcp.json (MCP auto-discovery for Claude Code, Cursor, etc.)"
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
- # Auto-create .mcp.json when tool_mode is :mcp and it doesn't exist
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)"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "4.4.0"
4
+ VERSION = "4.5.0"
5
5
  end
@@ -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. Zero config, 25 read-only tools.",
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": "3.1.0",
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/v3.1.0/rails-ai-context-mcp.mcpb",
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.0
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. Zero config.
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! Quick start:
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:context # generate context files
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. Zero config.
349
+ or CLI. Standalone or in-Gemfile.
344
350
  test_files: []