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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -0
  3. data/README.md +6 -0
  4. data/docs/CLI.md +1 -1
  5. data/docs/COMPATIBILITY.md +10 -0
  6. data/docs/GUIDE.md +1 -1
  7. data/docs/PROMPTS.md +3 -3
  8. data/docs/SETUP.md +191 -0
  9. data/docs/STANDALONE.md +1 -1
  10. data/docs/TROUBLESHOOTING.md +6 -2
  11. data/exe/rails-ai-context +53 -20
  12. data/lib/generators/rails_ai_context/install/install_generator.rb +8 -4
  13. data/lib/rails_ai_context/boot_manager.rb +7 -1
  14. data/lib/rails_ai_context/claude_code_settings.rb +1006 -0
  15. data/lib/rails_ai_context/cli/app_root.rb +126 -16
  16. data/lib/rails_ai_context/cli/entry_boot.rb +58 -6
  17. data/lib/rails_ai_context/configuration.rb +116 -34
  18. data/lib/rails_ai_context/doctor.rb +478 -23
  19. data/lib/rails_ai_context/gem_lock.rb +14 -7
  20. data/lib/rails_ai_context/initializer_settings.rb +4 -1
  21. data/lib/rails_ai_context/install/claude_plugin_removal.rb +107 -0
  22. data/lib/rails_ai_context/install/program.rb +133 -6
  23. data/lib/rails_ai_context/install/selection_record.rb +11 -5
  24. data/lib/rails_ai_context/introspectors/gemfile_gems.rb +2 -2
  25. data/lib/rails_ai_context/mcp_config_generator.rb +18 -6
  26. data/lib/rails_ai_context/redaction.rb +101 -14
  27. data/lib/rails_ai_context/resources.rb +15 -0
  28. data/lib/rails_ai_context/secret_columns.rb +1 -1
  29. data/lib/rails_ai_context/server.rb +6 -1
  30. data/lib/rails_ai_context/tools/safe_call.rb +5 -0
  31. data/lib/rails_ai_context/tools/search_docs.rb +2 -1
  32. data/lib/rails_ai_context/update_check.rb +17 -4
  33. data/lib/rails_ai_context/version.rb +1 -1
  34. data/lib/rails_ai_context.rb +26 -0
  35. 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
- files = "#{tool.mcp_config[:path]} #{where}; #{files}" if tool_mode == :mcp
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
- say_next_steps(ai_tools)
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
- ::RailsAiContext::Install::Program.next_steps(ai_tools).each { |line| $stderr.puts " #{line}" }
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
- $stderr.puts " #{tool.name.ljust(16)} -> #{context_files ? tool.files_label(mcp: tool_mode == :mcp) : 'MCP config only'}"
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
- Result.new(status: :failed, error: BootExitError.new("App called exit(#{e.status}) during boot"))
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