rails-ai-context 5.32.1 → 5.32.2

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/CONTEXT.md +10 -8
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +14 -11
  6. data/SECURITY.md +4 -4
  7. data/docs/ARCHITECTURE.md +11 -10
  8. data/docs/CLI.md +33 -7
  9. data/docs/COMPATIBILITY.md +48 -32
  10. data/docs/CONFIGURATION.md +10 -8
  11. data/docs/CUSTOM_TOOLS.md +8 -8
  12. data/docs/FAQ.md +6 -5
  13. data/docs/GUIDE.md +154 -80
  14. data/docs/INTROSPECTORS.md +24 -16
  15. data/docs/QUICKSTART.md +14 -3
  16. data/docs/RECIPES.md +50 -40
  17. data/docs/SECURITY.md +23 -13
  18. data/docs/SETUP.md +23 -26
  19. data/docs/STANDALONE.md +7 -3
  20. data/docs/TOOLS.md +79 -45
  21. data/docs/TROUBLESHOOTING.md +12 -11
  22. data/exe/rails-ai-context +8 -4
  23. data/lib/generators/rails_ai_context/install/install_generator.rb +1 -1
  24. data/lib/rails_ai_context/configuration.rb +2 -2
  25. data/lib/rails_ai_context/install/ai_tool.rb +11 -2
  26. data/lib/rails_ai_context/install/cleanup.rb +69 -23
  27. data/lib/rails_ai_context/install/program.rb +4 -1
  28. data/lib/rails_ai_context/install/selection_record.rb +20 -2
  29. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +2 -2
  30. data/lib/rails_ai_context/server.rb +2 -2
  31. data/lib/rails_ai_context/tasks/rails_ai_context.rake +12 -4
  32. data/lib/rails_ai_context/test_helper.rb +2 -0
  33. data/lib/rails_ai_context/tools/base_tool.rb +3 -1
  34. data/lib/rails_ai_context/tools/dependency_graph.rb +2 -2
  35. data/lib/rails_ai_context/tools/get_concern.rb +1 -1
  36. data/lib/rails_ai_context/tools/get_env.rb +1 -1
  37. data/lib/rails_ai_context/tools/get_gems.rb +2 -2
  38. data/lib/rails_ai_context/tools/query.rb +107 -39
  39. data/lib/rails_ai_context/tools/search_code.rb +20 -16
  40. data/lib/rails_ai_context/version.rb +1 -1
  41. data/server.json +1 -1
  42. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da0f12372e58a903300979aa8be03f53f53e9d5de073709e549a47b46cb8a5eb
4
- data.tar.gz: cc0b3c5e77840cef702e9e163bd3e2d73f24a56ec324fbdfe3ec5b8c6e58eaf7
3
+ metadata.gz: fac0badbc6cdd650df304ed443948b0d96190d77458caebca44b513642d28bd9
4
+ data.tar.gz: 52c14b40304a1fb7e475bfc8755e22954afd655d3e31607d08814c187bd5b05c
5
5
  SHA512:
