rails-ai-context 5.36.8 → 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 +125 -0
- data/README.md +6 -0
- data/docs/CLI.md +1 -1
- data/docs/COMPATIBILITY.md +10 -0
- data/docs/GUIDE.md +1 -1
- data/docs/PROMPTS.md +3 -3
- data/docs/SETUP.md +191 -0
- data/docs/STANDALONE.md +1 -1
- 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 +58 -6
- data/lib/rails_ai_context/configuration.rb +116 -34
- data/lib/rails_ai_context/doctor.rb +478 -23
- data/lib/rails_ai_context/gem_lock.rb +14 -7
- data/lib/rails_ai_context/initializer_settings.rb +4 -1
- 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/gemfile_gems.rb +2 -2
- data/lib/rails_ai_context/mcp_config_generator.rb +18 -6
- data/lib/rails_ai_context/redaction.rb +101 -14
- data/lib/rails_ai_context/resources.rb +15 -0
- data/lib/rails_ai_context/secret_columns.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -1
- data/lib/rails_ai_context/tools/safe_call.rb +5 -0
- 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 +3 -1
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
|