ask-rails 0.2.4 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e889ca7205fe9d828eaa76008efb362309f22768e553d4f656bb9e030806467b
4
- data.tar.gz: 88175d5a80292be3af7d330a9431a5d3153fccda7e6bd4d822d652027a73de41
3
+ metadata.gz: 1a8258ca96d9513529bc1c0c9c54bd8e2492a110e48d90c9af3f53ca3aa24d9f
4
+ data.tar.gz: 4a776f46230d7a87719d5a6e12154e90987f71f60af63707da686ba2cce20377
5
5
  SHA512:
6
- metadata.gz: 81c97d529341e34b15c6bc058a620ae85ef159d8a1450e98da92afbbf6a24757e0fd44fbf5055141fcab76d855d1eecfade26477ba5a88df11e2e58c3e9f18e9
7
- data.tar.gz: ade64f24813a53a09587850816b77f59026b32eb4f6392daf367ab260a40d947de434606c942f1c08e14ab2ad4ed6be85eeedd2a1eda296729918ec49f56c737
6
+ metadata.gz: 894f5146923889fd014f1ac770c52eba2cfdc6d84fd546062c05ac85473f6e716e70a298761f25a19ea1b00ba60461aae3ab38889c05a95698d891320879cc86
7
+ data.tar.gz: d58b94569c064b58493bd0a929cfb1dde3310a43c110c92474947c57e711f5a4e6d57ca596c389e73fcb4ee6bddeff7e8797bc5915ca8e4cbf94eadaf5f2353d
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [0.1.0] — 2026-07-26
2
+
3
+ ### Added
4
+
5
+ - Initial release of `ask-rails` — Rails integration for the ask-rb ecosystem.
6
+ - **Install generator** — `rails generate ask:install` creates `config/initializers/ask.rb`, `app/agents/application_agent.rb`, and `app/agents/` directory.
7
+ - **Railtie** — Wires `Ask::Agent.logger` to `Rails.logger`, discovers agent definitions in `app/agents/`.
data/README.md CHANGED
@@ -1,34 +1,59 @@
1
1
  # ask-rails
2
2
 