6
- metadata.gz: 5cc39f9e5208f88eea4ac18c3dac8a9abbe5da282c8a9b8f62594e5f9069483db9ced2f537768a97b864d17921b622ce7c640590e47546292fe3dafb5c7cd65e
7
- data.tar.gz: 56d23208cbd739927ac9d85af26a39bfab5da0cebd75731791717bd67b83752bc28a096d1307e956ee9de593287b17efc70d91b81fa47c6deec2678beb064fd4
6
+ metadata.gz: 139928d253882921153d4f676aa7b7317efb5a4396dcbd4fc9d90ba8cc9abb5f513481b1534a22b18319c731f6b6a4e99fc1b21145daf03f0b980c2fc337c2cf
7
+ data.tar.gz: 122546a2d53a798334bb16f27886300eda3b76a958e3ded63f5f2814257bcbe69b568c585eefb0b81e4cf81d63525412fcef8acddd24ed45a4df1a588bebcbfe
data/CHANGELOG.md CHANGED
@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.32.2] - 2026-10-07
9
+
10
+ ### Security
11
+
12
+ - **`rails_query` runs the text its checks read.** The read-only validator
13
+ strips comments before it looks for blocked keywords, sensitive columns and
14
+ a second statement, but the query that ran was the raw input. The stripper
15
+ did not know string literals, so a `--` or `/*` inside a quoted string hid
16
+ the rest of the line from every check while the database still ran it: a
17
+ column such as `api_key` could be read through an alias, unredacted. Comments
18
+ are now taken out by a scanner that copies quoted strings and identifiers
19
+ whole, and the query that runs is the text it leaves, so the checks and the
20
+ database read the same thing.
21
+
22
+ ### Fixed
23
+
24
+ - **The row limit holds whatever the query text looks like.** `LIMIT` was
25
+ appended after a trailing `-- note`, where the database read it as part of
26
+ the comment, and the first `LIMIT` anywhere in the text counted as the
27
+ query's own: one in a comment or a string literal left the query unlimited,
28
+ and one in a subquery was rewritten to the cap, which changed the answer.
29
+ Only a `LIMIT` or `FETCH FIRST` that ends the statement is the query's own
30
+ now, the appended one goes on its own line, and the answer is cut to the
31
+ limit after the database returns as well.
32
+ - **A `#` no longer ends a PostgreSQL or SQLite query line.** A line starting
33
+ with `#>>`, `#>` or `#-` was dropped from the validated text as a MySQL
34
+ comment. Hash comments are read on MySQL only, where they can start anywhere
35
+ on a line.
36
+ - **Dropping an AI tool on a re-run removes only what the gem generated.**
37
+ Answering yes to "Remove their generated files?" deleted the whole rules
38
+ directory and the whole root file, so a rule file written by hand in
39
+ `.claude/rules/`, and a `CLAUDE.md` the gem had never written to, went with
40
+ them. Cleanup now removes the rule files the gem names, the block between
41
+ its markers in a root file, and a split rules file it generated whole. It
42
+ leaves every other file and line, a symlink included, and says so when a
43
+ file kept the user's own content.
44
+ - **`search_code` trace names the routes that reach the call.** A controller
45
+ caller's heading listed the controller's first two routes whatever action
46
+ the call was in, so a call inside `create` could read `GET /posts`. The
47
+ heading now lists the routes of the actions the calls sit in, and none when
48
+ the call is in a `before_action` line, a private or class method, or a
49
+ nested class.
50
+ - **`rails-ai-context init` records the tool mode where it is read first.**
51
+ It wrote `tool_mode` to `.rails-ai-context.yml` only, and an initializer that
52
+ already set `config.tool_mode` won on read, so choosing CLI mode changed
53
+ nothing in such an app. It now updates the initializer line too, as it does
54
+ for the tool selection. An assignment written in another shape, such as one
55
+ read from `ENV`, is left alone and no second line is added beside it; the
56
+ same now holds for `config.context_files`.
57
+ - **`--defaults` on the install generator asks nothing.** The legacy-files
58
+ question still waited for an answer on a terminal.
59
+ - **`rails-ai-context init --mcp-only` no longer asks about legacy rule
60
+ files**, matching the generator.
61
+ - **`rails_get_gems` filters by every category it prints.** The `category`
62
+ parameter accepted 8 of the 19 categories, so `monitoring`, `admin`,
63
+ `payments` and the rest could not be asked for.
64
+ - **A composed answer carries the static tier banner once.** `rails_get_context`
65
+ repeated it for each tool the answer was built from.
66
+ - **`rails_ai_context/test_helper` loads the MCP SDK.** A spec for a custom
67
+ tool raised `uninitialized constant MCP` when the helper was the first thing
68
+ the suite required.
69
+ - **Descriptions say what the code does.** `rails_dependency_graph` no longer
70
+ claims services and controllers, `rails_get_concern` no longer promises a
71
+ `summary` level that differs from `standard`, `rails ai:context:codex` no
72
+ longer says it writes `.codex/config.toml`, and `init --help` no longer names
73
+ `.mcp.json` as the one MCP config.
74
+
75
+ - **The docs say what the code does.** They were checked against the code,
76
+ line by line. The install steps
77
+ are listed in full in the guide, MCP config samples show the command the
78
+ installer writes, every tool's parameters match its schema, and counts,
79
+ defaults and error messages are the ones the code has. `rails ai:setup`,
80
+ which was documented and never existed, is gone from the CLI reference.
81
+
8
82
  ## [5.32.1] - 2026-10-06
9
83
 
10
84
  ### Fixed
data/CONTEXT.md CHANGED
@@ -6,7 +6,7 @@ Terms this project uses in a narrower sense than everyday English. One entry per
6
6
 
7
7
  Overloaded. Always qualify it; never use "environment" bare in a tool name, an introspector name, or a config key.
8
8
 
9
- **Process environment** - the environment variables a running app sees, plus the places they are declared (`ENV[]` call sites, `.env.example`, Dockerfile, credentials keys). Served by `rails_get_env`, read by `EnvIntrospector`.
9
+ **Process environment** - the environment variables a running app sees, plus the places they are declared (`ENV[]` call sites, `.env.example`, Dockerfile, credentials keys). Served by `rails_get_env`: `EnvIntrospector` reads the variables and the `ENV[]` call sites, and the tool reads the other places itself.
10
10
 
11
11
  **Environment config** - what `config/environments/*.rb` declares per environment: the assigned `config.*` keys and the values of the notable toggles. Served by `rails_get_env_config`, read by `EnvConfigIntrospector`.
12
12
 
@@ -18,7 +18,7 @@ The assistant a user points at their app: claude, cursor, copilot, opencode, cod
18
18
 
19
19
  Avoid "format" for either sense - claude is not a file format, and the word collides with real formatting elsewhere in the code. The public config key `ai_tools` already uses the canonical term.
20
20
 
21
- `Install::AiTool` is the one table of what each means: name, context files, MCP config shape, legacy leftovers. `Install::SelectionRecord` owns which ones the user picked - written to YAML always and to the initializer line inside a Rails app, read initializer-first because that is the file a user hand-edits.
21
+ `Install::AiTool` is the one table of what each means: name, context files, MCP config shape, legacy leftovers. `Install::SelectionRecord` owns which ones the user picked - written to YAML always, and to the initializer line when the caller asks for it and the line is in a shape it can rewrite (an ordinary `ai:context` run refreshes the YAML only), read initializer-first because that is the file a user hand-edits.
22
22
 
23
23
  ## Path
24
24
 
@@ -30,13 +30,13 @@ Two senses, one per module, and neither is bare "path" in a name.
30
30
 
31
31
  ## Safe path
32
32
 
