rails-ai-context 5.36.7 → 5.36.9
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 +220 -0
- data/README.md +6 -0
- data/docs/CLI.md +1 -1
- data/docs/COMPATIBILITY.md +19 -0
- data/docs/CONFIGURATION.md +1 -1
- data/docs/GUIDE.md +1 -1
- data/docs/PROMPTS.md +3 -3
- data/docs/SECURITY.md +37 -2
- data/docs/SETUP.md +191 -0
- data/docs/STANDALONE.md +2 -2
- data/docs/TOOLS.md +5 -5
- data/docs/TROUBLESHOOTING.md +6 -2
- data/exe/rails-ai-context +53 -20
- data/lib/generators/rails_ai_context/install/install_generator.rb +8 -4
- data/lib/rails_ai_context/boot_manager.rb +7 -1
- data/lib/rails_ai_context/claude_code_settings.rb +1006 -0
- data/lib/rails_ai_context/cli/app_root.rb +126 -16
- data/lib/rails_ai_context/cli/entry_boot.rb +60 -6
- data/lib/rails_ai_context/configuration.rb +116 -34
- data/lib/rails_ai_context/doctor.rb +482 -24
- data/lib/rails_ai_context/gem_lock.rb +14 -7
- data/lib/rails_ai_context/initializer_settings.rb +603 -0
- data/lib/rails_ai_context/install/claude_plugin_removal.rb +107 -0
- data/lib/rails_ai_context/install/program.rb +133 -6
- data/lib/rails_ai_context/install/selection_record.rb +11 -5
- data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
- data/lib/rails_ai_context/introspectors/gemfile_gems.rb +2 -2
- data/lib/rails_ai_context/mcp_config_generator.rb +18 -6
- data/lib/rails_ai_context/redaction.rb +749 -26
- data/lib/rails_ai_context/resources.rb +15 -0
- data/lib/rails_ai_context/safe_path.rb +101 -11
- data/lib/rails_ai_context/secret_columns.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -1
- data/lib/rails_ai_context/sql_gate.rb +5 -0
- data/lib/rails_ai_context/tools/get_env.rb +20 -30
- data/lib/rails_ai_context/tools/get_partial_interface.rb +12 -3
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/review_changes.rb +74 -29
- data/lib/rails_ai_context/tools/safe_call.rb +5 -0
- data/lib/rails_ai_context/tools/search_code.rb +24 -8
- data/lib/rails_ai_context/tools/search_docs.rb +2 -1
- data/lib/rails_ai_context/update_check.rb +17 -4
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +26 -0
- metadata +4 -1
data/docs/STANDALONE.md
CHANGED
|
@@ -60,7 +60,7 @@ rails-ai-context version # Show version
|
|
|
60
60
|
1. **Loads only its boot shim** before the app - the binary requires the two files it needs to boot Rails, then `config/environment.rb`, and requires the gem itself only after the boot returns. Nothing it loads first is a gem an app pins: it reads the Gemfile and `config/boot.rb` with Ruby's own Ripper, not the prism gem, and loads json only once the app's bundle is set up
|
|
61
61
|
2. **Restores `$LOAD_PATH`** entries that `Bundler.setup` strips (since the gem isn't in the Gemfile). The gem's own dependencies come off the load path for the duration of the boot, so every gem the app locks resolves to the app's pin, and go back behind it afterwards
|
|
62
62
|
3. **YAML config** - uses `.rails-ai-context.yml` instead of a Ruby initializer
|
|
63
|
-
4. **Steps aside for the app's own copy** - in an app whose bundle carries its own rails-ai-context (another installed version, or a `path:` copy), a command that boots the app runs through that copy, as `bundle exec rails-ai-context` would, and says so on stderr. That is the copy the app's MCP configs start, and two copies in one process would each load half the gem. The bundle is the one `BUNDLE_GEMFILE` names when you set it (a dual boot's `Gemfile.next`), and a command typed in a subdirectory runs from the app root it found. `init` asks its questions itself and has that copy write only the context files; an MCP-only setup, chosen with `--mcp-only` or at the prompt, runs nothing of the app's. So does `--no-boot`: bundle exec would evaluate the app's Gemfile and start whatever copy the app names, which on a repo just cloned is code nobody has read. A bundle not installed yet (`bundle check` fails) is left alone too: its boot fails, and the static tier answers. A copy installed
|
|
63
|
+
4. **Steps aside for the app's own copy** - in an app whose bundle carries its own rails-ai-context (another installed version, or a `path:` copy), a command that boots the app runs through that copy, as `bundle exec rails-ai-context` would, and says so on stderr. That is the copy the app's MCP configs start, and two copies in one process would each load half the gem. The bundle is the one `BUNDLE_GEMFILE` names when you set it (a dual boot's `Gemfile.next`), and a command typed in a subdirectory runs from the app root it found. `init` asks its questions itself and has that copy write only the context files; an MCP-only setup, chosen with `--mcp-only` or at the prompt, runs nothing of the app's. So does `--no-boot`: bundle exec would evaluate the app's Gemfile and start whatever copy the app names, which on a repo just cloned is code nobody has read. A bundle not installed yet (`bundle check` fails) is left alone too: its boot fails, and the static tier answers. A copy installed under the bundle's own path (`bundle config set path vendor/bundle`, or a deployment bundle) is handed the run as well. A copy from git is not seen from outside the bundle, so there the installed copy runs and the app's boot loads the bundle's copy over it: in such an app, start the server with `bundle exec rails-ai-context`.
|
|
64
64
|
|
|
65
65
|
This means you get the same 35 tools, same MCP server, same context generation - without touching the project's Gemfile.
|
|
66
66
|
|
|
@@ -117,7 +117,7 @@ A key the gem does not know warns on stderr and is ignored; the rest of the file
|
|
|
117
117
|
|
|
118
118
|
### Precedence
|
|
119
119
|
|
|
120
|
-
In standalone mode `.rails-ai-context.yml` is the only config source. The gem is not loaded while `config/initializers` runs, so a `config/initializers/rails_ai_context.rb` contributes nothing: the generated file, which guards on `defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`, is a silent no-op, and one without that guard raises `NoMethodError` (the binary opens the `RailsAiContext` namespace before the boot, so the constant exists but `configure` is not defined on it) and drops the command into the static tier.
|
|
120
|
+
In standalone mode `.rails-ai-context.yml` is the only config source of a booted run. The gem is not loaded while `config/initializers` runs, so a `config/initializers/rails_ai_context.rb` contributes nothing there: the generated file, which guards on `defined?(RailsAiContext) && RailsAiContext.respond_to?(:configure)`, is a silent no-op, and one without that guard raises `NoMethodError` (the binary opens the `RailsAiContext` namespace before the boot, so the constant exists but `configure` is not defined on it) and drops the command into the static tier. On the static tier the gem reads the literal `sensitive_patterns`, `excluded_paths` and `skip_tools` values such a file writes, without running it ([SECURITY.md](SECURITY.md#configuration)).
|
|
121
121
|
|
|
122
122
|
For in-Gemfile installs both sources apply and merge key by key - see [Precedence](CONFIGURATION.md#precedence).
|
|
123
123
|
|
data/docs/TOOLS.md
CHANGED
|
@@ -77,7 +77,7 @@ Search your codebase with regex, ripgrep acceleration, and sensitive file blocki
|
|
|
77
77
|
| Parameter | Type | Default | Description |
|
|
78
78
|
|:----------|:-----|:--------|:------------|
|
|
79
79
|
| `pattern` | string | *required* | Regex pattern to search for. With `definition` or `class`, a plain method or constant name matches literally (`valid?`), anything else as a regex (`generate_.*`) |
|
|
80
|
-
| `path` | string | - | Subdirectory or file to search (relative to Rails root); a file is searched whatever its type or ignore files say, as ripgrep does, unless it is sensitive |
|
|
80
|
+
| `path` | string | - | Subdirectory or file to search (relative to Rails root); a file is searched whatever its type or ignore files say, as ripgrep does, unless it is sensitive or over `max_file_size` (`File too large`). A path through a link inside the app is searched as the folder it leads to |
|
|
81
81
|
| `match_type` | enum | `any` | `any`, `definition` (def lines whose method name contains the pattern), `class` (class/module lines whose name contains it), `call`, `trace` |
|
|
82
82
|
| `file_type` | string | - | Filter by extension (`rb`, `erb`, `js`, etc.) |
|
|
83
83
|
| `exact_match` | boolean | `false` | Literal, whole-word match. `def reblog?` does not match `def reblog` |
|
|
@@ -566,7 +566,7 @@ ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEn
|
|
|
566
566
|
|
|
567
567
|
#### `env` (was `rails_get_env`)
|
|
568
568
|
|
|
569
|
-
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`. A tag on a YAML comment line is not read: ERB runs it, but what it writes goes with the comment. Config YAML matching `sensitive_patterns` (`config/database.yml`, `config/cable.yml`, `config/storage.yml` and the rest) is read only for the ENV names in its ERB tags, so a default there is listed as present and not read; credentials and keys are never read, and the answer says so. A default is read from a second argument or from a block whose one statement is a literal (`ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`). The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address,
|
|
569
|
+
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`. A tag on a YAML comment line is not read: ERB runs it, but what it writes goes with the comment. Config YAML matching `sensitive_patterns` (`config/database.yml`, `config/cable.yml`, `config/storage.yml` and the rest) is read only for the ENV names in its ERB tags, so a default there is listed as present and not read; credentials and keys are never read, and the answer says so. A default is read from a second argument or from a block whose one statement is a literal (`ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`). The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. A Dockerfile `ENV` or `ARG` value, an `.env.example` value and a default in code read `[FILTERED]` when their shape can carry a secret: a URL whose user part is a key or holds a password, a webhook's address, a token in a query, a key-like run of characters, or a `BUNDLE_<HOST>` gem server credential ([Credential handling](SECURITY.md#credential-handling)). An `.env.example` value in quotes keeps a `#`, and an inline comment is hidden when it holds a credential or one of those shapes. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address, a hostname or a URL of none of the shapes above prints as written.
|
|
570
570
|
|
|
571
571
|
#### `env_config` (was `rails_get_env_config`)
|
|
572
572
|
|
|
@@ -782,13 +782,13 @@ Each database other than the primary is reached through a connection handler of
|
|
|
782
782
|
|
|
783
783
|
> Disabled in production by default.
|
|
784
784
|
|
|
785
|
-
> Name the columns you need. A query is refused, with a message that says why, when it does one of these: it references a secret column (directly, or through an alias or expression; an `encrypts` column also when `query_allowed_columns` lists it); it serialises a whole row; it carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed, since it renames no table column); it uses UNION; it calls a session-effecting function, a function that returns a whole table as one value (`table_to_xml`), or any function by a quoted name; it reads a statistics, session or credential relation (`pg_stats`, `pg_stat_activity`, `pg_settings`, `performance_schema`, the `mysql` schema) or a PostgreSQL setting that can hold a password (`primary_conninfo`); it is a `SHOW` with a `WHERE` clause, `SHOW ALL` on PostgreSQL or `EXPLAIN FOR CONNECTION` on MySQL and MariaDB; or it is over 1,000,000 bytes long. `SELECT *` is allowed and its secret columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format. With `csv`, a query that returns no rows answers the header row alone.
|
|
785
|
+
> Name the columns you need. A query is refused, with a message that says why, when it does one of these: it references a secret column (directly, or through an alias or expression; an `encrypts` column also when `query_allowed_columns` lists it); it serialises a whole row; it carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed, since it renames no table column); it uses UNION; it calls a session-effecting function, a function that returns a whole table as one value (`table_to_xml`), or any function by a quoted name; it reads a statistics, session or credential relation (`pg_stats`, `pg_stat_activity`, `pg_settings`, `performance_schema`, the `mysql` schema) or a PostgreSQL setting that can hold a password (`primary_conninfo`); it is a `SHOW` with a `WHERE` clause, `SHOW ALL` on PostgreSQL, or `EXPLAIN FOR CONNECTION` or a `PROCEDURE` clause on MySQL and MariaDB; or it is over 1,000,000 bytes long. `SELECT *` is allowed and its secret columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format. With `csv`, a query that returns no rows answers the header row alone.
|
|
786
786
|
|
|
787
787
|
> **Cell rendering by format.** A `markdown` table cell shows `NULL` as `_NULL_`, escapes a pipe (`a\|b`) so it cannot end the cell, renders a line break as the two characters `\n` so it cannot end the row, and shortens a value over 100 characters to `…` (a `SHOW CREATE` definition is not a table cell, so it is never shortened). A `csv` cell is the value itself: `NULL` is an empty field, nothing is escaped beyond RFC 4180 quoting (a field holding a comma, a quote or a line break is wrapped in quotes with its quotes doubled), and nothing is truncated - so a line break stays a real line break inside the quoted field. Binary (non-text) data shows as `[BLOB]` in both.
|
|
788
788
|
|
|
789
789
|
### `rails_read_logs`
|
|
790
790
|
|
|
791
|
-
Reverse file tail with level filtering and sensitive data redaction.
|
|
791
|
+
Reverse file tail with level filtering and sensitive data redaction. A value an SQL statement holds inline is filtered when the column it meets is a secret, and so are the values a database error repeats ([Redaction](SECURITY.md#redaction)).
|
|
792
792
|
|
|
793
793
|
| Parameter | Type | Default | Description |
|
|
794
794
|
|:----------|:-----|:--------|:------------|
|
|
@@ -822,7 +822,7 @@ A changed migration lists the tables it affects: the table each schema method ta
|
|
|
822
822
|
|
|
823
823
|
An app in a subfolder of its repository (a monorepo's `backend/`) is reviewed on its own: its changed files, named from the app root as `rails_validate` takes them, and the commits that touched it.
|
|
824
824
|
|
|
825
|
-
Each diff
|
|
825
|
+
Each diff line is shown as the line of its side, redacted with the whole of that side the way every tool's source is, so a secret a change adds or takes out reads `[FILTERED]`, and so does a key's body in a hunk that opens inside the key, in a hunk header's line too. A changed file on `sensitive_patterns` (`config/database.yml`, `.env`, a key) is listed with its line counts and none of its lines, and so is one whose diff is over `max_file_size` and a merge conflict's combined diff. The diff is read with the repository's colour, external diff and textconv settings off, and a submodule's change shows as the commits it moves between.
|
|
826
826
|
|
|
827
827
|
| Parameter | Type | Default | Description |
|
|
828
828
|
|:----------|:-----|:--------|:------------|
|
data/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -127,6 +127,8 @@ Codex reads `config.toml` as TOML, which is UTF-8, and loads nothing from a file
|
|
|
127
127
|
- `can't find executable rails-ai-context for gem rails-ai-context`: the bundle's copy of the gem lists no executable, as a `path:` copy of 5.32.2 or earlier outside a git checkout does. Point the Gemfile at a git checkout or a release.
|
|
128
128
|
- On Windows, a config that runs `bundle` or `rails-ai-context` itself rather than through `cmd /c`: those are batch files there, which Claude Code, Codex and the other clients that start a server without a shell do not run (`doctor` warns about it). Run the install again; on Windows it writes `cmd /c bundle exec rails-ai-context serve`.
|
|
129
129
|
- On macOS or Linux, a config that runs `cmd /c ...`: it was written on Windows, and there is no `cmd` here (`doctor` fails it). Run the install again on this machine, and keep the MCP configs out of git if the team works on both.
|
|
130
|
+
- The Claude Code plugin's server connects with no tools: it tells the session why (no gem for its Ruby, an older one, a Ruby too old, several apps or none) and what to run; `claude --debug` shows the same line as its stderr.
|
|
131
|
+
- The Claude Code plugin's server is listed as failed in `/mcp`, and `claude --debug` shows a `Bundler::GemNotFound` trace in its stderr: Claude Code was started from `bundle exec`, or from a shell holding the `RUBYOPT` or `BUNDLER_SETUP` it sets, for a bundle not installed for the plugin's Ruby, which fails before the plugin runs. Start Claude Code outside `bundle exec`, or unset those two where a shell profile sets them. After a failed start Claude Code skips the server in new sessions for about 15 minutes, so reconnect it in `/mcp` once the cause is fixed.
|
|
130
132
|
|
|
131
133
|
Check for Bundler/Ruby path issues:
|
|
132
134
|
|
|
@@ -148,8 +150,10 @@ command gives it 60. Start the service, or, for an app that is healthy but
|
|
|
148
150
|
slow to boot, raise both limits in the server's MCP entry: the boot's with
|
|
149
151
|
`RAILS_AI_CONTEXT_BOOT_TIMEOUT` (seconds) in its `env`, and the client's with
|
|
150
152
|
its own setting (Claude Code reads `MCP_TIMEOUT`, in milliseconds). Raising
|
|
151
|
-
only the boot's leaves a client that gives up first.
|
|
152
|
-
|
|
153
|
+
only the boot's leaves a client that gives up first. The Claude Code plugin's
|
|
154
|
+
entry is the plugin's own: set both in Claude Code's settings `env` (user,
|
|
155
|
+
project or local) or in the shell that starts Claude Code. `rails ai:serve`
|
|
156
|
+
has no static tier, so it exits with the same message instead.
|
|
153
157
|
|
|
154
158
|
### A server set up for a folder of apps fails to start
|
|
155
159
|
|
data/exe/rails-ai-context
CHANGED
|
@@ -311,12 +311,13 @@ class RailsAiContextCLI < Thor
|
|
|
311
311
|
# gets less than that.
|
|
312
312
|
if options[:transport] == "stdio"
|
|
313
313
|
entry_boot
|
|
314
|
+
require_relative "../lib/rails_ai_context/boot_manager"
|
|
314
315
|
::RailsAiContext::BootManager.default_timeout = ::RailsAiContext::BootManager::STDIO_TIMEOUT
|
|
315
316
|
end
|
|
316
317
|
|
|
317
318
|
# Live reload uses `listen` when it is there, standalone included.
|
|
318
319
|
reach_installed_gem("listen")
|
|
319
|
-
boot_rails!(command: "serve", allow_static: true)
|
|
320
|
+
boot_rails!(command: "serve", allow_static: true, client: options[:transport] == "stdio")
|
|
320
321
|
|
|
321
322
|
transport = options[:transport].to_sym
|
|
322
323
|
# No Thor default: it would overwrite the config file's http_port.
|
|
@@ -503,7 +504,8 @@ class RailsAiContextCLI < Thor
|
|
|
503
504
|
program.remove_mcp_entries(program_surface, unwired, root: Dir.pwd)
|
|
504
505
|
program.remove_context_files(program_surface, ai_tools, root: Dir.pwd) if stale
|
|
505
506
|
record_selection(ai_tools, tool_mode, context_files)
|
|
506
|
-
write_mcp_configs(ai_tools, tool_mode)
|
|
507
|
+
written = write_mcp_configs(ai_tools, tool_mode)
|
|
508
|
+
held = written[:held] || []
|
|
507
509
|
add_to_gitignore(context_files)
|
|
508
510
|
|
|
509
511
|
if bundled
|
|
@@ -519,7 +521,7 @@ class RailsAiContextCLI < Thor
|
|
|
519
521
|
end
|
|
520
522
|
|
|
521
523
|
# --- Instructions ---
|
|
522
|
-
show_standalone_instructions(ai_tools, tool_mode, context_files, removed: stale)
|
|
524
|
+
show_standalone_instructions(ai_tools, tool_mode, context_files, removed: stale, held: held, plugin_off: written[:plugin_off])
|
|
523
525
|
end
|
|
524
526
|
|
|
525
527
|
desc "uninstall", "Remove what rails-ai-context wrote: context files, MCP entries, its config, .gitignore lines and pre-commit hook"
|
|
@@ -541,6 +543,9 @@ class RailsAiContextCLI < Thor
|
|
|
541
543
|
bundled = app_roots.any? { |app| bundled_app?(app) }
|
|
542
544
|
where = display_place || (resolution.workspace? ? "this folder" : "this app")
|
|
543
545
|
$stderr.puts ""
|
|
546
|
+
require_relative "../lib/rails_ai_context/install/claude_plugin_removal"
|
|
547
|
+
::RailsAiContext::Install::ClaudePluginRemoval.lines(app_roots, cwd: @app_root_result&.cwd || Dir.pwd, workspace: resolution.workspace?)
|
|
548
|
+
.each { |line| $stderr.puts line }
|
|
544
549
|
if changed
|
|
545
550
|
$stderr.puts "rails-ai-context is uninstalled from #{where}. To remove the gem itself:"
|
|
546
551
|
else
|
|
@@ -742,8 +747,10 @@ class RailsAiContextCLI < Thor
|
|
|
742
747
|
def write_mcp_configs(ai_tools, tool_mode, root: Dir.pwd, servers: nil)
|
|
743
748
|
standalone = servers ? nil : ::RailsAiContext::InstallMode.standalone?(root: root)
|
|
744
749
|
in_starting_env do
|
|
750
|
+
there = run_there(root).delete_prefix(" run in ")
|
|
745
751
|
::RailsAiContext::Install::Program.write_mcp_configs(
|
|
746
|
-
program_surface, tools: ai_tools, tool_mode: tool_mode, root: root, servers: servers, standalone: standalone
|
|
752
|
+
program_surface, tools: ai_tools, tool_mode: tool_mode, root: root, servers: servers, standalone: standalone,
|
|
753
|
+
place: (there unless there.empty?)
|
|
747
754
|
)
|
|
748
755
|
end
|
|
749
756
|
end
|
|
@@ -827,7 +834,8 @@ class RailsAiContextCLI < Thor
|
|
|
827
834
|
end
|
|
828
835
|
|
|
829
836
|
$stderr.puts ""
|
|
830
|
-
write_mcp_configs(ai_tools, tool_mode, root: dir, servers: configured.map(&:server))
|
|
837
|
+
written = write_mcp_configs(ai_tools, tool_mode, root: dir, servers: configured.map(&:server))
|
|
838
|
+
held = written[:held] || []
|
|
831
839
|
# This folder's own .gitignore, when it has one: the Codex config here
|
|
832
840
|
# embeds this machine's PATH and GEM_HOME too.
|
|
833
841
|
add_to_gitignore(false, root: dir)
|
|
@@ -845,7 +853,7 @@ class RailsAiContextCLI < Thor
|
|
|
845
853
|
$stderr.puts "MCP-only setup: no context files written."
|
|
846
854
|
end
|
|
847
855
|
|
|
848
|
-
show_workspace_instructions(dir, apps, ai_tools, tool_mode, context_files, configured: configured)
|
|
856
|
+
show_workspace_instructions(dir, apps, ai_tools, tool_mode, context_files, configured: configured, held: held, plugin_off: written[:plugin_off])
|
|
849
857
|
return if failed.empty?
|
|
850
858
|
|
|
851
859
|
$stderr.puts "Error: no context files for #{failed.map { |app| "#{app.path}/" }.join(', ')} - see the output above. " \
|
|
@@ -927,7 +935,7 @@ class RailsAiContextCLI < Thor
|
|
|
927
935
|
"= #{spec.version}" ], false ]
|
|
928
936
|
end
|
|
929
937
|
|
|
930
|
-
def show_workspace_instructions(dir, apps, ai_tools, tool_mode, context_files, configured: apps)
|
|
938
|
+
def show_workspace_instructions(dir, apps, ai_tools, tool_mode, context_files, configured: apps, held: [], plugin_off: false)
|
|
931
939
|
$stderr.puts ""
|
|
932
940
|
$stderr.puts "=" * 50
|
|
933
941
|
$stderr.puts " rails-ai-context initialized for #{apps.size == 1 ? '1 app' : "#{apps.size} apps"}"
|
|
@@ -940,10 +948,16 @@ class RailsAiContextCLI < Thor
|
|
|
940
948
|
|
|
941
949
|
# The MCP config is this folder's, so it leaves the per-app list.
|
|
942
950
|
files = context_files ? "#{tool.files_label(mcp: false)} in each app" : "no context files"
|
|
943
|
-
|
|
951
|
+
# A committed plugin serves where no server was written, and the app's own .mcp.json its own sessions.
|
|
952
|
+
if tool_mode == :mcp
|
|
953
|
+
served = held.include?(tool.key) ? plugin_label(plugin_off) : "#{tool.mcp_config[:path]} #{where}"
|
|
954
|
+
own = configured.one? && held.include?(tool.key) && ::RailsAiContext::Install::Program.app_mcp_json(dir, configured.first.root)
|
|
955
|
+
served += ", and #{configured.first.path}/.mcp.json for sessions started in #{configured.first.path}" if own
|
|
956
|
+
files = "#{served}; #{files}"
|
|
957
|
+
end
|
|
944
958
|
$stderr.puts " #{tool.name.ljust(16)} -> #{files}"
|
|
945
959
|
end
|
|
946
|
-
if tool_mode == :mcp && configured.any?
|
|
960
|
+
if tool_mode == :mcp && configured.any? && (ai_tools - held).any?
|
|
947
961
|
$stderr.puts ""
|
|
948
962
|
$stderr.puts "MCP servers, one per app:"
|
|
949
963
|
width = configured.map { |app| app.server_name.size }.max
|
|
@@ -959,7 +973,8 @@ class RailsAiContextCLI < Thor
|
|
|
959
973
|
first = app_root.command_line([ app_root.option_path(app_root.display(example.root, Dir.pwd)) ])
|
|
960
974
|
if tool_mode == :mcp && configured.any?
|
|
961
975
|
$stderr.puts ""
|
|
962
|
-
|
|
976
|
+
# A folder holding one app is one the plugin serves.
|
|
977
|
+
say_next_steps(ai_tools, root: (dir if configured.one?), app: configured.first&.root)
|
|
963
978
|
end
|
|
964
979
|
$stderr.puts ""
|
|
965
980
|
$stderr.puts "Commands, from here:"
|
|
@@ -972,18 +987,32 @@ class RailsAiContextCLI < Thor
|
|
|
972
987
|
$stderr.puts ""
|
|
973
988
|
end
|
|
974
989
|
|
|
990
|
+
# Where a command has to run, when that is not where this one was typed.
|
|
991
|
+
def run_there(folder)
|
|
992
|
+
cwd = @app_root_result&.cwd or return ""
|
|
993
|
+
return "" if ::RailsAiContext::SafePath.canonical(folder) == ::RailsAiContext::SafePath.canonical(cwd)
|
|
994
|
+
|
|
995
|
+
" run in #{app_root.display(folder, cwd)}"
|
|
996
|
+
end
|
|
997
|
+
|
|
975
998
|
# Whether the app's bundle starts the gem. An app with no Gemfile has no
|
|
976
999
|
# bundle at all, which InstallMode, unable to read one, counts as bundled.
|
|
977
1000
|
def bundled_app?(root)
|
|
978
1001
|
File.file?(File.join(root.to_s, "Gemfile")) && !::RailsAiContext::InstallMode.standalone?(root: root)
|
|
979
1002
|
end
|
|
980
1003
|
|
|
981
|
-
def say_next_steps(ai_tools)
|
|
1004
|
+
def say_next_steps(ai_tools, root: nil, app: root)
|
|
982
1005
|
$stderr.puts "Next step, before the MCP tools appear:"
|
|
983
|
-
|
|
1006
|
+
place = root && run_there(root).sub(" run in ", "in ")
|
|
1007
|
+
::RailsAiContext::Install::Program.next_steps(ai_tools, root: root, place: place.to_s.empty? ? "here" : place, app: app).each { |line| $stderr.puts " #{line}" }
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# The next step names what serves instead where the committed plugin does not start for this user.
|
|
1011
|
+
def plugin_label(off)
|
|
1012
|
+
off ? "the rails-ai-context plugin (off here: see below)" : "the rails-ai-context plugin"
|
|
984
1013
|
end
|
|
985
1014
|
|
|
986
|
-
def show_standalone_instructions(ai_tools, tool_mode, context_files = true, removed: false)
|
|
1015
|
+
def show_standalone_instructions(ai_tools, tool_mode, context_files = true, removed: false, held: [], plugin_off: false)
|
|
987
1016
|
$stderr.puts ""
|
|
988
1017
|
$stderr.puts "=" * 50
|
|
989
1018
|
$stderr.puts " rails-ai-context initialized!"
|
|
@@ -992,7 +1021,9 @@ class RailsAiContextCLI < Thor
|
|
|
992
1021
|
$stderr.puts "Your setup:"
|
|
993
1022
|
::RailsAiContext::Install::AiTool.all.each do |tool|
|
|
994
1023
|
next unless ai_tools.include?(tool.key)
|
|
995
|
-
|
|
1024
|
+
files = context_files ? tool.files_label(mcp: tool_mode == :mcp) : "MCP config only"
|
|
1025
|
+
files = context_files ? "#{plugin_label(plugin_off)}; #{files}" : plugin_label(plugin_off) if held.include?(tool.key)
|
|
1026
|
+
$stderr.puts " #{tool.name.ljust(16)} -> #{files}"
|
|
996
1027
|
end
|
|
997
1028
|
unless context_files || removed
|
|
998
1029
|
$stderr.puts ""
|
|
@@ -1015,7 +1046,7 @@ class RailsAiContextCLI < Thor
|
|
|
1015
1046
|
$stderr.puts " #{binary} doctor # Check AI readiness"
|
|
1016
1047
|
$stderr.puts ""
|
|
1017
1048
|
if tool_mode == :mcp
|
|
1018
|
-
say_next_steps(ai_tools)
|
|
1049
|
+
say_next_steps(ai_tools, root: Dir.pwd)
|
|
1019
1050
|
else
|
|
1020
1051
|
$stderr.puts "CLI tools:"
|
|
1021
1052
|
$stderr.puts " AI agents can run `rails-ai-context tool schema table=users` directly."
|
|
@@ -1112,9 +1143,9 @@ class RailsAiContextCLI < Thor
|
|
|
1112
1143
|
# A command that needs more than the tier gives it says so here rather
|
|
1113
1144
|
# than being recognised by name.
|
|
1114
1145
|
def boot_rails!(command:, allow_static: false, no_boot: false, allow_source_only: allow_static,
|
|
1115
|
-
boot_failure_hint: nil)
|
|
1146
|
+
boot_failure_hint: nil, client: false)
|
|
1116
1147
|
outcome = enter_app(command: command, allow_static: allow_static, no_boot: no_boot,
|
|
1117
|
-
allow_source_only: allow_source_only)
|
|
1148
|
+
allow_source_only: allow_source_only, client: client)
|
|
1118
1149
|
outcome.messages.each { |line| $stderr.puts line }
|
|
1119
1150
|
return unless outcome.tier == :absent
|
|
1120
1151
|
|
|
@@ -1125,7 +1156,7 @@ class RailsAiContextCLI < Thor
|
|
|
1125
1156
|
|
|
1126
1157
|
# The app this command reads, booted or read from source as the command
|
|
1127
1158
|
# allows. Prints nothing: the outcome carries the lines to say.
|
|
1128
|
-
def enter_app(command:, allow_static:, no_boot:, allow_source_only:)
|
|
1159
|
+
def enter_app(command:, allow_static:, no_boot:, allow_source_only:, client: false)
|
|
1129
1160
|
# Read by the hand-off, which resolving the root may start: `tool`
|
|
1130
1161
|
# takes --no-boot after the tool's name, where Thor never sees it.
|
|
1131
1162
|
@no_boot = true if no_boot
|
|
@@ -1141,7 +1172,8 @@ class RailsAiContextCLI < Thor
|
|
|
1141
1172
|
# A command that can serve the static tier reads a source-only tree
|
|
1142
1173
|
# instead of refusing it.
|
|
1143
1174
|
entry_boot.call(root: Dir.pwd, allow_static: allow_static, no_boot: no_boot || options[:no_boot],
|
|
1144
|
-
allow_source_only: allow_source_only, command: command, doctor: doctor_command, shown: shown_root
|
|
1175
|
+
allow_source_only: allow_source_only, command: command, doctor: doctor_command, shown: shown_root,
|
|
1176
|
+
client: client)
|
|
1145
1177
|
end
|
|
1146
1178
|
|
|
1147
1179
|
# Which app this command reads, decided once and before anything reads
|
|
@@ -1194,6 +1226,7 @@ class RailsAiContextCLI < Thor
|
|
|
1194
1226
|
warning = app_root.bundle_warning(result.root, cwd) if entry_boot.app_present?(result.root, allow_source_only: true)
|
|
1195
1227
|
$stderr.puts warning if warning
|
|
1196
1228
|
hand_off_to_bundle!(result.root, cwd) if hand_off
|
|
1229
|
+
@start_update_check&.pinned! if app_root.pinned?(result.root)
|
|
1197
1230
|
result
|
|
1198
1231
|
end
|
|
1199
1232
|
|
|
@@ -1377,7 +1410,7 @@ class RailsAiContextCLI < Thor
|
|
|
1377
1410
|
%w[
|
|
1378
1411
|
gem_lock install/ai_tool install/selection_record install/cleanup
|
|
1379
1412
|
install/surface install/validation_hook install/program install/uninstall mcp_config_generator install/workspace
|
|
1380
|
-
legacy_cleanup
|
|
1413
|
+
legacy_cleanup claude_code_settings
|
|
1381
1414
|
].each { |file| require_relative "../lib/rails_ai_context/#{file}" }
|
|
1382
1415
|
|
|
1383
1416
|
# Both are defined in the entry file, which is the thing that must not
|
|
@@ -100,11 +100,13 @@ module RailsAiContext
|
|
|
100
100
|
# itself the generator reads Rails.root's, which is an engine's dummy
|
|
101
101
|
# app.
|
|
102
102
|
def create_mcp_config
|
|
103
|
-
RailsAiContext::Install::Program.write_mcp_configs(
|
|
103
|
+
written = RailsAiContext::Install::Program.write_mcp_configs(
|
|
104
104
|
program_surface,
|
|
105
105
|
tools: @selected_formats, tool_mode: @tool_mode, root: install_root,
|
|
106
106
|
standalone: RailsAiContext::InstallMode.standalone?(root: install_root)
|
|
107
107
|
)
|
|
108
|
+
@held = written[:held]
|
|
109
|
+
@plugin_off = written[:plugin_off]
|
|
108
110
|
program = RailsAiContext::Install::Program
|
|
109
111
|
unless context_files?
|
|
110
112
|
@removed_context_files = program.ask_context_removal(program_surface, tools: @selected_formats, root: install_root,
|
|
@@ -801,6 +803,8 @@ module RailsAiContext
|
|
|
801
803
|
RailsAiContext::Install::AiTool.all.each do |tool|
|
|
802
804
|
next unless @selected_formats.include?(tool.key)
|
|
803
805
|
files = context_files? ? tool.files_label(mcp: tool_mode == :mcp) : "MCP config only"
|
|
806
|
+
plugin = @plugin_off ? "the rails-ai-context plugin (off here: see below)" : "the rails-ai-context plugin"
|
|
807
|
+
files = context_files? ? "#{plugin}; #{files}" : plugin if Array(@held).include?(tool.key)
|
|
804
808
|
say " ✅ #{tool.name.ljust(16)} -> #{files}"
|
|
805
809
|
end
|
|
806
810
|
unless context_files? || @removed_context_files
|
|
@@ -816,7 +820,7 @@ module RailsAiContext
|
|
|
816
820
|
say ""
|
|
817
821
|
if @tool_mode == :mcp
|
|
818
822
|
say "Next step, before the MCP tools appear:", :yellow
|
|
819
|
-
RailsAiContext::Install::Program.next_steps(@selected_formats).each { |line| say " #{line}" }
|
|
823
|
+
RailsAiContext::Install::Program.next_steps(@selected_formats, root: install_root).each { |line| say " #{line}" }
|
|
820
824
|
else
|
|
821
825
|
say "CLI tools:", :yellow
|
|
822
826
|
example = engine_root ? "bundle exec rails-ai-context tool schema --table users" : "rails 'ai:tool[schema]' table=users"
|
|
@@ -825,8 +829,8 @@ module RailsAiContext
|
|
|
825
829
|
end
|
|
826
830
|
say ""
|
|
827
831
|
show_more_tools
|
|
828
|
-
committed = [ ("context files" if context_files?), ("MCP configs" if tool_mode == :mcp) ].compact
|
|
829
|
-
say "Commit the #{committed.join(' and ')} so your team benefits!", :green
|
|
832
|
+
committed = [ ("context files" if context_files?), ("MCP configs" if tool_mode == :mcp && (@selected_formats - Array(@held)).any?) ].compact
|
|
833
|
+
say "Commit the #{committed.join(' and ')} so your team benefits!", :green if committed.any?
|
|
830
834
|
# Only where a Codex config was written, and only while a commit
|
|
831
835
|
# would still take it along.
|
|
832
836
|
if tool_mode == :mcp && @selected_formats.include?(:codex) &&
|
|
@@ -138,7 +138,13 @@ module RailsAiContext
|
|
|
138
138
|
|
|
139
139
|
Result.new(status: :failed, error: BootError.new("config/environment.rb loaded but defined no Rails application"))
|
|
140
140
|
rescue SystemExit => e
|
|
141
|
-
|
|
141
|
+
# Bundler in a terminal exits on a missing gem; keeping its error as the cause lets callers name it.
|
|
142
|
+
error = begin
|
|
143
|
+
raise BootExitError, "App called exit(#{e.status}) during boot", cause: e.cause
|
|
144
|
+
rescue BootExitError => raised
|
|
145
|
+
raised
|
|
146
|
+
end
|
|
147
|
+
Result.new(status: :failed, error: error)
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
# The same three protections for callers that boot through their own
|