3
- Rails integration for the ask-rb ecosystem. The only gem a Rails app needs to join
4
- the ask-rb stack — provides a Railtie, AR session persistence, a session factory,
5
- automatic service gem discovery, and generators.
3
+ Rails integration for the [ask-rb](https://github.com/ask-rb) ecosystem. Provides generators, file conventions, and railtie for using AI agents in your Rails app.
6
4
 
7
5
  ## Installation
8
6
 
9
7
  ```bash
10
8
  bundle add ask-rails
11
- rails generate ask_rails:install
9
+ rails generate ask:install
12
10
  ```
13
11
 
12
+ This creates:
13
+ - `config/initializers/ask.rb` — agent configuration
14
+ - `app/agents/application_agent.rb` — base class for your agents
15
+ - `app/agents/` — directory for agent definitions
16
+
14
17
  ## Usage
15
18
 
19
+ Define an agent:
20
+
16
21
  ```ruby
17
- # In any Rails context
18
- session = Ask::Rails.agent_session
19
- session.run("Find all open issues labeled 'bug' in our repo")
22
+ # app/agents/support_bot.rb
23
+ class Agents::SupportBot < ApplicationAgent
24
+ model "gpt-4o"
25
+ system_prompt "You help users with support questions."
26
+
27
+ tool :bash
28
+ tool :read
29
+ tool :grep
30
+ end
20
31
  ```
21
32
 
22
- Service gems like `ask-github`, `ask-slack` are auto-discovered — the agent gets their
23
- context (auth info, quick-start snippets, error guides) in the system prompt automatically.
33
+ Run it:
34
+
35
+ ```ruby
36
+ agent = Ask::Agent.new("support_bot")
37
+ response = agent.run("Find all open issues in the codebase")
38
+ puts response
39
+ ```
24
40
 
25
- ## Development
41
+ Or use `Ask.chat` for one-off conversations:
26
42
 
27
- ```bash
28
- bin/setup
29
- bundle exec rake test
43
+ ```ruby
44
+ Ask.chat("Summarize this article: #{text}")
30
45
  ```
31
46
 
47
+ ## Configuration
48
+
49
+ API keys are resolved automatically by `Ask::Auth`:
50
+
51
+ - **Environment variables:** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.
52
+ - **Rails credentials:** `rails credentials:edit` → add `ask.openai`, `ask.anthropic`
53
+ - **File:** `~/.ask/credentials.yml`
54
+
55
+ No manual credential setup is required.
56
+
32
57
  ## License
33
58
 
34
59
  MIT
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Ask
6
+ module Rails
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Sets up ask-rb for Rails — creates initializer and app/agents/ directory"
12
+
13
+ def create_initializer
14
+ template "initializer.rb", "config/initializers/ask.rb"
15
+ end
16
+
17
+ def create_application_agent
18
+ template "application_agent.rb", "app/agents/application_agent.rb"
19
+ end
20
+
21
+ def create_agents_directory
22
+ empty_directory "app/agents"
23
+ create_file "app/agents/.keep", "" unless options[:skip_keep]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for your application's AI agents.
4
+ #
5
+ # Subclass this to define agents:
6
+ #
7
+ # class Agents::SupportBot < ApplicationAgent
8
+ # model "gpt-4o"
9
+ # system_prompt "You help users with support questions."
10
+ #
11
+ # tool :search_knowledge_base
12
+ # end
13
+ #
14
+ # Then run:
15
+ # agent = Ask::Agent.new("support_bot")
16
+ # agent.run("How do I reset my password?")
17
+ #
18
+ class ApplicationAgent < Ask::Agent::Definition
19
+ # Default instructions — override in subclasses
20
+ system_prompt "You are a helpful assistant."
21
+
22
+ # Uncomment to add built-in tools:
23
+ # tool :bash
24
+ # tool :read
25
+ # tool :grep
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ask Agent Configuration
4
+ # ========================
5
+ #
6
+ # API keys are resolved automatically by Ask::Auth:
7
+ # - Environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.
8
+ # - Rails credentials: rails credentials:edit → ask.openai, ask.anthropic
9
+ # - ~/.ask/credentials.yml
10
+ #
11
+ # See https://github.com/ask-rb/ask-auth for details.
12
+
13
+ Ask::Agent.configure do |config|
14
+ # config.default_model = "gpt-4o"
15
+ end
@@ -3,25 +3,8 @@
3
3
  module Ask
4
4
  module Rails
5
5
  class Railtie < ::Rails::Railtie
6
- rake_tasks do
7
- # Load rake tasks if any
8
- end
9
-
10
6
  generators do
11
- require_relative "../../generators/ask/rails/install/install_generator"
12
- end
13
-
14
- initializer "ask_rails.configure" do |app|
15
- Ask::Rails.configuration.default_model ||= ENV["ASK_DEFAULT_MODEL"] || "gpt-4o"
16
- Ask::Rails.configuration.max_turns ||= (ENV["ASK_MAX_TURNS"] || 25).to_i
17
- end
18
-
19
- initializer "ask_rails.discover_tools", after: :eager_load_most do
20
- Ask::Rails.discover_tools!
21
- end
22
-
23
- initializer "ask_rails.discover_services", after: :eager_load_most do
24
- Ask::Rails::ServiceDiscovery.discover!
7
+ require_relative "generators/install/install_generator"
25
8
  end
26
9
  end
27
10
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Rails
5
- VERSION = "0.2.4"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/ask/rails.rb CHANGED
@@ -1,80 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails"
4
3
  require "ask/agent"
5
- require "ask/auth"
6
4
 
7
5
  module Ask
8
6
  module Rails
9
- class << self
10
- def configure
11
- yield configuration
12
- end
13
-
14
- def configuration
15
- @configuration ||= Configuration.new
16
- end
17
-
18
- def agent_session(**extra)
19
- tools = configuration.tools.map { |t| t.is_a?(Class) ? t.new : t }
20
- prompt = extra.delete(:system_prompt) || configuration.system_prompt || default_system_prompt
21
-
22
- Ask::Agent::Session.new(
23
- model: configuration.default_model,
24
- max_turns: configuration.max_turns,
25
- system_prompt: prompt,
26
- tools: tools,
27
- persistence: configuration.persistence_adapter,
28
- **extra
29
- )
30
- end
31
-
32
- def discover_tools!
33
- self.configuration.tools = Ask::Tools::Shell::TOOLS.map(&:new) + discovered_rails_tools
34
- end
35
-
36
- def root
37
- @root ||= Pathname.new(File.expand_path("..", __dir__))
38
- end
39
-
40
- private
41
-
42
- def discovered_rails_tools
43
- tools = []
44
- files = Dir[::Rails.root.join("app", "tools", "*.rb")]
45
- files.each do |f|
46
- require f
47
- klass = File.basename(f, ".rb").camelize.constantize rescue next
48
- tools << klass if klass < Ask::Rails::Tool
49
- end
50
- tools
51
- rescue
52
- tools
53
- end
54
-
55
- def default_system_prompt
56
- <<~PROMPT
57
- You are a Ruby on Rails software engineer.
58
- You have direct access to the application's code, database, and runtime.
59
- Use your tools to inspect and modify the codebase.
60
- Once you have enough information, stop calling tools and give your answer.
61
- PROMPT
62
- end
63
- end
64
7
  end
65
8
  end
66
9
 
67
10
  require_relative "rails/version"
68
- require_relative "rails/engine"
69
- require_relative "rails/configuration"
70
- require_relative "rails/railtie"
71
- require_relative "rails/persistence"
72
- require_relative "rails/service_discovery"
73
- require_relative "rails/tool"
74
- require_relative "rails/tools/read_file"
75
- require_relative "rails/tools/run_command"
76
- require_relative "rails/tools/search_codebase"
77
- require_relative "rails/tools/read_routes"
78
- require_relative "rails/tools/query_database"
79
- require_relative "rails/tools/read_model"
80
- require_relative "rails/tools/read_log"
11
+ require_relative "rails/railtie" if defined?(::Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -24,21 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '7.1'
26
26
  - !ruby/object:Gem::Dependency
27
- name: ask-tools
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '0.1'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '0.1'
40
- - !ruby/object:Gem::Dependency
41
- name: ask-tools-shell
27
+ name: ask-agent
42
28
  requirement: !ruby/object:Gem::Requirement
43
29
  requirements:
44
30
  - - ">="
@@ -51,48 +37,6 @@ dependencies:
51
37
  - - ">="
52
38
  - !ruby/object:Gem::Version
53
39
  version: '0.1'
54
- - !ruby/object:Gem::Dependency
55
- name: ask-agent
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '0.1'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '0.1'
68
- - !ruby/object:Gem::Dependency
69
- name: ask-auth
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '0.1'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '0.1'
82
- - !ruby/object:Gem::Dependency
83
- name: sqlite3
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: '2.0'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '2.0'
96
40
  - !ruby/object:Gem::Dependency
97
41
  name: minitest
98
42
  requirement: !ruby/object:Gem::Requirement
@@ -135,42 +79,31 @@ dependencies:
135
79
  - - "~>"
136
80
  - !ruby/object:Gem::Version
137
81
  version: '13.0'
138
- description: Railtie, AR session persistence, session factory, service gem discovery,
139
- and generators.
82
+ description: Rails generators, file conventions, and railtie for using ask-agent in
83
+ Rails apps.
140
84
  email:
141
85
  - kaka@myrrlabs.com
142
86
  executables: []
143
87
  extensions: []
144
88
  extra_rdoc_files: []
145
89
  files:
90
+ - CHANGELOG.md
146
91
  - LICENSE
147
92
  - README.md
148
93
  - lib/ask-rails.rb
149
94
  - lib/ask/rails.rb
150
- - lib/ask/rails/configuration.rb
151
- - lib/ask/rails/engine.rb
152
- - lib/ask/rails/persistence.rb
95
+ - lib/ask/rails/generators/install/install_generator.rb
96
+ - lib/ask/rails/generators/install/templates/application_agent.rb
97
+ - lib/ask/rails/generators/install/templates/initializer.rb
153
98
  - lib/ask/rails/railtie.rb
154
- - lib/ask/rails/service_discovery.rb
155
- - lib/ask/rails/tool.rb
156
- - lib/ask/rails/tools/query_database.rb
157
- - lib/ask/rails/tools/read_file.rb
158
- - lib/ask/rails/tools/read_log.rb
159
- - lib/ask/rails/tools/read_model.rb
160
- - lib/ask/rails/tools/read_routes.rb
161
- - lib/ask/rails/tools/run_command.rb
162
- - lib/ask/rails/tools/search_codebase.rb
163
99
  - lib/ask/rails/version.rb
164
- - lib/ask/skills/rails.db_debug/SKILL.md
165
- - lib/ask/skills/rails.deploy_pipeline/SKILL.md
166
- - lib/ask/skills/rails.route_trouble/SKILL.md
167
- - lib/generators/ask/rails/install/install_generator.rb
168
- - lib/generators/ask/rails/install/templates/initializer.rb
169
- - lib/generators/ask/rails/install/templates/migration.rb
170
100
  homepage: https://github.com/ask-rb/ask-rails
171
101
  licenses:
172
102
  - MIT
173
- metadata: {}
103
+ metadata:
104
+ homepage_uri: https://github.com/ask-rb/ask-rails
105
+ source_code_uri: https://github.com/ask-rb/ask-rails
106
+ changelog_uri: https://github.com/ask-rb/ask-rails/blob/main/CHANGELOG.md
174
107
  rdoc_options: []
175
108
  require_paths:
176
109
  - lib
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- class Configuration
6
- attr_accessor :default_model, :max_turns, :system_prompt,
7
- :tool_concurrency, :persistence_adapter, :tools
8
-
9
- def initialize
10
- @default_model = "gpt-4o"
11
- @max_turns = 25
12
- @system_prompt = nil
13
- @tool_concurrency = 5
14
- @persistence_adapter = nil
15
- @tools = []
16
- end
17
- end
18
- end
19
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "railtie"
4
-
5
- module Ask
6
- module Rails
7
- class Engine < ::Rails::Engine
8
- isolate_namespace Ask::Rails
9
- end
10
- end
11
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- class Persistence
6
- def initialize(model_class: nil)
7
- @model_class = model_class
8
- end
9
-
10
- def save(session_id, data)
11
- record = model_class.find_or_initialize_by(session_id: session_id)
12
- record.update!(data: data)
13
- end
14
-
15
- def load(session_id)
16
- record = model_class.find_by(session_id: session_id)
17
- record&.data
18
- end
19
-
20
- def delete(session_id)
21
- model_class.where(session_id: session_id).delete_all
22
- end
23
-
24
- def list
25
- model_class.pluck(:session_id)
26
- end
27
-
28
- private
29
-
30
- def model_class
31
- @model_class || (raise "No model class configured. Use Persistence.new(model_class: MyModel)")
32
- end
33
- end
34
- end
35
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module ServiceDiscovery
6
- SERVICE_GEMS_PATTERN = /\Aask-(?!tools|tools-shell|agent|rails|core|auth|schema|llm)/
7
-
8
- module_function
9
-
10
- def discover!
11
- service_gems = Gem.loaded_specs.keys.select { |name| name.match?(SERVICE_GEMS_PATTERN) }
12
- contexts = []
13
-
14
- service_gems.each do |name|
15
- begin
16
- require "#{name.tr("-", "/")}/context"
17
- mod = name.split("-").map(&:capitalize).join.constantize
18
- contexts << mod if mod.respond_to?(:const_defined?) && mod.const_defined?(:DESCRIPTION)
19
- rescue LoadError, NameError
20
- end
21
- end
22
-
23
- unless contexts.empty?
24
- prompt = build_system_prompt(contexts)
25
- existing = Ask::Rails.configuration.system_prompt
26
- Ask::Rails.configuration.system_prompt = [existing, prompt].compact.join("\n\n")
27
- end
28
-
29
- contexts
30
- end
31
-
32
- def build_system_prompt(contexts)
33
- sections = ["## Available Services"]
34
-
35
- contexts.each do |mod|
36
- name = mod.respond_to?(:name) ? mod.name.to_s.split("::").last || "Unknown" : "Unknown"
37
- sections << "### #{name}"
38
- sections << mod::DESCRIPTION if mod.const_defined?(:DESCRIPTION)
39
- sections << "Documentation: #{mod::DOCS_URL}" if mod.const_defined?(:DOCS_URL)
40
- sections << "Authentication: #{mod::AUTH_HOW}" if mod.const_defined?(:AUTH_HOW)
41
- sections << ""
42
- end
43
-
44
- sections.join("\n")
45
- end
46
- end
47
- end
48
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- class Tool < Ask::Tool
6
- def rails_root
7
- ::Rails.root
8
- end
9
- end
10
- end
11
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class QueryDatabase < Ask::Rails::Tool
7
- description "Run a read-only SQL query against the application database. " \
8
- "Returns columns and rows. Only SELECT queries are allowed in production."
9
-
10
- param :sql, type: :string, desc: "SQL query (SELECT only in production)", required: true
11
- param :limit, type: :integer, desc: "Max rows to return (default 50)", required: false
12
-
13
- WRITE_STATEMENTS = /\A\s*(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE)\b/i
14
-
15
- def execute(sql:, limit: 50)
16
- sql = sql.strip
17
-
18
- if WRITE_STATEMENTS.match?(sql)
19
- return Ask::Result.failure(
20
- "Only SELECT queries are allowed. Write statements (#{sql.match(WRITE_STATEMENTS)[1]}) are rejected in all environments."
21
- )
22
- end
23
-
24
- if ::Rails.env.production? && !sql.match?(/\A\s*SELECT\b/i)
25
- return Ask::Result.failure(
26
- "Only SELECT queries are allowed in the production environment."
27
- )
28
- end
29
-
30
- pool = ActiveRecord::Base.connection_pool
31
- pool.with_connection do |conn|
32
- limited_sql = sql.match?(/\bLIMIT\b/i) ? sql : "#{sql.chomp(';')} LIMIT #{limit.to_i}"
33
- result = conn.exec_query(limited_sql)
34
- columns = result.columns
35
- rows = result.rows.first(limit.to_i).map { |row| build_row(row, columns) }
36
- {
37
- columns: columns,
38
- rows: rows,
39
- count: rows.size,
40
- truncated: result.rows.size > limit.to_i
41
- }
42
- end
43
- rescue ActiveRecord::StatementInvalid => e
44
- Ask::Result.failure("SQL error: #{e.message}")
45
- rescue ActiveRecord::ConnectionNotEstablished => e
46
- Ask::Result.failure("Database not connected: #{e.message}. Verify the database is running and Rails is connected.")
47
- end
48
-
49
- private
50
-
51
- def build_row(row, columns)
52
- columns.each_with_index.each_with_object({}) do |(col, i), hash|
53
- value = row[i]
54
- hash[col] = sanitize_value(value)
55
- end
56
- end
57
-
58
- def sanitize_value(value)
59
- return "[BINARY DATA]" if binary_value?(value)
60
- return value.iso8601 if value.respond_to?(:iso8601)
61
- value
62
- end
63
-
64
- def binary_value?(value)
65
- value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT && value.bytesize > 0
66
- rescue
67
- false
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class ReadFile < Ask::Rails::Tool
7
- description "Read a file from the Rails app. Paths are relative to Rails.root."
8
-
9
- param :path, type: :string, desc: "Relative path from Rails.root", required: true
10
-
11
- def execute(path:)
12
- full_path = rails_root.join(path)
13
- return Ask::Result.error(message: "File not found: #{path}") unless full_path.exist?
14
-
15
- content = full_path.read
16
- Ask::Result.success(
17
- data: { path: path, content: content, size: content.length },
18
- metadata: { path: path, size: content.length }
19
- )
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class ReadLog < Ask::Rails::Tool
7
- description "Read application log files with filtering. Supports Rails default " \
8
- "logger and log rotation. Reads from the end of the file (most recent first)."
9
-
10
- param :lines, type: :integer, desc: "Number of recent lines (default 50, max 500)", required: false
11
- param :level, type: :string, desc: "Filter by level: ERROR, WARN, INFO, DEBUG", required: false
12
- param :search, type: :string, desc: "Search term (plain text, case-insensitive)", required: false
13
- param :file, type: :string, desc: "Log file name (default: log/<env>.log)", required: false
14
-
15
- MAX_LINES = 500
16
- LEVEL_PATTERNS = {
17
- "ERROR" => /\bERROR\b/i,
18
- "WARN" => /\bWARN\b/i,
19
- "INFO" => /\bINFO\b/i,
20
- "DEBUG" => /\bDEBUG\b/i
21
- }.freeze
22
-
23
- def execute(lines: 50, level: nil, search: nil, file: nil)
24
- max_lines = [lines.to_i, MAX_LINES].min
25
- log_path = resolve_log_path(file)
26
-
27
- unless log_path.exist?
28
- return Ask::Result.failure(
29
- "Log file not found: #{log_path}. The application may not have written any logs yet."
30
- )
31
- end
32
-
33
- raw_lines = read_all_log_files(log_path)
34
- return { lines: [], total_lines: 0, path: log_path.to_s } if raw_lines.empty?
35
-
36
- filtered = apply_filters(raw_lines, level: level, search: search)
37
- recent = filtered.last(max_lines).map(&:chomp)
38
-
39
- {
40
- lines: recent,
41
- total_lines: raw_lines.size,
42
- matched_lines: filtered.size,
43
- path: log_path.to_s,
44
- filters_applied: { level: level, search: search }.compact
45
- }
46
- end
47
-
48
- private
49
-
50
- def resolve_log_path(custom_path)
51
- return rails_root.join(custom_path) if custom_path
52
- rails_root.join("log", "#{Rails.env}.log")
53
- end
54
-
55
- # Read from rotated archives too: log/production.log, .1, .2.gz, etc.
56
- def read_all_log_files(log_path)
57
- all_content = +""
58
- rotated_files(log_path).each do |path|
59
- content = read_file_content(path)
60
- all_content.prepend(content) if content
61
- end
62
- all_content.lines
63
- end
64
-
65
- def rotated_files(log_path)
66
- dir = log_path.dirname
67
- base = log_path.basename.to_s
68
- # Primary file, then rotated files in reverse order (oldest first, then primary last)
69
- pattern = File.join(dir, "#{base}.*")
70
- rotations = Dir[pattern].sort_by { |f| extract_rotation_number(f) }
71
- # Primary file is read last (most recent)
72
- rotations + [log_path.to_s]
73
- end
74
-
75
- def extract_rotation_number(path)
76
- File.basename(path).sub(/.*\.(\d+)(\.gz)?$/, '\1').to_i
77
- rescue
78
- 0
79
- end
80
-
81
- def read_file_content(path)
82
- if path.to_s.end_with?(".gz")
83
- Zlib::GzipReader.open(path.to_s) { |gz| gz.read }
84
- else
85
- File.read(path.to_s)
86
- end
87
- rescue => e
88
- warn "[ReadLog] Could not read #{path}: #{e.message}"
89
- nil
90
- end
91
-
92
- def read_tail(path, max_bytes)
93
- return "" unless path.exist?
94
-
95
- size = path.size
96
- return path.read if size <= max_bytes
97
-
98
- File.open(path, "rb") do |f|
99
- f.seek(-max_bytes, IO::SEEK_END)
100
- partial = f.read(max_bytes)
101
- if (idx = partial.index("\n"))
102
- partial[idx + 1..]
103
- else
104
- partial
105
- end
106
- end
107
- end
108
-
109
- def apply_filters(lines, level: nil, search: nil)
110
- filtered = lines
111
- filtered = filtered.select { |l| LEVEL_PATTERNS.fetch(level) { // }.match?(l) } if level
112
- filtered = filtered.select { |l| l.downcase.include?(search.downcase) } if search
113
- filtered
114
- end
115
- end
116
- end
117
- end
118
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class ReadModel < Ask::Rails::Tool
7
- description "Inspect an ActiveRecord model — columns, associations, validations, " \
8
- "scopes, and indexes. Returns structured data the agent can act on."
9
-
10
- param :name, type: :string, desc: "Model class name (e.g. 'User', 'Blog::Post')", required: true
11
- param :detail, type: :string, desc: "Which details: 'all' (default), 'columns', 'associations', 'validations', 'scopes'", required: false
12
-
13
- def execute(name:, detail: "all")
14
- klass = safe_constantize(name)
15
- return Ask::Result.failure("Model '#{name}' not found or is not an ActiveRecord model.") unless klass
16
-
17
- result = { name: klass.name, table_name: klass.table_name }
18
-
19
- result[:primary_key] = klass.primary_key if klass.respond_to?(:primary_key)
20
-
21
- if %w[all columns].include?(detail)
22
- result[:columns] = klass.columns.map { |c|
23
- entry = { name: c.name, type: c.type, null: c.null, default: c.default }
24
- entry[:primary_key] = true if c.name == klass.primary_key
25
- entry
26
- }
27
- end
28
-
29
- if %w[all associations].include?(detail)
30
- result[:associations] = klass.reflect_on_all_associations.group_by(&:macro).transform_values { |refs|
31
- refs.map { |a|
32
- entry = { name: a.name, class_name: a.class_name }
33
- entry[:through] = a.options[:through] if a.options[:through]
34
- entry[:source] = a.options[:source] if a.options[:source]
35
- entry[:foreign_key] = a.foreign_key if a.respond_to?(:foreign_key)
36
- entry
37
- }
38
- }
39
- end
40
-
41
- if %w[all scopes].include?(detail) && klass.respond_to?(:all)
42
- result[:scopes] = klass.methods(false)
43
- .reject { |m| m.to_s.end_with?("=", "!", "?") || %i[new allocate inspect to_s].include?(m) }
44
- .map(&:to_s).sort
45
- end
46
-
47
- if %w[all validations].include?(detail)
48
- result[:validators] = klass.validators.map { |v|
49
- {
50
- attribute: v.attributes.first&.to_s,
51
- kind: v.kind,
52
- options: v.options.reject { |k, _| k == :if }
53
- }
54
- }.reject { |v| v[:attribute].nil? }
55
- end
56
-
57
- result
58
- end
59
-
60
- private
61
-
62
- def safe_constantize(name)
63
- klass = name.safe_constantize
64
- return nil unless klass
65
- return nil unless klass < ActiveRecord::Base
66
- klass
67
- rescue
68
- nil
69
- end
70
- end
71
- end
72
- end
73
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class ReadRoutes < Ask::Rails::Tool
7
- description "Read the Rails routes from config/routes.rb"
8
- def execute
9
- routes_file = rails_root.join("config", "routes.rb")
10
- return Ask::Result.error(message: "No routes file found") unless routes_file.exist?
11
-
12
- content = routes_file.read
13
- Ask::Result.success(
14
- data: { content: content },
15
- metadata: { size: content.length }
16
- )
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class RunCommand < Ask::Rails::Tool
7
- description "Run a shell command in the Rails app root directory."
8
- param :command, type: :string, desc: "Shell command to run", required: true
9
-
10
- def execute(command:)
11
- output = `cd #{rails_root} && #{command} 2>&1`
12
- Ask::Result.success(
13
- data: { output: output, exit_status: $?.exitstatus },
14
- metadata: { exit_status: $?.exitstatus }
15
- )
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Rails
5
- module Tools
6
- class SearchCodebase < Ask::Rails::Tool
7
- description "Search the Rails codebase with grep."
8
- param :pattern, type: :string, desc: "Search pattern", required: true
9
- param :path, type: :string, desc: "Subdirectory to search (optional)"
10
-
11
- def execute(pattern:, path: nil)
12
- search_path = path ? rails_root.join(path) : rails_root
13
- results = `cd #{rails_root} && grep -rn '#{pattern}' #{search_path} 2>&1 | head -50`
14
- Ask::Result.success(
15
- data: { results: results, count: results.lines.count },
16
- metadata: { pattern: pattern, count: results.lines.count }
17
- )
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,118 +0,0 @@
1
- ---
2
- name: rails.db_debug
3
- description: Step-by-step methodology for debugging database performance issues in Rails
4
- ---
5
-
6
- Use this skill when investigating slow queries, N+1 problems, missing indexes, or
7
- general database performance issues in a Rails application.
8
-
9
- ## Step 1: Identify the Slow Queries
10
-
11
- Use `ReadLog.new.call(lines: 200, search: "SELECT")` to find recent database
12
- queries in the application log. Look for:
13
-
14
- - Queries taking > 100ms (look for "duration:" or "↳" markers in Rails logs)
15
- - Repetitive queries with the same structure (potential N+1)
16
- - Queries running in loops (same query, different IDs)
17
-
18
- If you need more detail, narrow the search: `ReadLog.new.call(lines: 500, level: "WARN")`.
19
-
20
- ## Step 2: Understand the Schema
21
-
22
- For each model involved in the slow queries, inspect it:
23
-
24
- ```ruby
25
- ReadModel.new.call(name: "User")
26
- ReadModel.new.call(name: "Post")
27
- ```
28
-
29
- Focus on:
30
- - **Columns** — are there columns that look like foreign keys without indexes?
31
- - **Associations** — what associations exist and what `class_name` do they use?
32
- - **Validators** — any database-level constraints that could be missing?
33
-
34
- Run `ReadModel.new.call(name: "User", detail: "columns")` if you only need columns.
35
-
36
- ## Step 3: Check for Missing Indexes
37
-
38
- Query the database for actual indexes:
39
-
40
- ```ruby
41
- QueryDatabase.new.call(
42
- sql: "SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' ORDER BY tablename, indexname",
43
- limit: 200
44
- )
45
- ```
46
-
47
- Look for:
48
- - Foreign key columns that lack indexes (e.g. `user_id` without index)
49
- - Columns used in `WHERE` clauses without indexes
50
- - Composite indexes that could cover multiple query patterns
51
-
52
- If an index is missing, check if adding one would help:
53
- ```ruby
54
- QueryDatabase.new.call(
55
- sql: "EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 1",
56
- limit: 10
57
- )
58
- ```
59
-
60
- ## Step 4: Detect N+1 Queries
61
-
62
- N+1 manifests as the same query repeated with different IDs:
63
-
64
- ```ruby
65
- # Search for repetitive query patterns
66
- ReadLog.new.call(lines: 500, search: "WHERE.*IN")
67
- ```
68
-
69
- Common N+1 patterns:
70
- - Loading `Post.all` then accessing `post.comments` individually
71
- - Loading `User.all` then accessing `user.profile` individually
72
- - Serializing associations in views without eager loading
73
-
74
- Fix with `.includes(:association)`, `.eager_load(:association)`, or
75
- `.preload(:association)`.
76
-
77
- ## Step 5: Profile Slow Queries with EXPLAIN
78
-
79
- For any identified slow query, get the execution plan:
80
-
81
- ```ruby
82
- QueryDatabase.new.call(
83
- sql: "EXPLAIN (ANALYZE, BUFFERS) SELECT posts.* FROM posts WHERE posts.user_id = 1 ORDER BY posts.created_at DESC LIMIT 20",
84
- limit: 10
85
- )
86
- ```
87
-
88
- What to look for in explain output:
89
- - **Sequential scans** (`Seq Scan`) — missing index
90
- - **Sort operations** — missing index on sort column
91
- - **Nested loop joins** with high row counts — missing composite index
92
- - **Bitmap heap scans** with high costs — index may be suboptimal
93
-
94
- If `ANALYZE` takes too long, use `EXPLAIN (BUFFERS)` without `ANALYZE` for cost estimates.
95
-
96
- ## Step 6: Check Bulk Loading and Batch Operations
97
-
98
- If the issue involves importing or updating many records:
99
-
100
- ```ruby
101
- QueryDatabase.new.call(
102
- sql: "SELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch FROM pg_stat_user_tables ORDER BY seq_scan DESC",
103
- limit: 20
104
- )
105
- ```
106
-
107
- High `seq_scan` counts compared to `idx_scan` indicate tables where indexes
108
- are missing and sequential scans are happening frequently.
109
-
110
- ## Failure Mode Guide
111
-
112
- | Symptom | Likely Cause | Action |
113
- |---------|-------------|--------|
114
- | Query takes >1s | Missing index on WHERE column | Check pg_indexes, add index |
115
- | Same query 50x in log | N+1 in controller/view | Add `.includes()` |
116
- | Query slow AFTER adding index | Wrong index type or order | Verify index column order matches query |
117
- | `EXPLAIN ANALYZE` hanging | Table lock or very large table | Use `EXPLAIN (BUFFERS)` without ANALYZE |
118
- | Missing `pg_indexes` output | Not Postgres | Use `SHOW INDEX FROM <table>` for MySQL |
@@ -1,125 +0,0 @@
1
- ---
2
- name: rails.deploy_pipeline
3
- description: Pre-deployment checklist for Rails applications — migrations, assets, credentials, jobs, and logs
4
- ---
5
-
6
- Use this skill before or during a Rails deployment to ensure nothing is missed.
7
- Follow the steps in order — each builds on the previous.
8
-
9
- ## Step 1: Check Pending Migrations
10
-
11
- Before deploying, always check for unapplied migrations:
12
-
13
- ```ruby
14
- RunCommand.new.call(command: "bin/rails db:migrate:status | grep 'down'")
15
- ```
16
-
17
- If there are pending migrations:
18
- 1. Review them with `ReadFile.new.call(path: "db/migrate/<TIMESTAMP>_migration_name.rb")`
19
- 2. Verify they're reversible: check `change`, `up`/`down`, or `reversible` blocks
20
- 3. Check for data migrations (e.g., backfills) that should run separately
21
- 4. Estimate execution time — large tables may need locking strategy
22
-
23
- ## Step 2: Verify Assets Pipeline
24
-
25
- For Rails with assets (Sprockets or Propshaft):
26
-
27
- ```ruby
28
- # Precompile locally to catch errors early
29
- RunCommand.new.call(command: "bin/rails assets:precompile 2>&1")
30
- ```
31
-
32
- Check for:
33
- - Missing asset references (SCSS variables, image references)
34
- - JavaScript compilation errors
35
- - Asset fingerprint changes (manifold fingerprints if CSS changed)
36
-
37
- For importmap or esbuild/vite setups:
38
-
39
- ```ruby
40
- # Check build config
41
- ReadFile.new.call(path: "package.json")
42
- ReadFile.new.call(path: "vite.config.ts") if File.exist?("Rails.root.join('vite.config.ts')")
43
- ```
44
-
45
- ## Step 3: Review Credentials and Secrets
46
-
47
- Verify that all required credentials exist in the target environment:
48
-
49
- ```ruby
50
- # For production credentials
51
- RunCommand.new.call(command: "bin/rails credentials:show --environment production 2>&1")
52
- ```
53
-
54
- Check for common secrets that may need updating:
55
- - `secret_key_base`
56
- - `database_password`
57
- - Third-party API keys (AWS, Stripe, SendGrid, etc.)
58
- - JWT signing keys
59
- - Any env vars referenced in `config/` files that aren't in credentials
60
-
61
- ## Step 4: Check Background Jobs
62
-
63
- Review sidekiq/active job configuration before deploy:
64
-
65
- ```ruby
66
- # Check job files for any that need queue configuration
67
- Glob.new.call(pattern: "app/jobs/**/*.rb")
68
- ```
69
-
70
- For any new or modified jobs:
71
- 1. Verify the queue adapter is configured in `production.rb`
72
- 2. Check for job retry logic that might affect rollback
73
- 3. Review `ReadFile.new.call(path: "config/sidekiq.yml")` if using Sidekiq
74
- 4. Verify scheduled/cron jobs if using `sidekiq-cron` or `whenever`
75
-
76
- ## Step 5: Review Production Log for Pre-deploy Issues
77
-
78
- Check that the current production environment is healthy before deploying:
79
-
80
- ```ruby
81
- ReadLog.new.call(lines: 100, level: "ERROR")
82
- ```
83
-
84
- If there are recent errors, investigate before deploying more changes.
85
-
86
- ## Step 6: Verify Dependencies and Gem Versions
87
-
88
- Check for critical gem updates or changes:
89
-
90
- ```ruby
91
- # Review Gemfile for new/modified gems
92
- RunCommand.new.call(command: "git diff HEAD -- Gemfile")
93
- ```
94
-
95
- If adding a gem that needs system dependencies or native extensions:
96
- ```ruby
97
- RunCommand.new.call(command: "bundle platform")
98
- ```
99
-
100
- ## Step 7: Config File Checklist
101
-
102
- Verify configuration files for the target environment:
103
-
104
- ```ruby
105
- ReadFile.new.call(path: "config/environments/production.rb")
106
- ReadFile.new.call(path: "config/database.yml")
107
- ReadFile.new.call(path: "config/cable.yml") if File.exist?("config/cable.yml")
108
- ReadFile.new.call(path: "config/storage.yml") if File.exist?("config/storage.yml")
109
- ```
110
-
111
- Key production checks:
112
- - `config.force_ssl = true`
113
- - `config.consider_all_requests_local = false`
114
- - Proper cache store configured (`:mem_cache_store`, `:redis_cache_store`)
115
- - Active Storage service configured for production
116
-
117
- ## Step 8: Quick Rollback Checklist
118
-
119
- Before deploying, know how to roll back:
120
-
121
- 1. **Database**: `RunCommand.new.call(command: "bin/rails db:migrate:down VERSION=<previous>")`
122
- 2. **Code**: Git revert the deploy commit
123
- 3. **Assets**: Previous version's assets should still be cached
124
- 4. **Jobs**: Check if backward-incompatible changes won't replay failed jobs
125
- 5. **Feature flags**: If using flipper or similar, toggle off new features first
@@ -1,127 +0,0 @@
1
- ---
2
- name: rails.route_trouble
3
- description: Step-by-step methodology for debugging routing issues in Rails
4
- ---
5
-
6
- Use this skill when a route is returning 404, you're getting route matching errors,
7
- or routes aren't behaving as expected in a Rails application.
8
-
9
- ## Step 1: Read the Routes File
10
-
11
- Start by examining the routes configuration:
12
-
13
- ```ruby
14
- ReadRoutes.new.call
15
- ```
16
-
17
- This reads `config/routes.rb` — the source of truth for all route definitions.
18
-
19
- Look for:
20
- - The overall structure (namespaced, nested, shallow routes?)
21
- - Resources that might be missing
22
- - Constraints or conditions that could block matching
23
- - Route ordering (more specific routes should come before less specific)
24
-
25
- ## Step 2: Check the Compiled Routes
26
-
27
- Rails routes have a specific matching order. Use `RunCommand` to inspect the live routes:
28
-
29
- ```ruby
30
- RunCommand.new.call(command: "bin/rails routes")
31
- ```
32
-
33
- If you know the path you're trying to match, grep for it:
34
-
35
- ```ruby
36
- RunCommand.new.call(command: "bin/rails routes | grep users")
37
- ```
38
-
39
- For a specific route name:
40
-
41
- ```ruby
42
- RunCommand.new.call(command: "bin/rails routes --grep user_profile")
43
- ```
44
-
45
- ## Step 3: Check Route Parameters and Constraints
46
-
47
- Routes often fail because of parameter mismatches or constraints. Check for:
48
-
49
- **Required parameters:** Does the route define `:id` but the URL doesn't include it?
50
-
51
- **Format constraints:** Routes with `:format` (like `.json`, `.html`) constraints
52
- can fail silently. Check if there's a default format:
53
-
54
- ```ruby
55
- # In routes.rb:
56
- resources :posts, defaults: { format: :json }
57
- ```
58
-
59
- **Constraint classes:** Custom route constraints like `subdomain` or request-based
60
- constraints can prevent matching:
61
-
62
- ```ruby
63
- # In routes.rb:
64
- get "admin", to: "admin/dashboard#show", constraints: ->(req) { req.subdomain == "admin" }
65
- ```
66
-
67
- Check the constraints against the actual request parameters.
68
-
69
- ## Step 4: Trace the Match
70
-
71
- For a failing request, trace how Rails would match it:
72
-
73
- ```ruby
74
- RunCommand.new.call(
75
- command: "bin/rails runner \"puts Rails.application.routes.recognize_path('/users/1/edit', method: :get)\""
76
- )
77
- ```
78
-
79
- This will raise `ActionController::RoutingError` if no route matches — the
80
- error message tells you what routes were tried before failing.
81
-
82
- If the route exists but the controller isn't found:
83
-
84
- ```ruby
85
- ReadFile.new.call(path: "app/controllers/users_controller.rb")
86
- ```
87
-
88
- ## Step 5: Check Namespace and Module Nesting
89
-
90
- Routes in namespaced controllers fail when the controller file is in the wrong
91
- directory or the module is misnamed:
92
-
93
- ```ruby
94
- # Route:
95
- namespace :admin do
96
- resources :users
97
- end
98
-
99
- # Expects:
100
- # app/controllers/admin/users_controller.rb
101
- # class Admin::UsersController < ApplicationController
102
- ```
103
-
104
- Use `Glob` to verify the controller exists:
105
-
106
- ```ruby
107
- # Via RunCommand:
108
- RunCommand.new.call(command: "ls app/controllers/admin/")
109
- ```
110
-
111
- ## Step 6: Verify Helper Paths and Named Routes
112
-
113
- If you're debugging a `No route matches` error from a view (link_to, form_with):
114
-
115
- 1. Check the route name: `RunCommand.new.call(command: "bin/rails routes | grep user_path")`
116
- 2. Verify the route helper: `RunCommand.new.call(command: "bin/rails routes --grep user")`
117
- 3. Check for route helper overrides in custom constraints or defaults
118
-
119
- ## Failure Mode Guide
120
-
121
- | Symptom | Likely Cause | Action |
122
- |---------|-------------|--------|
123
- | 404 for existing route | Route constraint blocking | Check `constraints:` block in routes.rb |
124
- | `No route matches` | Missing resource definition | Add `resources :model_name` |
125
- | Route works in dev, not prod | Different route ordering | Check routes file isn't conditionally loaded |
126
- | `Missing controller` | Wrong namespace or filename | Verify file path matches module structure |
127
- | Path helper not responding | Wrong route name or params mismatch | Check `bin/rails routes --grep helper_name` |
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
-
5
- module Ask
6
- module Rails
7
- module Generators
8
- class InstallGenerator < ::Rails::Generators::Base
9
- source_root File.expand_path("templates", __dir__)
10
-
11
- desc "Creates ask-rails configuration and migration"
12
-
13
- def create_initializer
14
- template "initializer.rb", "config/initializers/ask_rails.rb"
15
- end
16
-
17
- def create_migration
18
- timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
19
- template "migration.rb", "db/migrate/#{timestamp}_create_ask_sessions.rb"
20
- end
21
-
22
- def create_tools_directory
23
- empty_directory "app/tools"
24
- create_file "app/tools/.keep", ""
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,5 +0,0 @@
1
- Ask::Rails.configure do |config|
2
- config.default_model = ENV.fetch("ASK_DEFAULT_MODEL", "gpt-4o")
3
- config.max_turns = ENV.fetch("ASK_MAX_TURNS", 25).to_i
4
- # config.persistence_adapter = Ask::Rails::Persistence.new
5
- end
@@ -1,12 +0,0 @@
1
- class CreateAskSessions < ActiveRecord::Migration[7.1]
2
- def change
3
- create_table :ask_sessions do |t|
4
- t.string :session_id, null: false
5
- t.string :model
6
- t.jsonb :data, default: {}
7
- t.timestamps
8
- end
9
-
10
- add_index :ask_sessions, :session_id, unique: true
11
- end
12
- end