33
- A caller-supplied path resolved once, through `SafePath`, before anything reads it. Its refusal order is the contract, not an implementation detail: traversal, sensitive name, realpath, containment, sensitive realpath, file, size. A caller that reorders those checks, or repeats one of them itself, gets a different answer on a symlink or a dotfile than every other tool does, which is the divergence the module exists to end. `safe_glob` is the globbed-path form: the same guard applied to each path a pattern yields.
33
+ A caller-supplied path resolved once, through `SafePath`, before anything reads it. Its refusal order is the contract, not an implementation detail: traversal, sensitive name, realpath, containment, sensitive realpath, file, size. A caller that reorders those checks, or repeats one of them itself, gets a different answer on a symlink or a dotfile than every other tool does, which is the divergence the module exists to end. `BaseTool.safe_glob` is the globbed-path form: realpath, containment and the sensitive-realpath check on each path a pattern yields, with the size cap left to the read that follows.
34
34
 
35
35
  Not the same as a path that merely looks harmless, and not a caller's own containment check - "the tool guards this parameter" means it hands the parameter to `SafePath` and renders whatever refusal comes back.
36
36
 
37
37
  ## Carried path
38
38
 
39
- A path the payload already holds because the gem's own walk found it - a controller's or a model's `file:`. It is re-read with `SafeFile.read(File.join(root, relative))`, which applies the size cap and nothing else. The reason is the walk: `SourceScan` deliberately keeps the spelling the app uses rather than the realpath, so a pack or an in-repo engine symlinked out of the root is spelled inside it, and `SafePath`'s realpath containment would refuse the very file the payload just named - the source comes back nil and a section silently empties.
39
+ A path the payload already holds because the gem's own walk found it - a controller's or a model's `file:`. A controller's is re-read with `SafeFile.read(File.join(root, relative))`, which checks that it is a regular file under the size cap and nothing else; a model's goes through `PortablePath.resolve` first, since it can be a `gem:` path. The reason is the walk: `SourceScan` deliberately keeps the spelling the app uses when the realpath falls outside the root, so a pack or an in-repo engine symlinked out of the root is spelled inside it, and `SafePath`'s realpath containment would refuse the very file the payload just named - the source comes back nil and a section silently empties.
40
40
 
41
41
  Not an exception to the **safe path** rule, the other side of it: a caller-supplied path is untrusted and goes through `SafePath`; a carried path was produced by this gem and only needs the cap.
42
42
 
@@ -58,7 +58,7 @@ The other half of the same problem is the reverse trip. Once a name is the decla
58
58
 
59
59
  The table a model reads. Rails builds it as `table_name_prefix` + the demodulized class name, pluralized unless `pluralize_table_names` is off, + `table_name_suffix`, unless the class assigns one itself or is an STI child, which reads its parent's. The static tier answers the same order from source: an explicit `self.table_name =`, else the table of the model it inherits from, else the prefix and suffix the enclosing modules declare around the file's own basename. That basename is pluralized unless the class, then its superclass chain, then the app sets `pluralize_table_names = false`. The app-wide affixes and pluralize setting come from `ActiveRecordSettings`, which reads config/application.rb, the environment file, the initializers and `on_load(:active_record)` blocks. Never the underscored constant - that turns `Admin::ActionLog` into `admin/action_logs` and `OAuthClientConfig` into `o_auth_client_configs`, neither of which is a table.
60
60
 
61
- `TableName` reads the five declarations of one class body (`table_name`, `table_name_prefix`, `table_name_suffix`, `pluralize_table_names` and `primary_key`) and holds the two derivations; walking the namespace and the superclass chain belongs to the caller, because only it has the other files. A reader that starts from a model name instead of a file asks `TableName.for_model_name`, which takes the table the model tier already recorded before falling back to the convention. A prefix declared outside the model directories is still invisible, so the answer stays [STATIC].
61
+ `TableName` reads the five declarations of one class body (`table_name`, `table_name_prefix`, `table_name_suffix`, `pluralize_table_names` and `primary_key`) and holds the two derivations; walking the namespace and the superclass chain belongs to the caller, because only it has the other files. A reader that starts from a model name instead of a file asks `TableName.for_model_name`, which takes the table the model tier already recorded before falling back to the convention. A module prefix declared outside the model directories is still invisible, an engine's in `lib/**/engine.rb` aside, so the answer stays [STATIC].
62
62
 
63
63
  ## Static tier
64
64
 
@@ -94,8 +94,10 @@ A Rack app the routing table attaches at a path, engine or not. `mount App =>
94
94
  "/path"` is `match("/path", to: App, via: :all, anchor: false)` with a name
95
95
  derived, so both spellings build the same endpoint and both are this. The
96
96
  payload key is `mounted_engines` for the sections that predate the widening;
97
- what it holds is every controller-less, non-dynamic endpoint the route set
98
- carries, which is what the count beside it has always counted.
97
+ in the booted routes section it holds every controller-less, non-dynamic
98
+ endpoint the route set carries, which is what the count beside it has always
99
+ counted. The engines section, and the static routes section, hold the mounts
100
+ parsed from the route files.
99
101
 
100
102
  Distinct from a **Rails engine**, which is a `Rails::Engine` subclass whether
101
103
  or not anything mounts it, and which `rails_get_engines` lists separately under
@@ -139,7 +141,7 @@ Which filters a controller runs, and which of them a given action runs. `ActionF
139
141
 
140
142
  **A block filter is named by its line.** `before_action { ... }` and `after_action -> { ... }` are callbacks of their own, listed as `block (line N)` and `lambda (line N)` (a lambda says so, as `Proc#lambda?` does, so it reads apart from a block on its line), N being the line the block opens on in the controller's own file; a block in any other file (a concern's class method, an ancestor's body) is `block (line N of app/controllers/concerns/cache_concern.rb)`, as two blocks on one line number in two files are two callbacks. The booted tier lists only a block the app wrote; a framework's or a gem's (`allow_browser`) is left out, as the static tier cannot see it. `skip_forgery_protection` is the `skip_before_action :verify_authenticity_token` Rails defines it as.
