rails-agent-kit 0.1.0 → 0.2.0
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 +62 -2
- data/README.md +275 -59
- data/exe/rails-agent-kit +94 -4
- data/lib/rails_agent_kit/client_config.rb +147 -0
- data/lib/rails_agent_kit/routes_inspector.rb +22 -4
- data/lib/rails_agent_kit/ruby_env.rb +174 -0
- data/lib/rails_agent_kit/schema_inspector.rb +125 -26
- data/lib/rails_agent_kit/server.rb +24 -8
- data/lib/rails_agent_kit/skill_installer.rb +105 -19
- data/lib/rails_agent_kit/version.rb +1 -1
- data/lib/rails_agent_kit.rb +2 -0
- data/skills/rails-8-conventions/SKILL.md +3 -2
- metadata +13 -9
|
@@ -3,21 +3,72 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module RailsAgentKit
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Installs the agent skills bundled with the gem (skills/<name>/SKILL.md) into a Rails app.
|
|
7
|
+
#
|
|
8
|
+
# Agents with skill folders get a copy of each SKILL.md:
|
|
9
|
+
#
|
|
10
|
+
# cursor -> .cursor/skills/<name>/SKILL.md
|
|
11
|
+
# claude -> .claude/skills/<name>/SKILL.md
|
|
12
|
+
#
|
|
13
|
+
# Agents that read a single instructions file get the skills rendered into a managed block between
|
|
14
|
+
# MARKER_START and MARKER_END. Content outside the block is never touched:
|
|
15
|
+
#
|
|
16
|
+
# codex -> AGENTS.md (also read by Cursor, Copilot, Windsurf, Zed and others)
|
|
17
|
+
# copilot -> .github/copilot-instructions.md
|
|
18
|
+
# gemini -> GEMINI.md
|
|
8
19
|
class SkillInstaller
|
|
9
20
|
SKILLS_DIR = File.expand_path("../../skills", __dir__)
|
|
10
|
-
|
|
21
|
+
SKILL_DIRS = {
|
|
11
22
|
"cursor" => ".cursor/skills",
|
|
12
23
|
"claude" => ".claude/skills"
|
|
13
24
|
}.freeze
|
|
25
|
+
INSTRUCTION_FILES = {
|
|
26
|
+
"codex" => "AGENTS.md",
|
|
27
|
+
"copilot" => ".github/copilot-instructions.md",
|
|
28
|
+
"gemini" => "GEMINI.md"
|
|
29
|
+
}.freeze
|
|
30
|
+
TARGETS = SKILL_DIRS.merge(INSTRUCTION_FILES).freeze
|
|
31
|
+
MARKER_START = "<!-- rails-agent-kit:start -->"
|
|
32
|
+
MARKER_END = "<!-- rails-agent-kit:end -->"
|
|
14
33
|
|
|
15
|
-
Result = Struct.new(:skill, :path, :status, keyword_init: true)
|
|
34
|
+
Result = Struct.new(:skill, :path, :status, :message, keyword_init: true)
|
|
16
35
|
|
|
17
36
|
def self.available_skills
|
|
18
37
|
Dir.glob(File.join(SKILLS_DIR, "*", "SKILL.md")).map { |f| File.basename(File.dirname(f)) }.sort
|
|
19
38
|
end
|
|
20
39
|
|
|
40
|
+
# The managed block for instruction-file agents: the skills' bodies without front matter, with every
|
|
41
|
+
# heading one level deeper so they nest under the file's own top-level heading.
|
|
42
|
+
def self.render_block(skills)
|
|
43
|
+
bodies = skills.map { |name| demote_headings(strip_front_matter(File.read(skill_source(name)))).strip }
|
|
44
|
+
[
|
|
45
|
+
MARKER_START,
|
|
46
|
+
"<!-- Generated by `rails-agent-kit install-skills`. Edits inside this block are replaced on the next run. -->",
|
|
47
|
+
"",
|
|
48
|
+
bodies.join("\n\n"),
|
|
49
|
+
MARKER_END
|
|
50
|
+
].join("\n") + "\n"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.skill_source(name)
|
|
54
|
+
source = File.join(SKILLS_DIR, name, "SKILL.md")
|
|
55
|
+
raise Error, "unknown skill #{name.inspect} (available: #{available_skills.join(', ')})" unless File.file?(source)
|
|
56
|
+
|
|
57
|
+
source
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.strip_front_matter(text)
|
|
61
|
+
text.sub(/\A---\s*\n.*?\n---\s*\n/m, "")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.demote_headings(text)
|
|
65
|
+
in_fence = false
|
|
66
|
+
text.lines.map do |line|
|
|
67
|
+
in_fence = !in_fence if line.start_with?("```", "~~~")
|
|
68
|
+
!in_fence && line.match?(/\A\#{1,5} /) ? "##{line}" : line
|
|
69
|
+
end.join
|
|
70
|
+
end
|
|
71
|
+
|
|
21
72
|
def initialize(root, agent: "cursor", force: false)
|
|
22
73
|
@root = File.expand_path(root)
|
|
23
74
|
@agent = agent.to_s
|
|
@@ -25,23 +76,32 @@ module RailsAgentKit
|
|
|
25
76
|
raise Error, "unknown agent #{@agent.inspect} (supported: #{TARGETS.keys.join(', ')})" unless TARGETS.key?(@agent)
|
|
26
77
|
end
|
|
27
78
|
|
|
28
|
-
# Installs the named skills (default: all bundled skills).
|
|
29
|
-
#
|
|
30
|
-
# :
|
|
79
|
+
# Installs the named skills (default: all bundled skills). Returns one Result per written path with status
|
|
80
|
+
# :created, :added (block appended to an existing file), :updated, :unchanged or :skipped. A skill copy you
|
|
81
|
+
# have edited is skipped unless force: true; a managed block is always brought up to date.
|
|
31
82
|
def install(skills = self.class.available_skills)
|
|
32
|
-
skills
|
|
33
|
-
|
|
34
|
-
raise Error, "unknown skill #{name.inspect} (available: #{self.class.available_skills.join(', ')})" unless File.file?(source)
|
|
83
|
+
run(skills, write: true)
|
|
84
|
+
end
|
|
35
85
|
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
# Same statuses as #install, without writing anything.
|
|
87
|
+
def plan(skills = self.class.available_skills)
|
|
88
|
+
run(skills, write: false)
|
|
38
89
|
end
|
|
39
90
|
|
|
40
91
|
private
|
|
41
92
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
93
|
+
def run(skills, write:)
|
|
94
|
+
skills.each { |name| self.class.skill_source(name) }
|
|
95
|
+
if INSTRUCTION_FILES.key?(@agent)
|
|
96
|
+
[install_block(skills, write: write)]
|
|
97
|
+
else
|
|
98
|
+
skills.map { |name| install_skill(name, write: write) }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def install_skill(name, write:)
|
|
103
|
+
dest = File.join(@root, SKILL_DIRS.fetch(@agent), name, "SKILL.md")
|
|
104
|
+
content = File.read(self.class.skill_source(name))
|
|
45
105
|
status =
|
|
46
106
|
if !File.exist?(dest) then :created
|
|
47
107
|
elsif File.read(dest) == content then :unchanged
|
|
@@ -49,11 +109,37 @@ module RailsAgentKit
|
|
|
49
109
|
else :skipped
|
|
50
110
|
end
|
|
51
111
|
|
|
52
|
-
if %i[created updated].include?(status)
|
|
53
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
54
|
-
File.write(dest, content)
|
|
55
|
-
end
|
|
112
|
+
write_file(dest, content) if write && %i[created updated].include?(status)
|
|
56
113
|
Result.new(skill: name, path: dest, status: status)
|
|
57
114
|
end
|
|
115
|
+
|
|
116
|
+
def install_block(skills, write:)
|
|
117
|
+
dest = File.join(@root, INSTRUCTION_FILES.fetch(@agent))
|
|
118
|
+
block = self.class.render_block(skills)
|
|
119
|
+
result = ->(status, message = nil) { Result.new(skill: skills.join(", "), path: dest, status: status, message: message) }
|
|
120
|
+
return result.call(:created).tap { write_file(dest, block) if write } unless File.exist?(dest)
|
|
121
|
+
|
|
122
|
+
current = File.read(dest)
|
|
123
|
+
start = current.index(MARKER_START)
|
|
124
|
+
finish = start && current.index(MARKER_END, start)
|
|
125
|
+
if start.nil? && !current.include?(MARKER_END)
|
|
126
|
+
separator = current.empty? || current.end_with?("\n\n") ? "" : (current.end_with?("\n") ? "\n" : "\n\n")
|
|
127
|
+
write_file(dest, current + separator + block) if write
|
|
128
|
+
return result.call(:added)
|
|
129
|
+
end
|
|
130
|
+
return result.call(:skipped, "found an unmatched rails-agent-kit marker; fix or remove it") if finish.nil?
|
|
131
|
+
|
|
132
|
+
stop = finish + MARKER_END.length
|
|
133
|
+
stop += 1 if current[stop] == "\n"
|
|
134
|
+
return result.call(:unchanged) if current[start...stop] == block
|
|
135
|
+
|
|
136
|
+
write_file(dest, current[0...start] + block + current[stop..]) if write
|
|
137
|
+
result.call(:updated)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def write_file(dest, content)
|
|
141
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
142
|
+
File.write(dest, content)
|
|
143
|
+
end
|
|
58
144
|
end
|
|
59
145
|
end
|
data/lib/rails_agent_kit.rb
CHANGED
|
@@ -5,8 +5,10 @@ require_relative "rails_agent_kit/schema_parser"
|
|
|
5
5
|
require_relative "rails_agent_kit/structure_sql_parser"
|
|
6
6
|
require_relative "rails_agent_kit/schema_inspector"
|
|
7
7
|
require_relative "rails_agent_kit/routes_parser"
|
|
8
|
+
require_relative "rails_agent_kit/ruby_env"
|
|
8
9
|
require_relative "rails_agent_kit/routes_inspector"
|
|
9
10
|
require_relative "rails_agent_kit/skill_installer"
|
|
11
|
+
require_relative "rails_agent_kit/client_config"
|
|
10
12
|
|
|
11
13
|
# Gives coding agents ground truth about a Rails app (schema, routes) over MCP.
|
|
12
14
|
module RailsAgentKit
|
|
@@ -20,8 +20,9 @@ change the code the way this app already does things.
|
|
|
20
20
|
|
|
21
21
|
## 1. Check the facts before you edit
|
|
22
22
|
|
|
23
|
-
- **Schema:** call the `rails_schema` MCP tool
|
|
24
|
-
|
|
23
|
+
- **Schema:** call the `rails_schema` MCP tool before you write a model attribute, query,
|
|
24
|
+
validation, fixture or migration. With no arguments it lists the tables; pass `table:`
|
|
25
|
+
(one name or an array) for their columns, or `search:` to find a column by name. Without the
|
|
25
26
|
tool, read `db/schema.rb` (or `db/structure.sql`). Never guess a column name, type,
|
|
26
27
|
nullability or default.
|
|
27
28
|
- **Routes:** call the `rails_routes` MCP tool (pass `filter:`) before you use a
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-agent-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrii Furmanets
|
|
@@ -23,10 +23,11 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.8'
|
|
26
|
-
description: rails-agent-kit is a
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
description: rails-agent-kit is a read-only Model Context Protocol (stdio) server
|
|
27
|
+
for Rails apps. rails_schema returns tables, columns, indexes and foreign keys parsed
|
|
28
|
+
statically from db/schema.rb (without booting the app); rails_routes returns every
|
|
29
|
+
route from `bin/rails routes --expanded`. It also ships a Rails 8 conventions skill
|
|
30
|
+
for Cursor and Claude Code, installed with `rails-agent-kit install-skills`.
|
|
30
31
|
executables:
|
|
31
32
|
- rails-agent-kit
|
|
32
33
|
extensions: []
|
|
@@ -37,8 +38,10 @@ files:
|
|
|
37
38
|
- README.md
|
|
38
39
|
- exe/rails-agent-kit
|
|
39
40
|
- lib/rails_agent_kit.rb
|
|
41
|
+
- lib/rails_agent_kit/client_config.rb
|
|
40
42
|
- lib/rails_agent_kit/routes_inspector.rb
|
|
41
43
|
- lib/rails_agent_kit/routes_parser.rb
|
|
44
|
+
- lib/rails_agent_kit/ruby_env.rb
|
|
42
45
|
- lib/rails_agent_kit/schema_inspector.rb
|
|
43
46
|
- lib/rails_agent_kit/schema_parser.rb
|
|
44
47
|
- lib/rails_agent_kit/server.rb
|
|
@@ -51,8 +54,9 @@ licenses:
|
|
|
51
54
|
- MIT
|
|
52
55
|
metadata:
|
|
53
56
|
homepage_uri: https://github.com/afurm/rails-agent-kit
|
|
54
|
-
source_code_uri: https://github.com/afurm/rails-agent-kit
|
|
57
|
+
source_code_uri: https://github.com/afurm/rails-agent-kit/tree/main
|
|
55
58
|
changelog_uri: https://github.com/afurm/rails-agent-kit/blob/main/CHANGELOG.md
|
|
59
|
+
documentation_uri: https://afurm.github.io/rails-agent-kit/
|
|
56
60
|
bug_tracker_uri: https://github.com/afurm/rails-agent-kit/issues
|
|
57
61
|
rubygems_mfa_required: 'true'
|
|
58
62
|
rdoc_options: []
|
|
@@ -69,8 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
69
73
|
- !ruby/object:Gem::Version
|
|
70
74
|
version: '0'
|
|
71
75
|
requirements: []
|
|
72
|
-
rubygems_version:
|
|
76
|
+
rubygems_version: 4.0.20
|
|
73
77
|
specification_version: 4
|
|
74
|
-
summary: MCP server and agent
|
|
75
|
-
Rails app's schema and routes.
|
|
78
|
+
summary: MCP server and Rails 8 agent skill that give coding agents (Cursor, Claude
|
|
79
|
+
Code, Codex) ground truth about your Rails app's schema and routes.
|
|
76
80
|
test_files: []
|