rails-agent-stack 0.1.1

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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/README.md +16 -0
  3. data/.github/workflows/ci.yml +32 -0
  4. data/.gitignore +8 -0
  5. data/.rspec +3 -0
  6. data/AGENTS.md +18 -0
  7. data/CHANGELOG.md +40 -0
  8. data/Gemfile +15 -0
  9. data/MIT-LICENSE +21 -0
  10. data/README.md +228 -0
  11. data/Rakefile +8 -0
  12. data/app/assets/images/rails_agents/kip-logo.png +0 -0
  13. data/app/assets/images/rails_agents/kip-waving.png +0 -0
  14. data/app/assets/stylesheets/rails_agents/application.css +411 -0
  15. data/app/controllers/rails_agents/application_controller.rb +25 -0
  16. data/app/controllers/rails_agents/connect_controller.rb +36 -0
  17. data/app/controllers/rails_agents/dashboard_controller.rb +70 -0
  18. data/app/controllers/rails_agents/handshake_controller.rb +55 -0
  19. data/app/controllers/rails_agents/open_wire_inbound_controller.rb +153 -0
  20. data/app/controllers/rails_agents/pulls_controller.rb +42 -0
  21. data/app/controllers/rails_agents/registrations_controller.rb +96 -0
  22. data/app/controllers/rails_agents/schema_controller.rb +123 -0
  23. data/app/controllers/rails_agents/sessions_controller.rb +76 -0
  24. data/app/controllers/rails_agents/tool_bridge_controller.rb +67 -0
  25. data/app/views/layouts/rails_agents/application.html.erb +18 -0
  26. data/app/views/rails_agents/dashboard/show.html.erb +139 -0
  27. data/app/views/rails_agents/registrations/new.html.erb +83 -0
  28. data/app/views/rails_agents/sessions/new.html.erb +73 -0
  29. data/app/views/rails_agents/shared/_auth_brand.html.erb +20 -0
  30. data/config/routes.rb +34 -0
  31. data/docs/AGENTS.md +220 -0
  32. data/docs/PRD.md +89 -0
  33. data/docs/assets/hero-banner.png +0 -0
  34. data/examples/support/agent.rb +14 -0
  35. data/examples/support/channels/.keep +0 -0
  36. data/examples/support/evals/smoke.yml +4 -0
  37. data/examples/support/knowledge/shipping.md +1 -0
  38. data/examples/support/memory.rb +3 -0
  39. data/examples/support/prompt.md +9 -0
  40. data/examples/support/skills/.keep +0 -0
  41. data/examples/support/tools/.keep +0 -0
  42. data/exe/rails-agents +6 -0
  43. data/lib/generators/rails_agents/agent/agent_generator.rb +80 -0
  44. data/lib/generators/rails_agents/agent/templates/agent.rb.tt +14 -0
  45. data/lib/generators/rails_agents/agent/templates/agent_database.rb.tt +42 -0
  46. data/lib/generators/rails_agents/agent/templates/channels/slack.rb.tt +8 -0
  47. data/lib/generators/rails_agents/agent/templates/evals/smoke.yml.tt +4 -0
  48. data/lib/generators/rails_agents/agent/templates/imports.yml.tt +9 -0
  49. data/lib/generators/rails_agents/agent/templates/memory.rb.tt +6 -0
  50. data/lib/generators/rails_agents/agent/templates/prompt.md.tt +10 -0
  51. data/lib/generators/rails_agents/agent/templates/prompt_database.md.tt +14 -0
  52. data/lib/generators/rails_agents/install/INSTALL +10 -0
  53. data/lib/generators/rails_agents/install/install_generator.rb +72 -0
  54. data/lib/generators/rails_agents/install/templates/AGENTS.md.tt +46 -0
  55. data/lib/generators/rails_agents/install/templates/initializer.rb.tt +8 -0
  56. data/lib/generators/rails_agents/install/templates/library_README.md.tt +28 -0
  57. data/lib/generators/rails_agents/install/templates/library_manifest.yml.tt +5 -0
  58. data/lib/generators/rails_agents/install/templates/rails_agents_autoload.rb.tt +10 -0
  59. data/lib/rails-agent-stack.rb +4 -0
  60. data/lib/rails_agents/background_agent.rb +15 -0
  61. data/lib/rails_agents/base.rb +294 -0
  62. data/lib/rails_agents/chat_agent.rb +15 -0
  63. data/lib/rails_agents/cli.rb +245 -0
  64. data/lib/rails_agents/client.rb +216 -0
  65. data/lib/rails_agents/configuration.rb +41 -0
  66. data/lib/rails_agents/creation_agent.rb +10 -0
  67. data/lib/rails_agents/credentials_writer.rb +38 -0
  68. data/lib/rails_agents/engine.rb +19 -0
  69. data/lib/rails_agents/knowledge_agent.rb +10 -0
  70. data/lib/rails_agents/library_imports.rb +163 -0
  71. data/lib/rails_agents/local_auth.rb +56 -0
  72. data/lib/rails_agents/local_sync.rb +113 -0
  73. data/lib/rails_agents/monitoring_agent.rb +10 -0
  74. data/lib/rails_agents/open_wire_adapter.rb +31 -0
  75. data/lib/rails_agents/operations_agent.rb +10 -0
  76. data/lib/rails_agents/railtie.rb +14 -0
  77. data/lib/rails_agents/tasks.rake +8 -0
  78. data/lib/rails_agents/version.rb +5 -0
  79. data/lib/rails_agents/workflow_agent.rb +11 -0
  80. data/lib/rails_agents.rb +57 -0
  81. data/rails-agent-stack.gemspec +49 -0
  82. data/rails-agents.code-workspace +25 -0
  83. data/spec/generators/agent_generator_spec.rb +36 -0
  84. data/spec/rails_agents/base_spec.rb +202 -0
  85. data/spec/rails_agents/client_spec.rb +121 -0
  86. data/spec/rails_agents/configuration_spec.rb +55 -0
  87. data/spec/rails_agents/library_imports_spec.rb +80 -0
  88. data/spec/rails_agents/local_sync_spec.rb +63 -0
  89. data/spec/rails_agents/taxonomy_spec.rb +52 -0
  90. data/spec/spec_helper.rb +27 -0
  91. metadata +229 -0
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "active_support/core_ext/string/inflections"
5
+
6
+ module RailsAgents
7
+ # Prefer product-facing typed subclasses: KnowledgeAgent, WorkflowAgent,
8
+ # OperationsAgent, or MonitoringAgent.
9
+ class Base
10
+ class << self
11
+ attr_reader :agent_name, :model_setting, :model_provider, :model_credential,
12
+ :memory_setting, :memory_provider, :memory_recall, :knowledge_glob,
13
+ :tool_definitions, :skill_definitions, :channel_definitions
14
+
15
+ def agent_kind
16
+ :base
17
+ end
18
+
19
+ def inherited(subclass)
20
+ super
21
+ subclass.instance_variable_set(:@tool_definitions, {})
22
+ subclass.instance_variable_set(:@skill_definitions, {})
23
+ subclass.instance_variable_set(:@channel_definitions, [])
24
+ subclass.instance_variable_set(:@channel_via, {})
25
+ subclass.instance_variable_set(:@channel_message_handler, nil)
26
+ end
27
+
28
+ # model :gpt_5_mini, provider: :openai, credential: :company_openai
29
+ # model :auto # legacy cloud-routed default (still supported)
30
+ def model(value, provider: nil, credential: nil)
31
+ @model_setting = value
32
+ @model_provider = provider
33
+ @model_credential = credential
34
+ end
35
+
36
+ # memory :conversation, provider: :mem0, recall: 5
37
+ def memory(value, provider: nil, recall: nil)
38
+ @memory_setting = value
39
+ @memory_provider = provider
40
+ @memory_recall = recall
41
+ end
42
+
43
+ def knowledge_from(glob)
44
+ @knowledge_glob = glob
45
+ end
46
+
47
+ def tool(name, &block)
48
+ @tool_definitions ||= {}
49
+ @tool_definitions[name.to_sym] = block
50
+ end
51
+
52
+ def skill(name, from:)
53
+ @skill_definitions ||= {}
54
+ @skill_definitions[name.to_sym] = from
55
+ end
56
+
57
+ # Declares a SaaS connector (manifest lives under connectors/).
58
+ # `plugin` remains as a deprecated alias.
59
+ def connector(name, **opts)
60
+ @connector_definitions ||= {}
61
+ @connector_definitions[name.to_sym] = opts
62
+ end
63
+ alias_method :plugin, :connector
64
+
65
+ def channel(kind, via: nil)
66
+ @channel_definitions ||= []
67
+ @channel_definitions << kind.to_sym
68
+ @channel_via ||= {}
69
+ @channel_via[kind.to_sym] = via.to_sym if via
70
+ end
71
+
72
+ def channel_via(kind)
73
+ (@channel_via || {})[kind.to_sym]
74
+ end
75
+
76
+ # Callback for Open-Wire (and future channel) inbound messages.
77
+ # on_channel_message { |msg| … }
78
+ def on_channel_message(&block)
79
+ @channel_message_handler = block
80
+ end
81
+
82
+ def channel_message_handler
83
+ @channel_message_handler
84
+ end
85
+
86
+ def agent_directory
87
+ @agent_directory ||= resolve_agent_directory
88
+ end
89
+
90
+ def run(message = nil, session_id: nil, client: nil, **inputs)
91
+ text = coerce_run_message(message, inputs)
92
+ new(client: client).run(text, session_id: session_id)
93
+ end
94
+
95
+ def deploy(client: nil)
96
+ new(client: client).deploy
97
+ end
98
+
99
+ def sync_knowledge(client: nil)
100
+ new(client: client).sync_knowledge
101
+ end
102
+
103
+ def coerce_run_message(message, inputs)
104
+ if inputs.any?
105
+ if !message.nil?
106
+ raise ArgumentError, "pass either a message string or keyword inputs, not both"
107
+ end
108
+
109
+ inputs.map { |key, value| "#{key}: #{value}" }.join("\n")
110
+ elsif message.nil?
111
+ raise ArgumentError, "message is required (or pass keyword inputs like order_id:)"
112
+ else
113
+ message.to_s
114
+ end
115
+ end
116
+ private :coerce_run_message
117
+
118
+ def resolve_agent_directory
119
+ caller_path = Pathname.new(caller_locations(1, 1).first.absolute_path)
120
+ caller_path.dirname
121
+ end
122
+ private :resolve_agent_directory
123
+ end
124
+
125
+ def initialize(client: nil)
126
+ @client = client || Client.new
127
+ end
128
+
129
+ def run(message = nil, session_id: nil, **inputs)
130
+ text = self.class.send(:coerce_run_message, message, inputs)
131
+ response = @client.create_run(
132
+ agent: agent_identifier,
133
+ message: text,
134
+ session_id: session_id,
135
+ metadata: agent_metadata
136
+ )
137
+
138
+ RunResult.new(response, client: @client)
139
+ end
140
+
141
+ def deploy
142
+ @client.deploy(agent: agent_identifier, bundle_path: self.class.agent_directory.to_s)
143
+ end
144
+
145
+ def sync_knowledge
146
+ paths = knowledge_paths
147
+ @client.sync_knowledge(agent: agent_identifier, paths: paths)
148
+ end
149
+
150
+ def agent_metadata
151
+ {
152
+ kind: self.class.agent_kind,
153
+ model: model_metadata,
154
+ memory: memory_metadata,
155
+ tools: self.class.tool_definitions&.keys || [],
156
+ skills: self.class.skill_definitions || {},
157
+ channels: self.class.channel_definitions || []
158
+ }
159
+ end
160
+
161
+ def model_metadata
162
+ setting = self.class.model_setting
163
+ provider = self.class.model_provider
164
+ credential = self.class.model_credential
165
+
166
+ if provider.nil? && credential.nil?
167
+ setting
168
+ else
169
+ {
170
+ name: setting,
171
+ provider: provider,
172
+ credential: credential
173
+ }.compact
174
+ end
175
+ end
176
+
177
+ def memory_metadata
178
+ setting = self.class.memory_setting
179
+ provider = self.class.memory_provider
180
+ recall = self.class.memory_recall
181
+
182
+ if provider.nil? && recall.nil?
183
+ setting
184
+ else
185
+ {
186
+ type: setting,
187
+ provider: provider,
188
+ recall: recall
189
+ }.compact
190
+ end
191
+ end
192
+
193
+ def agent_identifier
194
+ if self.class.name && !self.class.name.empty?
195
+ self.class.name.underscore
196
+ else
197
+ self.class.agent_directory.basename.to_s
198
+ end
199
+ end
200
+
201
+ def knowledge_paths
202
+ glob = self.class.knowledge_glob
203
+ return [] unless glob
204
+
205
+ dir = self.class.agent_directory
206
+ local = Dir.glob(dir.join(glob).to_s).map do |path|
207
+ Pathname.new(path).relative_path_from(dir).to_s
208
+ end
209
+ (local + LibraryImports.new(dir).knowledge_paths).uniq
210
+ end
211
+
212
+ class RunResult
213
+ attr_reader :response, :client
214
+
215
+ def initialize(response, client:)
216
+ @response = response
217
+ @client = client
218
+ end
219
+
220
+ def run_id
221
+ response["id"] ||
222
+ response["run_id"] ||
223
+ dig_data("id")
224
+ end
225
+
226
+ def stream(&block)
227
+ client.stream_run(run_id, &block)
228
+ end
229
+
230
+ # Prefer the sync create_run body; otherwise drain the SSE stream.
231
+ # Legacy alias for output_text (Markdown final answer).
232
+ def output
233
+ text = output_text
234
+ return text if text.is_a?(String) && !text.empty?
235
+
236
+ final = nil
237
+ chunks = []
238
+ stream do |event|
239
+ # Prefer terminal / normalized payloads (done / output events).
240
+ if event["output_text"].is_a?(String) && !event["output_text"].empty?
241
+ final = event["output_text"]
242
+ elsif event["output"].is_a?(String) && !event["output"].empty? &&
243
+ (event.key?("items") || event.key?("format") || event.key?("status"))
244
+ final = event["output"]
245
+ else
246
+ piece = event["content"] || event["text"]
247
+ chunks << piece if piece.is_a?(String) && !piece.empty?
248
+ end
249
+ end
250
+ final || chunks.join
251
+ end
252
+
253
+ # Eve-aligned: final assistant Markdown (provider-agnostic).
254
+ def output_text
255
+ response["output_text"] ||
256
+ dig_data("output_text") ||
257
+ response["output"] ||
258
+ dig_data("output")
259
+ end
260
+
261
+ # Optional structured final payload (when a schema/result was requested).
262
+ def output_data
263
+ response["output_data"] || dig_data("output_data")
264
+ end
265
+
266
+ # Typed turn items: message (+ optional result). Tool trail lives on #trace.
267
+ def items
268
+ response["items"] || dig_data("items") || []
269
+ end
270
+
271
+ def format
272
+ response["format"] || dig_data("format") || "markdown"
273
+ end
274
+
275
+ def status
276
+ response["status"] || dig_data("status")
277
+ end
278
+
279
+ def trace
280
+ response["trace"] || dig_data("trace")
281
+ end
282
+
283
+ private
284
+
285
+ def dig_data(key)
286
+ data = response["data"]
287
+ data.is_a?(Hash) ? data[key] : nil
288
+ end
289
+ end
290
+ end
291
+
292
+ # Alias for PRD compatibility
293
+ RailsAgent = RailsAgents unless defined?(RailsAgent)
294
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ # @deprecated Use {KnowledgeAgent} for conversational / Q&A agents.
5
+ class ChatAgent < KnowledgeAgent
6
+ def self.inherited(subclass)
7
+ super
8
+ warn "[RailsAgents] ChatAgent is deprecated; use KnowledgeAgent instead.", uplevel: 1
9
+ end
10
+
11
+ def self.agent_kind
12
+ :knowledge
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "io/console"
5
+
6
+ module RailsAgents
7
+ class CLI < Thor
8
+ package_name "rails-agents"
9
+
10
+ desc "install", "Mount the engine and write the initializer"
11
+ def install
12
+ exec_rails_generator("rails_agents:install")
13
+ end
14
+
15
+ desc "new NAME", "Scaffold a new agent directory"
16
+ def new(name)
17
+ exec_rails_generator("rails_agents:agent", name)
18
+ end
19
+
20
+ desc "login", "Sign in to Rails Agent Cloud and print export lines for your API key"
21
+ method_option :email, type: :string, aliases: "-e"
22
+ method_option :workspace, type: :string, aliases: "-w"
23
+ def login
24
+ email = options[:email] || ask("Email:")
25
+ password = $stdin.tty? ? $stdin.getpass("Password: ") : ask("Password:")
26
+ workspace = options[:workspace]
27
+
28
+ response = Client.new.handshake(
29
+ email: email,
30
+ password: password,
31
+ workspace: workspace
32
+ )
33
+ data = response["data"] || response
34
+ api_key = data["api_key"] || data["apiKey"]
35
+ api_key = api_key["token"] if api_key.is_a?(Hash)
36
+ project_id = data["project_id"] || data["projectId"] || data.dig("project", "id")
37
+ dashboard_url = data["dashboard_url"] || data["dashboardUrl"]
38
+
39
+ say ""
40
+ say "Signed in to Rails Agent Cloud."
41
+ say "Cloud host is fixed — only these belong in .env (or production env):"
42
+ say ""
43
+ say "export RAILS_AGENTS_API_KEY=#{api_key}" if api_key.present?
44
+ say "export RAILS_AGENTS_PROJECT_ID=#{project_id}" if project_id.present?
45
+ say ""
46
+ say "Get or rotate keys anytime: https://cloud.rails-agent.com/dashboard/keys"
47
+ say "Then visit /agents in your Rails app."
48
+ if dashboard_url.present?
49
+ say "Dashboard: #{dashboard_url.to_s.split("?").first}"
50
+ end
51
+ rescue Client::Error => e
52
+ say_error e.message
53
+ exit 1
54
+ end
55
+
56
+ map "run" => :execute
57
+
58
+ desc "execute NAME [MESSAGE]", "Run an agent against the cloud runtime"
59
+ method_option :session, type: :string, desc: "Session ID for conversation memory"
60
+ def execute(name, message = nil)
61
+ agent_class = load_agent_class(name)
62
+ message ||= $stdin.tty? ? Thor::Shell::Basic.new.ask("Message:") : $stdin.read
63
+
64
+ result = agent_class.run(message, session_id: options[:session])
65
+ say result.output
66
+ rescue LoadError, NameError => e
67
+ say_error "Could not load agent #{name}: #{e.message}"
68
+ exit 1
69
+ rescue Client::Error => e
70
+ say_error e.message
71
+ exit 1
72
+ end
73
+
74
+ desc "deploy [NAME]", "Deploy agent bundle(s) to Rails Agent Cloud"
75
+ def deploy(name = nil)
76
+ agents = name ? [name] : discover_agent_names
77
+ agents.each do |agent_name|
78
+ agent_class = load_agent_class(agent_name)
79
+ response = agent_class.deploy
80
+ say "Deployed #{agent_name}: #{response["id"] || response["status"] || "ok"}"
81
+ end
82
+ rescue Client::Error => e
83
+ say_error e.message
84
+ exit 1
85
+ end
86
+
87
+ desc "sync NAME", "Push local agent files to the cloud"
88
+ def sync(name)
89
+ agent_class = load_agent_class(name)
90
+ dir = agent_class.agent_directory
91
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
92
+ files = Dir[File.join(dir, "**", "*")].select { |p| File.file?(p) }.map do |path|
93
+ rel = path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
94
+ rel = rel.sub(%r{\A.*?app/agents/}, "app/agents/") if rel.include?("app/agents/")
95
+ { "path" => rel, "content" => File.read(path) }
96
+ end
97
+ imported = LibraryImports.new(dir).virtual_files
98
+ local_paths = files.to_h { |file| [file["path"], true] }
99
+ conflicts = imported.filter_map do |file|
100
+ file["path"] if local_paths[file["path"]]
101
+ end
102
+ unless conflicts.empty?
103
+ raise LibraryImports::Error,
104
+ "Library imports conflict with agent-local files: #{conflicts.join(", ")}"
105
+ end
106
+ files.concat(
107
+ imported.map { |file| { "path" => file["path"], "content" => file["content"] } }
108
+ )
109
+
110
+ Client.new.sync_files(agent: name, files: files)
111
+ suffix = imported.empty? ? "" : " (#{imported.size} from app/agents_library)"
112
+ say "Synced #{files.size} files for #{name}#{suffix}"
113
+ rescue LibraryImports::Error => e
114
+ say_error e.message
115
+ exit 1
116
+ rescue Client::Error => e
117
+ say_error e.message
118
+ exit 1
119
+ end
120
+
121
+ desc "pull AGENT_ID_OR_SLUG", "[deprecated] Pull agent files from cloud into app/agents/<slug>/"
122
+ def pull(agent_id)
123
+ warn "[RailsAgents] `rails-agents pull` is deprecated; scaffold locally and use `rails-agents sync`."
124
+ result = LocalSync.new.pull!(agent_id)
125
+ say "Pulled #{result["count"]} files into app/agents/#{result["slug"]}/"
126
+ Array(result["written"]).each { |path| say " #{path}" }
127
+ rescue LocalSync::Error, Client::Error => e
128
+ say_error e.message
129
+ exit 1
130
+ end
131
+
132
+ desc "packages QUERY", "Search Skills.sh / APM / Smithery packages"
133
+ method_option :registry, type: :string, default: "all",
134
+ desc: "all | skills_sh | apm | smithery"
135
+ method_option :limit, type: :numeric, default: 12
136
+ def packages(query)
137
+ response = Client.new.search_packages(
138
+ query: query,
139
+ registry: options[:registry],
140
+ limit: options[:limit]
141
+ )
142
+ rows = response["data"] || []
143
+ if rows.empty?
144
+ say "No packages found for #{query.inspect}"
145
+ return
146
+ end
147
+ rows.each do |row|
148
+ installs = row["installs"] ? " (#{row["installs"]} installs)" : ""
149
+ say "#{row["registry"]} #{row["name"]}#{installs}"
150
+ say " #{row["id"]}"
151
+ say " #{row["description"]}"
152
+ say ""
153
+ end
154
+ rescue Client::Error => e
155
+ say_error e.message
156
+ exit 1
157
+ end
158
+
159
+ desc "add-package ID", "Install a registry package onto an agent (and Library)"
160
+ method_option :agent, type: :string, required: true, aliases: "-a"
161
+ method_option :registry, type: :string, required: true,
162
+ desc: "skills_sh | apm | smithery"
163
+ method_option :name, type: :string
164
+ method_option :source, type: :string
165
+ method_option :description, type: :string
166
+ def add_package(id)
167
+ payload = {
168
+ registry: options[:registry],
169
+ id: id,
170
+ name: options[:name] || id.split("/").last,
171
+ source: options[:source] || id.sub(/\A(skills_sh|apm|smithery):/, ""),
172
+ description: options[:description],
173
+ agent_id: options[:agent],
174
+ save_to_library: true
175
+ }.compact
176
+ response = Client.new.install_package(payload)
177
+ data = response["data"] || {}
178
+ pkg = data["package"] || {}
179
+ say "Installed #{pkg["name"] || id}"
180
+ Array(data["paths"]).each { |path| say " #{path}" }
181
+ say "Then run: bundle exec rails-agents sync #{options[:agent]}"
182
+ rescue Client::Error => e
183
+ say_error e.message
184
+ exit 1
185
+ end
186
+
187
+ desc "logs [NAME]", "Fetch recent run logs from the cloud"
188
+ method_option :limit, type: :numeric, default: 50
189
+ def logs(name = nil)
190
+ print_json Client.new.logs(agent: name, limit: options[:limit])
191
+ end
192
+
193
+ desc "traces [NAME]", "Fetch recent traces from the cloud"
194
+ method_option :limit, type: :numeric, default: 50
195
+ def traces(name = nil)
196
+ print_json Client.new.traces(agent: name, limit: options[:limit])
197
+ end
198
+
199
+ desc "evals [NAME]", "Fetch eval results from the cloud"
200
+ def evals(name = nil)
201
+ print_json Client.new.evals(agent: name)
202
+ end
203
+
204
+ default_task :help
205
+
206
+ private
207
+
208
+ def exec_rails_generator(generator, *args)
209
+ if File.exist?("bin/rails")
210
+ exec "bin/rails", "generate", generator, *args
211
+ else
212
+ say_error "Run this command from your Rails app root (bin/rails not found)."
213
+ exit 1
214
+ end
215
+ end
216
+
217
+ def load_agent_class(name)
218
+ path = Rails.root.join("app/agents/#{name}/agent.rb") if defined?(Rails)
219
+ path ||= File.expand_path("app/agents/#{name}/agent.rb", Dir.pwd)
220
+
221
+ require path
222
+ classify(name).constantize
223
+ end
224
+
225
+ def discover_agent_names
226
+ base = defined?(Rails) ? Rails.root.join("app/agents") : Pathname.new(Dir.pwd).join("app/agents")
227
+ return [] unless base.directory?
228
+
229
+ base.children.select(&:directory?).map { |d| d.basename.to_s }
230
+ end
231
+
232
+ def classify(name)
233
+ name.to_s.split(%r{[_\-/]}).map(&:capitalize).join
234
+ end
235
+
236
+ def print_json(data)
237
+ require "json"
238
+ say JSON.pretty_generate(data)
239
+ end
240
+
241
+ def say_error(message)
242
+ say message, :red
243
+ end
244
+ end
245
+ end