141
143
 
142
- **What it cannot see is stated rather than guessed.** The walk reads `ApplicationController`'s file by the one name Rails fixes, so its filters are in the chain; a parent the listing does not hold and the app has no file for - a gem-owned base, or one an inflection renames - still ends the walk. `docs/COMPATIBILITY.md` says both halves.
144
+ **What it cannot see is stated rather than guessed.** The walk reads `ApplicationController`'s file by the one name Rails fixes, so its filters are in the chain; a parent the listing does not hold and the app has no file for - a gem-owned base other than Devise's or Doorkeeper's (there the walk resumes at the parent the initializer names), or one an inflection renames - still ends the walk. `docs/COMPATIBILITY.md` says both halves.
143
145
 
144
146
  ## Payload
145
147
 
data/CONTRIBUTING.md CHANGED
@@ -35,7 +35,7 @@ lib/rails_ai_context/
35
35
  3. Register it in `lib/rails_ai_context/introspector.rb` (the `INTROSPECTOR_MAP`)
36
36
  4. `extend StaticTier` and declare one: `static_tier :files_only` if `#call` runs unchanged with nothing booted, `:runtime_only` if it needs live reflection, or `:alternate_source` plus a `static_call` reading elsewhere. A mapped introspector that declares nothing fails the suite
37
37
  5. Add the key to the appropriate preset(s) in `Configuration::PRESETS` (`:full` is the default, `:standard` for core-only)
38
- 6. Write specs in `spec/lib/rails_ai_context/your_introspector_spec.rb`
38
+ 6. Write specs in `spec/lib/rails_ai_context/introspectors/your_introspector_spec.rb`
39
39
 
40
40
  ## Adding a New MCP Tool
41
41
 
@@ -86,7 +86,7 @@ bundle exec rubocop --parallel # Lint check
86
86
 
87
87
  `spec/fixtures/static_app` is the app the static tier is exercised against, and
88
88
  `spec/lib/rails_ai_context/serializers/real_shape_smoke_spec.rb` pins its model
89
- count in two places (`Models 7` and `Models (7)`).
89
+ count in three assertions (a `Models 7` match and `Models (7)` twice).
90
90
 
91
91
  Adding or removing a fixture model means changing that spec in the same commit.
