rails-ai-context 5.35.0 → 5.35.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1024 -0
- data/CONTEXT.md +37 -7
- data/CONTRIBUTING.md +30 -1
- data/README.md +9 -9
- data/SECURITY.md +2 -2
- data/docs/ARCHITECTURE.md +12 -4
- data/docs/CLI.md +44 -15
- data/docs/COMPATIBILITY.md +34 -8
- data/docs/CONFIGURATION.md +28 -5
- data/docs/CUSTOM_TOOLS.md +8 -2
- data/docs/FAQ.md +5 -5
- data/docs/GUIDE.md +33 -65
- data/docs/INTROSPECTORS.md +4 -4
- data/docs/QUICKSTART.md +1 -1
- data/docs/RECIPES.md +5 -5
- data/docs/RELEASING.md +3 -1
- data/docs/SECURITY.md +255 -74
- data/docs/SETUP.md +1 -1
- data/docs/STANDALONE.md +2 -2
- data/docs/TOOLS.md +151 -90
- data/docs/TROUBLESHOOTING.md +123 -8
- data/docs/_config.yml +1 -1
- data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
- data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
- data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
- data/docs/index.md +2 -2
- data/exe/rails-ai-context +24 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
- data/lib/rails_ai_context/app_databases.rb +377 -0
- data/lib/rails_ai_context/app_kind.rb +2 -0
- data/lib/rails_ai_context/cli/app_root.rb +1 -0
- data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
- data/lib/rails_ai_context/configuration.rb +15 -8
- data/lib/rails_ai_context/database_family/mysql_runtime.rb +170 -0
- data/lib/rails_ai_context/database_family/postgresql_plan.rb +124 -0
- data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
- data/lib/rails_ai_context/database_family/sqlite_runtime.rb +391 -0
- data/lib/rails_ai_context/database_family.rb +282 -0
- data/lib/rails_ai_context/database_file.rb +61 -7
- data/lib/rails_ai_context/database_yml.rb +7 -29
- data/lib/rails_ai_context/doctor.rb +15 -16
- data/lib/rails_ai_context/instrumentation.rb +1 -1
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
- data/lib/rails_ai_context/introspectors/declared_constant.rb +19 -7
- data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
- data/lib/rails_ai_context/introspectors/job_introspector.rb +7 -7
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
- data/lib/rails_ai_context/introspectors/model_introspector.rb +33 -39
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +33 -30
- data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +80 -24
- data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
- data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
- data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
- data/lib/rails_ai_context/migration_status.rb +18 -46
- data/lib/rails_ai_context/payload.rb +48 -1
- data/lib/rails_ai_context/pending_migrations.rb +3 -4
- data/lib/rails_ai_context/run_cache.rb +5 -0
- data/lib/rails_ai_context/schema_adapter.rb +13 -18
- data/lib/rails_ai_context/secret_columns.rb +506 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -10
- data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
- data/lib/rails_ai_context/server.rb +143 -29
- data/lib/rails_ai_context/sql_gate.rb +1007 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
- data/lib/rails_ai_context/test_helper.rb +3 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
- data/lib/rails_ai_context/tools/base_tool.rb +71 -48
- data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +105 -42
- data/lib/rails_ai_context/tools/generate_test.rb +4 -6
- data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
- data/lib/rails_ai_context/tools/get_api.rb +1 -6
- data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
- data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
- data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
- data/lib/rails_ai_context/tools/get_concern.rb +6 -6
- data/lib/rails_ai_context/tools/get_config.rb +189 -12
- data/lib/rails_ai_context/tools/get_context.rb +111 -105
- data/lib/rails_ai_context/tools/get_controllers.rb +8 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
- data/lib/rails_ai_context/tools/get_engines.rb +1 -6
- data/lib/rails_ai_context/tools/get_env.rb +1 -6
- data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
- data/lib/rails_ai_context/tools/get_gems.rb +1 -6
- data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
- data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
- data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
- data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
- data/lib/rails_ai_context/tools/get_schema.rb +10 -8
- data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
- data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
- data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
- data/lib/rails_ai_context/tools/onboard.rb +24 -24
- data/lib/rails_ai_context/tools/performance_check.rb +15 -2
- data/lib/rails_ai_context/tools/query.rb +142 -1356
- data/lib/rails_ai_context/tools/read_logs.rb +1 -1
- data/lib/rails_ai_context/tools/review_changes.rb +6 -7
- data/lib/rails_ai_context/tools/runtime_info.rb +125 -88
- data/lib/rails_ai_context/tools/safe_call.rb +70 -28
- data/lib/rails_ai_context/tools/search_code.rb +0 -4
- data/lib/rails_ai_context/tools/search_docs.rb +3 -1
- data/lib/rails_ai_context/tools/security_scan.rb +9 -5
- data/lib/rails_ai_context/tools/session_context.rb +18 -12
- data/lib/rails_ai_context/tools/validate.rb +1 -0
- data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +4 -7
- data/server.json +2 -2
- metadata +14 -3
|
@@ -4,6 +4,7 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetApi < BaseTool
|
|
6
6
|
tool_name "rails_get_api"
|
|
7
|
+
section_renderer!
|
|
7
8
|
description "Returns the app's API layer: api_only mode, serialization strategy (Jbuilder templates, " \
|
|
8
9
|
"serializer classes), GraphQL schema size, API versioning, rate limiting, CORS origins, pagination gems, " \
|
|
9
10
|
"and OpenAPI specs. " \
|
|
@@ -18,12 +19,6 @@ module RailsAiContext
|
|
|
18
19
|
}
|
|
19
20
|
)
|
|
20
21
|
|
|
21
|
-
guide_row(
|
|
22
|
-
order: 30,
|
|
23
|
-
mcp: "rails_get_api",
|
|
24
|
-
summary: "API layer: api_only mode, serializers, GraphQL, versioning, rate limiting, CORS, pagination"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
22
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
28
23
|
|
|
29
24
|
# The name each section key goes by in the one-liner.
|
|
@@ -4,6 +4,7 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetAutoload < BaseTool
|
|
6
6
|
tool_name "rails_get_autoload"
|
|
7
|
+
section_renderer!
|
|
7
8
|
description "Get autoloading setup: Zeitwerk vs Classic mode, autoloaders with collapsed/ignored dirs, autoload/eager-load paths, and custom inflections. " \
|
|
8
9
|
"Use when: debugging NameError/unloaded-constant issues, naming a file or class to match Zeitwerk rules, or checking custom inflections."
|
|
9
10
|
|
|
@@ -13,12 +14,6 @@ module RailsAiContext
|
|
|
13
14
|
|
|
14
15
|
input_schema(properties: {})
|
|
15
16
|
|
|
16
|
-
guide_row(
|
|
17
|
-
order: 43,
|
|
18
|
-
mcp: "rails_get_autoload",
|
|
19
|
-
summary: "Zeitwerk mode, autoload/eager-load paths, collapsed dirs, custom inflections"
|
|
20
|
-
)
|
|
21
|
-
|
|
22
17
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
23
18
|
|
|
24
19
|
def self.call(server_context: nil)
|
|
@@ -90,14 +90,15 @@ module RailsAiContext
|
|
|
90
90
|
callbacks = data[:callbacks] || {}
|
|
91
91
|
association_lines = association_callback_lines(data)
|
|
92
92
|
if callbacks.empty? && association_lines.empty?
|
|
93
|
-
|
|
93
|
+
hint = next_hint(pointer("rails_get_model_details", %(model:"#{name}"), "for full model detail."))
|
|
94
|
+
return [ "# #{name}", "No callbacks defined.", hint ].compact.join("\n\n")
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
lines = [ "# #{name} - Callbacks", "" ]
|
|
97
98
|
if callbacks.empty?
|
|
98
99
|
lines << "No record callbacks defined."
|
|
99
100
|
lines.concat(association_section(association_lines))
|
|
100
|
-
lines
|
|
101
|
+
add_next_hint(lines, pointer("rails_get_model_details", %(model:"#{name}"), "for associations and validations"))
|
|
101
102
|
return lines.join("\n")
|
|
102
103
|
end
|
|
103
104
|
|
|
@@ -150,9 +151,8 @@ module RailsAiContext
|
|
|
150
151
|
lines.concat(association_section(association_lines))
|
|
151
152
|
|
|
152
153
|
# Cross-reference hints
|
|
153
|
-
lines
|
|
154
|
-
|
|
155
|
-
lines << " | `rails_get_concern(name:\"ConcernName\")` for concern-provided callbacks_"
|
|
154
|
+
add_next_hint(lines, pointer("rails_get_model_details", %(model:"#{name}"), "for associations and validations"),
|
|
155
|
+
pointer("rails_get_concern", %(name:"ConcernName"), "for concern-provided callbacks"), lines: true)
|
|
156
156
|
|
|
157
157
|
lines.join("\n")
|
|
158
158
|
end
|
|
@@ -28,7 +28,7 @@ module RailsAiContext
|
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
guide_row(
|
|
31
|
-
order:
|
|
31
|
+
order: 22,
|
|
32
32
|
mcp: "rails_get_component_catalog(component:\"X\")",
|
|
33
33
|
cli_args: "component=X",
|
|
34
34
|
summary: "ViewComponent/Phlex: props, slots, previews, usage"
|
|
@@ -66,12 +66,11 @@ module RailsAiContext
|
|
|
66
66
|
note = api_only_note("app/components", dir: "app/components")
|
|
67
67
|
return text_response(note) if note
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
69
|
+
tries = [ pointer("rails_get_partial_interface", %(partial:"shared/partial_name"), "- partial locals contract + usage"),
|
|
70
|
+
pointer("rails_get_view", %(controller:"name"), "- view templates with partial/Stimulus references") ].compact
|
|
71
|
+
text = "No components found in app/components/."
|
|
72
|
+
text += "\n\nThis app may use ERB partials instead of ViewComponent/Phlex. Try:\n#{tries.map { |t| "- #{t}" }.join("\n")}" if tries.any?
|
|
73
|
+
return text_response(text)
|
|
75
74
|
end
|
|
76
75
|
text_response(render_catalog(components, data[:summary], detail, offset: offset, limit: limit, bases: data[:bases]))
|
|
77
76
|
end
|
|
@@ -230,7 +230,7 @@ module RailsAiContext
|
|
|
230
230
|
lines << "" << "_No model or concern in app/models wires this validator._"
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
lines
|
|
233
|
+
add_next_hint(lines, pointer("rails_search_code", %(pattern:"#{name.demodulize.camelize}"), "for every use"))
|
|
234
234
|
return text_response(lines.join("\n"))
|
|
235
235
|
end
|
|
236
236
|
|
|
@@ -244,15 +244,15 @@ module RailsAiContext
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
# Cross-reference hints
|
|
247
|
-
|
|
248
|
-
case concern_type
|
|
247
|
+
hint = case concern_type
|
|
249
248
|
when "model"
|
|
250
|
-
|
|
249
|
+
next_hint(pointer("rails_get_model_details", %(model:"ModelName"), "for models using this concern"))
|
|
251
250
|
when "controller"
|
|
252
|
-
|
|
251
|
+
next_hint(pointer("rails_get_controllers", %(controller:"ControllerName"), "for controllers using this concern"))
|
|
253
252
|
else
|
|
254
|
-
|
|
253
|
+
next_hint(pointer("rails_search_code", %(pattern:"include #{name.demodulize.camelize}"), "for everything using this concern"))
|
|
255
254
|
end
|
|
255
|
+
lines << "" << hint if hint
|
|
256
256
|
|
|
257
257
|
text_response(lines.join("\n"))
|
|
258
258
|
end
|
|
@@ -1,24 +1,200 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module RailsAiContext
|
|
4
6
|
module Tools
|
|
5
7
|
class GetConfig < BaseTool
|
|
6
8
|
tool_name "rails_get_config"
|
|
7
|
-
description "Get Rails app configuration: cache store, session store, timezone, queue adapter, custom middleware, initializers. " \
|
|
8
|
-
"Use when: configuring caching, checking session/queue setup, or seeing what initializers exist. " \
|
|
9
|
-
"No parameters needed. Returns non-default middleware and all initializers."
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
# One part of the answer. `tool_name` is the tool that answered it, a retired
|
|
11
|
+
# name for every section but app; skip_tools turns the section off by it.
|
|
12
|
+
Section = Data.define(:name, :tool_name, :params, :summary) do
|
|
13
|
+
def app?
|
|
14
|
+
name == "app"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The class that renders it; the app section is GetConfig's own.
|
|
18
|
+
def renderer
|
|
19
|
+
BaseTool.tool_class(tool_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on?(config = RailsAiContext.configuration)
|
|
23
|
+
!config.skip_tools.include?(tool_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# On, and rails_get_config is the gem's own tool, with no custom one in its place.
|
|
27
|
+
def served?(config = RailsAiContext.configuration)
|
|
28
|
+
on?(config) && RailsAiContext::Server.served_tool("rails_get_config", config).equal?(GetConfig)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def off_message
|
|
32
|
+
"The `#{name}` section of rails_get_config is turned off in this app: skip_tools lists #{tool_name} " \
|
|
33
|
+
"(.rails-ai-context.yml or config/initializers/rails_ai_context.rb). Take it out of skip_tools to use it."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
SECTIONS = [
|
|
38
|
+
Section.new(name: "app", tool_name: "rails_get_config", params: [],
|
|
39
|
+
summary: "database, auth, assets, cache, session, queue, Action Cable, middleware, initializers"),
|
|
40
|
+
Section.new(name: "gems", tool_name: "rails_get_gems", params: %i[category offset limit], summary: "notable gems by category"),
|
|
41
|
+
Section.new(name: "frontend", tool_name: "rails_get_frontend_stack", params: %i[detail],
|
|
42
|
+
summary: "JS framework, build tool, TypeScript, package manager"),
|
|
43
|
+
Section.new(name: "api", tool_name: "rails_get_api", params: %i[detail],
|
|
44
|
+
summary: "serializers, GraphQL, versioning, CORS, rate limiting, pagination"),
|
|
45
|
+
Section.new(name: "engines", tool_name: "rails_get_engines", params: [], summary: "mounted engines and Rack apps"),
|
|
46
|
+
Section.new(name: "conventions", tool_name: "rails_get_conventions", params: [], summary: "architecture and patterns"),
|
|
47
|
+
Section.new(name: "autoload", tool_name: "rails_get_autoload", params: [], summary: "Zeitwerk paths, inflections"),
|
|
48
|
+
Section.new(name: "active_support", tool_name: "rails_get_active_support", params: [],
|
|
49
|
+
summary: "concerns, notifications, on_load hooks"),
|
|
50
|
+
Section.new(name: "env", tool_name: "rails_get_env", params: %i[detail],
|
|
51
|
+
summary: "names of environment variables and credentials keys"),
|
|
52
|
+
Section.new(name: "env_config", tool_name: "rails_get_env_config", params: %i[environment offset limit],
|
|
53
|
+
summary: "config/environments/*.rb per environment"),
|
|
54
|
+
Section.new(name: "i18n", tool_name: "rails_get_i18n", params: %i[locale offset limit], summary: "locales, locale files, coverage")
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def self.section_named(name)
|
|
58
|
+
SECTIONS.find { |section| section.name == name.to_s.downcase }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The section a retired tool name stands for, or nil.
|
|
62
|
+
def self.retired_section(tool_name)
|
|
63
|
+
SECTIONS.find { |section| !section.app? && section.tool_name == tool_name.to_s }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.sections_on(config = RailsAiContext.configuration)
|
|
67
|
+
SECTIONS.select { |section| section.on?(config) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Each parameter's schema, less the description that names its sections.
|
|
71
|
+
PARAM_SCHEMAS = {
|
|
72
|
+
detail: [ {}, "summary, standard (default) or full." ],
|
|
73
|
+
category: [
|
|
74
|
+
{ type: "string", enum: Introspectors::GemIntrospector::NOTABLE_GEMS.values.map { |gem| gem[:category].to_s }.uniq + %w[all] },
|
|
75
|
+
"only this category. Default: all."
|
|
76
|
+
],
|
|
77
|
+
environment: [ { type: "string" }, "only this environment (e.g. \"production\")." ],
|
|
78
|
+
locale: [ { type: "string" }, "only this locale (e.g. \"fr\")." ],
|
|
79
|
+
offset: [ { type: "integer" }, "skip this many entries. Default: 0." ],
|
|
80
|
+
limit: [ { type: "integer" }, "max entries. Default: 50." ]
|
|
81
|
+
}.freeze
|
|
12
82
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
83
|
+
# The schema over the given sections: a parameter none of them reads is
|
|
84
|
+
# left out, and `section` is required once app, its default, is not among them.
|
|
85
|
+
def self.schema_for(sections, any_section: false)
|
|
86
|
+
default = sections.any?(&:app?)
|
|
87
|
+
about = default ? "Default: app." : "Required: the app section is turned off in this app."
|
|
88
|
+
properties = { section: { type: "string", enum: sections.map(&:name), description: "Which section to answer. #{about}" } }
|
|
89
|
+
properties[:section].delete(:enum) if any_section
|
|
90
|
+
PARAM_SCHEMAS.each do |param, (schema, text)|
|
|
91
|
+
readers = sections.select { |section| section.params.include?(param) }
|
|
92
|
+
next if readers.empty?
|
|
93
|
+
|
|
94
|
+
text = "#{readers.map(&:name).join(', ')}: #{text}"
|
|
95
|
+
properties[param] = param == :detail ? RailsAiContext::DetailLevel.schema(text) : schema.merge(description: text)
|
|
96
|
+
end
|
|
97
|
+
default ? { properties: properties } : { properties: properties, required: [ "section" ] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.description_for(sections)
|
|
101
|
+
listed = sections.map { |section| "#{section.name} (#{'default: ' if section.app?}#{section.summary})" }
|
|
102
|
+
"Get the app's configuration, one section per call. section#{' (required)' if sections.none?(&:app?)}: #{listed.join(', ')}."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The schema that validates a call takes any section and every parameter, so `call` gives
|
|
106
|
+
# the one refusal for a section that is off or unknown; tools/list publishes the sections that are on.
|
|
107
|
+
VALIDATING_SCHEMA = ArgumentSchema.new(schema_for(SECTIONS, any_section: true))
|
|
108
|
+
description(description_for(SECTIONS))
|
|
109
|
+
|
|
110
|
+
def self.input_schema(*args)
|
|
111
|
+
args.empty? ? VALIDATING_SCHEMA : super
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.input_schema_value
|
|
115
|
+
MCP::Tool::InputSchema.new(schema_for(sections_on))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.description_value
|
|
119
|
+
description_for(sections_on)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The row calls the default section, or the first one on when app is off.
|
|
123
|
+
def self.guide_row
|
|
124
|
+
app, others = sections_on.partition(&:app?)
|
|
125
|
+
names = others.map(&:name).join(", ")
|
|
126
|
+
summary = [ ("App config (database, auth, assets, cache, queue)" if app.any?),
|
|
127
|
+
("`section` picks #{app.any? ? 'another' : 'one'}: #{names}" unless names.empty?) ].compact.join("; ")
|
|
128
|
+
first = others.first unless app.any?
|
|
129
|
+
GuideRow.new(order: 20, mcp: first ? %(rails_get_config(section:"#{first.name}")) : "rails_get_config",
|
|
130
|
+
cli_args: ("section=#{first.name}" if first), summary: summary)
|
|
131
|
+
end
|
|
18
132
|
|
|
19
133
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
20
134
|
|
|
21
|
-
def self.call(server_context: nil)
|
|
135
|
+
def self.call(section: nil, detail: nil, category: nil, offset: nil, limit: nil, environment: nil, locale: nil, server_context: nil)
|
|
136
|
+
on = sections_on.map(&:name).join(", ")
|
|
137
|
+
return error_response(RailsAiContext::Server.skipped_tool_message(tool_name)) if on.empty?
|
|
138
|
+
|
|
139
|
+
asked = section_named(section.nil? ? "app" : section)
|
|
140
|
+
return error_response("`section` does not take '#{echo_input(section)}'. It takes: #{on}.") unless asked
|
|
141
|
+
return error_response(section.nil? ? section_required_message : "#{asked.off_message} The sections that are on: #{on}.") unless asked.on?
|
|
142
|
+
|
|
143
|
+
given = { detail: detail, category: category, offset: offset, limit: limit, environment: environment, locale: locale }.compact
|
|
144
|
+
response = asked.app? ? app_section : render(asked, given)
|
|
145
|
+
append_note(response, unread_note(asked.name, given.keys - asked.params))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A retired name's answer: its section's, ending with the call to make
|
|
149
|
+
# instead, which carries the arguments the section reads. A refusal ends as it is.
|
|
150
|
+
def self.with_retired_note(response, section, arguments)
|
|
151
|
+
response.error? ? response : append_note(response, retired_note(section, arguments))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The refusal of a parameter no section reads in a retired name's call, naming its own section's; or nil.
|
|
155
|
+
def self.retired_refusal(section, arguments)
|
|
156
|
+
unknown = unknown_param_names(arguments.keys, input_schema.to_h[:properties])
|
|
157
|
+
unknown_param_response(unknown, section.params.map(&:to_s)) if unknown.any?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A value goes into the call only when it reads the same in a code span, an MCP call and a shell command.
|
|
161
|
+
CALL_SAFE = /\A[\w.-]+\z/
|
|
162
|
+
|
|
163
|
+
# Every other value, and one too long to echo whole, is named after the call.
|
|
164
|
+
def self.retired_note(section, arguments)
|
|
165
|
+
spanned, outside = arguments.slice(*section.params).partition do |_, value|
|
|
166
|
+
value.is_a?(Integer) || value == true || value == false || (value.is_a?(String) && value.match?(CALL_SAFE) && echo_input(value) == value)
|
|
167
|
+
end
|
|
168
|
+
read = spanned.map { |key, value| "#{key}:#{JSON.generate(value)}" }.join(", ")
|
|
169
|
+
call = pointer(section.tool_name, read)
|
|
170
|
+
same = ", with the same #{outside.map { |key, _| "`#{key}`" }.join(' and ')}" if outside.any?
|
|
171
|
+
cli_form("\n\n_#{section.tool_name} is now #{call}#{same}; call that instead._") if call
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The renderer answers as a tool another tool called, so an answered call is recorded here.
|
|
175
|
+
private_class_method def self.render(section, given)
|
|
176
|
+
section.renderer.call(**given.slice(*section.params)).tap { |response| record_call(response_text(response)) unless response.error? }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private_class_method def self.section_required_message
|
|
180
|
+
"`section` is required in this app: skip_tools lists rails_get_config, which turns the default `app` section off. " \
|
|
181
|
+
"It takes: #{sections_on.map(&:name).join(', ')}."
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Only a detail the caller sent is normalized, so a section that does
|
|
185
|
+
# not read it can say so.
|
|
186
|
+
def self.normalize_detail(kwargs)
|
|
187
|
+
kwargs[:detail].nil? ? kwargs : super
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private_class_method def self.unread_note(section, unread)
|
|
191
|
+
return nil if unread.empty?
|
|
192
|
+
|
|
193
|
+
names = unread.map { |name| "`#{name}`" }.join(", ")
|
|
194
|
+
"\n\n_The `#{section}` section does not read #{names}, so #{unread.size > 1 ? 'they were' : 'it was'} ignored._"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private_class_method def self.app_section
|
|
22
198
|
if static_refusal_for(:config) && (rackup = rackup_lines).any?
|
|
23
199
|
return text_response(([ unavailable_text ] + rackup).join("\n"))
|
|
24
200
|
end
|
|
@@ -197,8 +373,9 @@ module RailsAiContext
|
|
|
197
373
|
env = Rails.configuration.database_configuration&.dig(Rails.env) rescue nil
|
|
198
374
|
return nil unless env.is_a?(Hash)
|
|
199
375
|
|
|
200
|
-
# A multi-database environment nests one block per database;
|
|
201
|
-
|
|
376
|
+
# A multi-database environment nests one block per database; the first is the app's own.
|
|
377
|
+
adapter = env["adapter"] || env.values.find { |v| v.is_a?(Hash) }&.dig("adapter")
|
|
378
|
+
adapter && SchemaAdapter.display(adapter.to_s)
|
|
202
379
|
end
|
|
203
380
|
|
|
204
381
|
# The auth section's rule, read from the files when the section did not
|