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.
@@ -3,21 +3,72 @@
3
3
  require "fileutils"
4
4
 
5
5
  module RailsAgentKit
6
- # Copies the agent skills bundled with the gem (skills/<name>/SKILL.md) into a Rails app,
7
- # e.g. <root>/.cursor/skills/<name>/SKILL.md for Cursor.
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
- TARGETS = {
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). Existing files that differ are left
29
- # untouched unless force: true. Returns one Result per skill with status :created, :updated,
30
- # :unchanged or :skipped.
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.map do |name|
33
- source = File.join(SKILLS_DIR, name, "SKILL.md")
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
- install_one(name, source)
37
- end
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 install_one(name, source)
43
- dest = File.join(@root, TARGETS.fetch(@agent), name, "SKILL.md")
44
- content = File.read(source)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAgentKit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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 (pass `table:` to get one table) before
24
- you write a model attribute, query, validation, fixture or migration. Without the
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.1.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 small Model Context Protocol (stdio) server. It
27
- exposes a Rails app's database schema (parsed from db/schema.rb without booting
28
- the app) and its routes to coding agents such as Cursor and Claude Code, and ships
29
- a Rails 8 conventions skill for Cursor/Claude (installed with `rails-agent-kit install-skills`).
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: 3.6.7
76
+ rubygems_version: 4.0.20
73
77
  specification_version: 4
74
- summary: MCP server and agent skills that give coding agents ground truth about a
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: []