92
92
 
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![Downloads](https://img.shields.io/gem/dt/rails-ai-context?style=flat-square&color=blue)](https://rubygems.org/gems/rails-ai-context)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/crisnahine/rails-ai-context/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/crisnahine/rails-ai-context/actions)
10
10
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green?style=flat-square)](https://registry.modelcontextprotocol.io)
11
- [![Ruby](https://img.shields.io/badge/Ruby-3.1_to_3.4-CC342D?style=flat-square&logo=ruby&logoColor=white)](https://github.com/crisnahine/rails-ai-context)
11
+ [![Ruby](https://img.shields.io/badge/Ruby-3.1_to_4.0-CC342D?style=flat-square&logo=ruby&logoColor=white)](https://github.com/crisnahine/rails-ai-context)
12
12
  [![Rails](https://img.shields.io/badge/Rails-7.0_to_8.1-CC0000?style=flat-square&logo=rubyonrails&logoColor=white)](https://github.com/crisnahine/rails-ai-context)
13
13
  [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](LICENSE)
14
14
 
@@ -30,7 +30,7 @@
30
30
  **rails-ai-context** is a Ruby gem that turns your Rails app into the source of truth for AI coding assistants. Instead of guessing your schema, associations, routes and conventions from training data, the assistant asks your app: 45 read-only tools served over [MCP](https://modelcontextprotocol.io) or run from the CLI, plus generated context files for Claude Code, Cursor, GitHub Copilot, OpenCode and Codex CLI.
31
31
 
32
32
  > [!TIP]
33
- > Nothing to add to your Gemfile if you don't want to. `gem install rails-ai-context`, then `rails-ai-context init` inside any Rails app. It also works on an app that won't boot: pass `--no-boot` and every tool answers from the source files.
33
+ > Nothing to add to your Gemfile if you don't want to. `gem install rails-ai-context`, then `rails-ai-context init` inside any Rails app. It also works on an app that won't boot: pass `--no-boot` and the tools answer from the source files. The two that need a live app, `query` and `runtime_info`, say so.
34
34
 
35
35
  ## Why
36
36
 
@@ -63,7 +63,7 @@ You catch it, fix it, re-prompt, and something next to it breaks. The tokens are
63
63
  ## Features
64
64
 
65
65
  - **45 read-only tools** for schema, models, controllers, routes, views, Stimulus, Turbo, jobs, services, mailers, i18n, gems, config, tests, security, performance and more. Every answer comes from your app.
66
- - **Prism AST parsing** for model introspection. Each result carries `[VERIFIED]` or `[INFERRED]` so the assistant knows what is ground truth and what needs a runtime check.
66
+ - **Prism AST parsing** for model introspection. A model heading and each scope carry `[VERIFIED]` or `[INFERRED]` (`[STATIC]` without a boot) so the assistant knows what is ground truth and what needs a runtime check.
67
67
  - **Three ways in**: MCP over stdio, MCP mounted inside your Rails app over HTTP, or plain CLI in any terminal.
68
68
  - **Generated context files** for Claude Code, Cursor, GitHub Copilot, OpenCode and Codex CLI, with the MCP config each tool auto-detects on project open.
69
69
  - **Live resources**: `rails://` and `rails-ai-context://` URIs that introspect fresh on every read.
@@ -87,7 +87,7 @@ bundle add rails-ai-context --group development
87
87
  rails generate rails_ai_context:install
88
88
  ```
89
89
 
90
- The generator asks which AI tools you use and what to write, then creates the context files, the MCP config for each tool, and `config/initializers/rails_ai_context.rb`. Re-running it is safe; it keeps what you have and adds what is missing.
90
+ The generator asks which AI tools you use and what to write, then creates the context files, the MCP config for each tool, `config/initializers/rails_ai_context.rb` and `.rails-ai-context.yml`. In a git repo with no pre-commit hook it also offers to install one. An empty answer to the tools question selects all five, and `--defaults` takes the default for each question. Re-running it is safe; it keeps what you have and adds what is missing. Every step is in [What the install generator does](docs/GUIDE.md#what-the-install-generator-does).
91
91
 
92
92
  Keeping your own `CLAUDE.md` and `AGENTS.md`? `rails generate rails_ai_context:install --mcp-only` writes the MCP config and leaves every context file alone.
93
93
 
@@ -125,10 +125,12 @@ Then open the project in your AI tool. The MCP config it wrote is picked up on o
125
125
  The default. Each AI tool gets its own config file (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.codex/config.toml`) pointing at:
126
126
 
127
127
  ```bash
128
- rails ai:serve # in-Gemfile
129
- rails-ai-context serve # standalone
128
+ bundle exec rails-ai-context serve # in-Gemfile
129
+ rails-ai-context serve # standalone
130
130
  ```
131
131
 
132
+ `rails ai:serve` starts the same server from rake.
133
+
132
134
  ### MCP over HTTP
133
135
 
134
136
  Mount the server inside your app. It inherits your routing, auth and middleware, and needs no second process.
@@ -152,7 +154,7 @@ rails 'ai:tool[search_code]' pattern="publishable?" match_type=trace
152
154
  rails-ai-context tool schema --table users --detail full
153
155
  ```
154
156
 
155
- Tool names resolve loosely: `schema`, `get_schema` and `rails_get_schema` all work. Most tools take `detail=summary|standard|full`.
157
+ Tool names resolve loosely: `schema`, `get_schema` and `rails_get_schema` all work. About half the tools take `detail=summary|standard|full`.
156
158
 
157
159
  ### Commands
158
160
 
@@ -186,9 +188,9 @@ Every tool is read-only and answers from your app.
186
188
 
187
189
  A few worth knowing on day one:
188
190
 
189
- - `search_code` with `match_type=trace` returns definition, source, every caller grouped by type, and the tests, in one call. That replaces 4 to 5 file reads.
191
+ - `search_code` with `match_type=trace` returns definition, source, every caller grouped by file and labelled by type, and the tests, in one call. That replaces 4 to 5 file reads.
190
192
  - `get_controllers` returns the action source with inherited filters, strong params and the render map.
191
- - `get_model_details` returns associations, validations, scopes, enums and macros from the AST, each tagged `[VERIFIED]` or `[INFERRED]`.
193
+ - `get_model_details` returns associations, validations, scopes, enums and macros from the AST, with the model and each scope tagged `[VERIFIED]` or `[INFERRED]`.
192
194
  - `query` runs read-only SQL with a timeout, a row limit and column redaction. `read_logs` redacts sensitive data before it leaves the process.
193
195
 
194
196
  Parameters for all 45 are in the [tools reference](docs/TOOLS.md); worked examples in [recipes](docs/RECIPES.md).
@@ -234,7 +236,7 @@ Code is found in the conventional layout, in packwerk packs (`packs/*/app/*`), i
234
236
 
235
237
  ```ruby
236
238
  # config/initializers/rails_ai_context.rb
237
- if defined?(RailsAiContext)
239
+ if defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)
238
240
  RailsAiContext.configure do |config|
239
241
  config.ai_tools = %i[claude cursor] # which AI tools to generate for
240
242
  config.tool_mode = :mcp # :mcp (default) or :cli
@@ -254,8 +256,9 @@ Register your own tools next to the built-in ones:
254
256
  class RailsGetBusinessMetrics < MCP::Tool
255
257
  tool_name "rails_get_business_metrics"
256
258
  description "Key business metrics for this app"
259
+ input_schema(properties: { period: { type: "string" } })
257
260
 
258
- def call(period: "week")
261
+ def self.call(period: "week", server_context: nil)
259
262
  MCP::Tool::Response.new([{ type: "text", text: "Users this #{period}: #{User.recent.count}" }])
260
263
  end
261
264
  end
data/SECURITY.md CHANGED
@@ -21,11 +21,11 @@ If you discover a security vulnerability in rails-ai-context, please report it r
21
21
  - All 45 MCP tools are **read-only** and never modify your application or database.
22
22
  - **Sensitive file blocking** - configurable `sensitive_patterns` blocks access to `.env`, `*.key`, `*.pem`, `credentials.yml.enc` across all search and read tools. Patterns are checked in `rails_search_code`, `rails_get_edit_context`, and all new tools.
23
23
  - **Path traversal protection** - all file-reading tools validate paths with `File.realpath()` against `Rails.root` to prevent directory escape.
24
- - **Command injection prevention** - code search uses `Open3.capture2` with array arguments (never shell strings). The `--` flag separator prevents pattern injection.
25
- - **Regex DoS protection** - user-supplied regex patterns have 1-2 second timeouts via `Regexp.new(pattern, timeout:)`.
26
- - **Credential safety** - `rails_get_env` only reads `.env.example` (never `.env`), shows credential key names only (never values), and redacts secrets. `rails_get_config` exposes adapter/framework names, not connection strings.
24
+ - **Command injection prevention** - code search uses `Open3.capture3` with array arguments (never shell strings). The `--` flag separator prevents pattern injection.
25
+ - **Regex DoS protection** - user-supplied regex patterns have 1-2 second timeouts via `Regexp.new(pattern, timeout:)` on Ruby 3.2 and newer. Ruby 3.1 has no `Regexp.timeout`, so the timeout is skipped there.
26
+ - **Credential safety** - `rails_get_env` only reads `.env.example`, `.env.sample` or `.env.template` (never `.env`), shows credential key names only (never values), and redacts secrets. `rails_get_config` exposes adapter/framework names, not connection strings.
27
27
  - **Brakeman integration** - optional `rails_security_scan` tool runs static security analysis. Graceful degradation if not installed. Users can exclude it via `config.skip_tools = %w[rails_security_scan]`.
28
- - **File size limits** - all tools enforce configurable `max_file_size` (default 5MB) to prevent memory exhaustion on large files.
28
+ - **File size limits** - tools that read a whole file skip one over the configurable `max_file_size` (default 5MB) to prevent memory exhaustion on large files. `rails_read_logs` reads the last 1MB of a log instead, and `rails_search_code` caps its output with `max_search_results`.
29
29
  - **SQL comment stripping** - `rails_query` strips block (`/* */`), line (`--`), and MySQL-style (`#`) comments before validation to prevent keyword hiding.
30
30
  - **Regex interpolation safety** - all introspectors use `Regexp.escape` when interpolating model/association names into patterns to prevent regex injection.
31
31
  - **Log redaction** - `rails_read_logs` redacts passwords, tokens, secrets, API keys, cookies, session IDs, emails, and environment variables before output.
data/docs/ARCHITECTURE.md CHANGED
@@ -93,7 +93,7 @@ flowchart LR
93
93
  F --> G[Cache Result]
94
94
  G --> D
95
95
  D --> H[Hydrate\nSchema Hints]
96
- H --> I[Truncate\nmax_chars]
96
+ H --> I[Truncate\nmax_tool_response_chars]
97
97
  I --> J[MCP::Tool::Response]
98
98
  ```
99
99
 
@@ -132,10 +132,10 @@ Two more answer a question a tool asks:
132
132
 
133
133
  **Prism AST parsing** replaced all regex-based Ruby source parsing in v5.2.0.
134
134
 
135
- - **AstCache** - Thread-safe parse cache (`Concurrent::Map`), keyed by path + SHA256 + mtime; a stat match answers without a read for a file already two seconds older than the read that recorded it
135
+ - **AstCache** - Thread-safe parse cache (`Concurrent::Map`), keyed by the SHA256 of the file's content; a stat match answers without a read for a file already two seconds older than the read that recorded it
136
136
  - **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeding every registered listener simultaneously
137
- - **Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros and Methods are the default map for model analysis; the rest are used through targeted walks over schema dumps, migrations, Gemfiles, rake tasks and initializers, and `MethodCallListener` reports a named call with its arguments and options wherever one is asked for
138
- - **Confidence** - Every result carries `[VERIFIED]` (static literals) or `[INFERRED]` (dynamic expressions), and a record in a static-tier entry is capped at `[STATIC]`, since no record can claim more than the tier that carries it
137
+ - **Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros, Methods and Mixins are the default map for model analysis; the rest are used through targeted walks over schema dumps, migrations, Gemfiles, rake tasks and initializers, and `MethodCallListener` reports a named call with its arguments and options wherever one is asked for
138
+ - **Confidence** - A listener that records one tags each record `[VERIFIED]` (static literals) or `[INFERRED]` (dynamic expressions), and a record in a static-tier entry is capped at `[STATIC]`, since no record can claim more than the tier that carries it
139
139
 
140
140
  ### Tool Registry (`lib/rails_ai_context/tools/base_tool.rb`)
141
141
 
@@ -196,7 +196,7 @@ Result: controller and view tools automatically include relevant schema informat
196
196
  | `CopilotSerializer` | `.github/copilot-instructions.md` |
197
197
  | `CopilotInstructionsSerializer` | `.github/instructions/*.instructions.md` |
198
198
  | `OpencodeSerializer` | `AGENTS.md` (root) |
199
- | `OpencodeRulesSerializer` | `app/*/AGENTS.md` |
199
+ | `OpencodeRulesSerializer` | `app/models/AGENTS.md`, `app/controllers/AGENTS.md` |
200
200
  | `JsonSerializer` | `.ai-context.json` |
201
201
  | `MarkdownSerializer` | Base formatting |
202
202
  | `ContextFileSerializer` | Atomic file writes with section markers |
@@ -207,6 +207,7 @@ Result: controller and view tools automatically include relevant schema informat
207
207
  | `SectionFacts` | The facts every surface states about an app - auth, assets, associations, the filter chain, an unread entry's row, the static-tier notice - each rendered in one place |
208
208
  | `SectionMarkerWriter` | Writes a managed section into a file the user also owns |
209
209
  | `ContextModeDispatch` | Picks full or compact rendering for a run |
210
+ | `Base` | Holds the context hash a serializer renders |
210
211
 
211
212
  ### CLI (`exe/rails-ai-context`, `lib/rails_ai_context/cli/`)
212
213
 
@@ -219,7 +220,7 @@ Thor-based CLI that works standalone (no Gemfile entry):
219
220
 
220
221
  ### Path safety (`lib/rails_ai_context/safe_path.rb`, `lib/rails_ai_context/view_file.rb`)
221
222
 
222
- - **SafePath** - Resolves a caller-supplied path once, in a fixed refusal order: traversal, sensitive name, realpath, containment, sensitive realpath, file, size. `safe_glob` is the globbed-path form
223
+ - **SafePath** - Resolves a caller-supplied path once, in a fixed refusal order: traversal, sensitive name, realpath, containment, sensitive realpath, file, size. `BaseTool.safe_glob` is the globbed-path form, checking realpath, containment and the sensitive realpath on each path a pattern yields
223
224
  - **ViewFile** - Which file a view name means, and whether the app owns it, so no tool builds a template path itself
224
225
 
225
226
  ### Caching
@@ -237,7 +238,7 @@ Four cache layers:
237
238
 
238
239
  SHA256-based change detection:
239
240
 
240
- - Watches: `app/`, `config/`, `db/`, `lib/`, `rakelib/`, `test/`, `spec/`, Gemfile.lock, `config.ru`, the Rakefile
241
+ - Watches: `app/`, `config/`, `db/`, `lib/`, `rakelib/`, `test/`, `spec/`, the Gemfile and Gemfile.lock (or `gems.rb` and `gems.locked`), `package.json`, `tsconfig.json`, `config.ru`, the Rakefile
241
242
  - Computes a composite fingerprint from all watched files
242
243
  - Used by introspection cache and live reload to detect actual changes
243
244
 
@@ -245,9 +246,9 @@ SHA256-based change detection:
245
246
 
246
247
  1. **Official MCP SDK** - Not a custom protocol. Uses `mcp` gem's `MCP::Tool`, `MCP::Server`, transports.
247
248
  2. **Read-only tools** - All 45 tools annotated as non-destructive. Defense-in-depth for query tool.
248
- 3. **Graceful degradation** - Works without database (parses schema.rb as text), without Brakeman, without ripgrep, without listen gem.
249
- 4. **Zeitwerk autoloading** - Files loaded on-demand. No `require_relative` in the gem.
250
- 5. **Diff-aware generation** - Context file regeneration skips unchanged files using fingerprinting.
249
+ 3. **Graceful degradation** - Works without database (parses `schema.rb` or `structure.sql`, or replays the migrations), without Brakeman, without ripgrep, without listen gem.
250
+ 4. **Zeitwerk autoloading** - Files loaded on-demand. `require_relative` is kept for the few files that load before or outside the loader.
251
+ 5. **Diff-aware generation** - Context file regeneration skips a file whose content is unchanged, the timestamp line aside.
251
252
  6. **Section markers** - Root file content wrapped in `<!-- BEGIN/END rails-ai-context -->` to preserve user-added content.
252
253
 
253
254
  ## Dependencies
data/docs/CLI.md CHANGED
@@ -29,6 +29,7 @@ Start the MCP server.
29
29
 
30
30
  ```bash
31
31
  rails ai:serve # stdio (default)
32
+ rails ai:serve_http # HTTP transport
32
33
  rails-ai-context serve # stdio (default)
33
34
  rails-ai-context serve --transport http --port 6029 # HTTP transport
34
35
  ```
@@ -58,7 +59,8 @@ rails-ai-context tool model_details --model User
58
59
  | Option | Description |
59
60
  |:-------|:------------|
60
61
  | `--list` | List all available tools |
61
- | `--json` | Output as JSON |
62
+ | `--json` | Output as JSON (`-j` after the tool name) |
63
+ | `--help` | After the tool name, print that tool's parameters. Rake: `help=true` |
62
64
  | `--no-boot` | Skip booting the app; answer from source alone |
63
65
 
64
66
  Global options may be typed before or after the command name:
@@ -94,11 +96,15 @@ unanswered.
94
96
 
95
97
  A thing that is simply not there is an ordinary answer and exits 0: a file the
96
98
  tool looked for and did not find, a search with no matches, a directory the
97
- `--path` names that does not exist.
99
+ `--path` names that does not exist. A tool that needs a
100
+ booted app and is run with `--no-boot` (`query`, `runtime_info`) prints
101
+ `[UNAVAILABLE]` with the reason and also exits 0.
98
102
 
99
103
  A value the schema cannot hold - `--detail bogus` where the parameter takes
100
104
  `summary`, `standard` or `full`, or `--limit abc` where it takes an integer -
101
- is warned about on stderr and the tool's own default is used.
105
+ is warned about on stderr and the tool's own default is used. A required
106
+ parameter has no default, so a value outside its list (`--action bogus` on
107
+ `migration_advisor`) exits 1.
102
108
 
103
109
  ### The static tier and `--no-boot`
104
110
 
@@ -131,7 +137,7 @@ rails 'ai:tool[get_schema]'
131
137
  rails 'ai:tool[rails_get_schema]'
132
138
  ```
133
139
 
134
- Resolution order: exact match → `rails_` prefix → `rails_get_` prefix → `get_` prefix.
140
+ Resolution order: exact match → `rails_` prefix → `rails_get_` prefix → short name (the name with `rails_get_` or `rails_` removed).
135
141
 
136
142
  ### `context`
137
143
 
@@ -143,7 +149,9 @@ rails ai:context:claude # Claude only
143
149
  rails ai:context:cursor # Cursor only
144
150
  rails ai:context:copilot # Copilot only
145
151
  rails ai:context:opencode # OpenCode only
152
+ rails ai:context:codex # Codex only
146
153
  rails ai:context:json # JSON export
154
+ rails ai:context:full # All formats, full mode
147
155
 
148
156
  rails-ai-context context # All
149
157
  rails-ai-context context --format claude # Specific format
@@ -153,6 +161,10 @@ rails-ai-context context --format claude # Specific format
153
161
  |:-------|:--------|:------------|
154
162
  | `--format` | all configured | `claude`, `cursor`, `copilot`, `opencode`, `codex`, `json`, `all` |
155
163
 
164
+ The rake tasks `ai:context`, `ai:context:<format>` and `ai:context_for` also read
165
+ `CONTEXT_MODE`: `CONTEXT_MODE=full rails ai:context` overrides `config.context_mode`
166
+ for that run. The standalone binary does not read it.
167
+
156
168
  ### `doctor`
157
169
 
158
170
  Run the diagnostic checks and report an AI readiness score.
@@ -183,14 +195,16 @@ Interactive setup for standalone mode.
183
195
 
184
196
  ```bash
185
197
  rails-ai-context init
198
+ rails-ai-context init --mcp-only # MCP config only, no context files
186
199
  ```
187
200
 
188
- Asks which AI tools to configure and whether to use MCP or CLI mode. Creates `.rails-ai-context.yml` and MCP config files.
201
+ Asks which AI tools to configure and what to write: MCP config and context files, context files only (CLI mode), or MCP config only. Creates `.rails-ai-context.yml`, the MCP config files (except in CLI mode) and the context files (except with MCP config only). `--mcp-only` skips the second question.
189
202
 
190
203
  ### `version`
191
204
 
192
205
  ```bash
193
206
  rails-ai-context version
207
+ rails-ai-context --version # or -v
194
208
  ```
195
209
 
196
210
  ### `inspect`
@@ -199,6 +213,7 @@ Print introspection summary as JSON.
199
213
 
200
214
  ```bash
201
215
  rails-ai-context inspect
216
+ rails ai:inspect # short text summary instead of JSON
202
217
  ```
203
218
 
204
219
  ### `facts`
@@ -206,17 +221,19 @@ rails-ai-context inspect
206
221
  Print a schema facts summary: tables, associations and dependencies.
207
222
 
208
223
  ```bash
224
+ rails ai:facts
209
225
  rails-ai-context facts
210
226
  rails-ai-context facts --no-boot
211
227
  ```
212
228
 
213
229
  ### `preset`
214
230
 
215
- Run a named group of tools in one pass. With no name it lists the presets.
231
+ Run a named group of tools in one pass: `architecture`, `debugging` or `migration`. With no name it lists the presets.
216
232
 
217
233
  ```bash
218
234
  rails-ai-context preset
219
235
  rails-ai-context preset architecture
236
+ rails 'ai:preset[architecture]'
220
237
  ```
221
238
 
222
239
  ### `tree`
@@ -239,12 +256,18 @@ rails-ai-context tree
239
256
  | `rails ai:context:cursor` | Generate Cursor context |
240
257
  | `rails ai:context:copilot` | Generate Copilot context |
241
258
  | `rails ai:context:opencode` | Generate OpenCode context |
259
+ | `rails ai:context:codex` | Generate Codex context |
242
260
  | `rails ai:context:json` | Generate JSON export |
261
+ | `rails ai:context:full` | Generate all formats in full mode |
262
+ | `rails 'ai:context_for[claude]'` | Generate one format by name |
243
263
  | `rails ai:serve` | Start MCP server (stdio) |
264
+ | `rails ai:serve_http` | Start MCP server (HTTP) |
244
265
  | `rails ai:tool` | List tools or run a tool |
245
266
  | `rails ai:doctor` | Run diagnostics |
246
267
  | `rails ai:watch` | Watch mode |
247
- | `rails ai:setup` | Interactive setup (alternative to generator) |
268
+ | `rails ai:inspect` | Print introspection summary |
269
+ | `rails ai:facts` | Print schema facts summary |
270
+ | `rails 'ai:preset[name]'` | Run a preset; no name lists them |
248
271
 
249
272
  ---
250
273
 
@@ -259,6 +282,7 @@ rails 'ai:tool[tool_name]' key=value key2=value2
259
282
  - Strings: `table=users`
260
283
  - Booleans: `explain=true` or `explain=false`
261
284
  - Enums: `detail=full`
285
+ - Arrays: `files=a.rb,b.rb`
262
286
  - Spaces: `pattern="has_many :posts"`
263
287
 
264
288
  ### Thor format
@@ -270,6 +294,7 @@ rails-ai-context tool tool_name --key value --key2 value2
270
294
  - Strings: `--table users` or `--table=users`
271
295
  - Booleans: `--explain` (true) or `--no-explain` (false)
272
296
  - Enums: `--detail full`
297
+ - Arrays: `--files a.rb b.rb` or `--files a.rb,b.rb`
273
298
  - Spaces: `--pattern "has_many :posts"`
274
299
 
275
300
  ### JSON output
@@ -278,6 +303,7 @@ Add `--json` for machine-readable output:
278
303
 
279
304
  ```bash
280
305
  rails-ai-context tool schema --table users --json
306
+ JSON=1 rails 'ai:tool[schema]' table=users
281
307
  ```
282
308
 
283
309
  ---