Commit 00db2f1
Changed files (8)
lib
elelem
spec
elelem
lib/elelem/plugins/execute.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:execute) do |toolbox|
+ toolbox.add("execute",
+ description: "Run shell command (supports pipes and redirections)",
+ params: { command: { type: "string" } },
+ required: ["command"],
+ aliases: ["bash", "sh", "exec"]
+ ) do |a|
+ Elelem.sh("bash", args: ["-c", a["command"]]) { |x| $stdout.print(x) }
+ end
+end
lib/elelem/plugins/mcp.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:mcp) do |toolbox|
+ mcp = Elelem::MCP.new
+ mcp.tools.each do |name, tool|
+ fn = tool[:fn]
+ toolbox.add(name,
+ description: tool[:description],
+ params: tool[:params],
+ required: tool[:required],
+ &fn
+ )
+ end
+end
lib/elelem/plugins/read.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:read) do |toolbox|
+ toolbox.add("read",
+ description: "Read file",
+ params: { path: { type: "string" } },
+ required: ["path"],
+ aliases: ["open"]
+ ) do |a|
+ path = Pathname.new(a["path"]).expand_path
+ path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
+ end
+
+ toolbox.after("read") do |_, result|
+ if result[:error]
+ $stdout.puts " ! #{result[:error]}"
+ else
+ system("bat", "--style=plain", "--paging=never", result[:path])
+ end
+ end
+end
lib/elelem/plugins/verify.rb
@@ -30,12 +30,15 @@ module Elelem
toolbox.after("write") do |_, result|
next if result[:error]
- result[:verify] = {}
Verifiers.for(result[:path]).each do |cmd|
- $stdout.puts "\n → verify: #{cmd}"
+ $stdout.puts "\n -> verify: #{cmd}"
v = Elelem.sh("bash", args: ["-c", cmd]) { |x| $stdout.print(x) }
- result[:verify][cmd] = v
- break if v[:exit_status] != 0
+ status = v[:exit_status] == 0 ? "\u2713" : "\u2717"
+ $stdout.puts " #{status} #{cmd}"
+ if v[:exit_status] != 0
+ $stdout.puts v[:content].lines.first(5).map { |l| " #{l}" }.join
+ break
+ end
end
end
end
lib/elelem/plugins/write.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:write) do |toolbox|
+ toolbox.add("write",
+ description: "Write file",
+ params: { path: { type: "string" }, content: { type: "string" } },
+ required: ["path", "content"]
+ ) do |a|
+ path = Pathname.new(a["path"]).expand_path
+ FileUtils.mkdir_p(path.dirname)
+ { bytes: path.write(a["content"]), path: a["path"] }
+ end
+
+ toolbox.after("write") do |_, result|
+ if result[:error]
+ $stdout.puts " ! #{result[:error]}"
+ else
+ $stdout.puts " -> #{result[:path]}"
+ end
+ end
+end
lib/elelem/agent.rb
@@ -13,9 +13,7 @@ module Elelem
@terminal = terminal || Terminal.new(commands: COMMANDS)
@history = history || []
@memory = nil
- @toolbox.add("task", task_tool)
- @mcp = MCP.new
- @mcp.tools.each { |name, tool| @toolbox.add(name, tool) }
+ register_task_tool
end
def repl
@@ -69,24 +67,22 @@ module Elelem
def process(tool_call)
name, args = tool_call[:name], tool_call[:arguments]
terminal.say toolbox.header(name, args)
- toolbox.run(name.to_s, args).tap do |result|
- terminal.say toolbox.format_result(name, result)
- end
+ toolbox.run(name.to_s, args)
end
- def task_tool
- {
+ def register_task_tool
+ agent = self
+ @toolbox.add("task",
description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
params: { prompt: { type: "string" } },
- required: ["prompt"],
- fn: ->(a) {
- sub = Agent.new(client, toolbox, terminal: terminal, history: [
- { role: "system", content: "Research agent. Search, analyze, report. Be concise." }
- ])
- sub.turn(a["prompt"])
- { result: sub.history.last[:content] }
- }
- }
+ required: ["prompt"]
+ ) do |a|
+ sub = Agent.new(agent.client, agent.toolbox, terminal: agent.terminal, history: [
+ { role: "system", content: "Research agent. Search, analyze, report. Be concise." }
+ ])
+ sub.turn(a["prompt"])
+ { result: sub.history.last[:content] }
+ end
end
def fetch_response(ctx)
lib/elelem/toolbox.rb
@@ -2,50 +2,17 @@
module Elelem
class Toolbox
- TOOLS = {
- "read" => {
- description: "Read file",
- params: { path: { type: "string" } },
- required: ["path"],
- fn: lambda do |a|
- path = Pathname.new(a["path"]).expand_path
- path.exist? ? { content: path.read } : { error: "not found" }
- end
- },
- "write" => {
- description: "Write file",
- params: { path: { type: "string" }, content: { type: "string" } },
- required: ["path", "content"],
- fn: lambda do |a|
- path = Pathname.new(a["path"]).expand_path
- FileUtils.mkdir_p(path.dirname)
- { bytes: path.write(a["content"]), path: a["path"] }
- end
- },
- "execute" => {
- description: "Run shell command (supports pipes and redirections)",
- params: { command: { type: "string" } },
- required: ["command"],
- fn: ->(a) { Elelem.sh("bash", args: ["-c", a["command"]]) { |x| $stdout.print(x) } }
- }
- }.freeze
+ attr_reader :tools, :hooks, :aliases
- ALIASES = {
- "bash" => "execute",
- "sh" => "execute",
- "exec" => "execute",
- "open" => "read"
- }.freeze
-
- attr_reader :tools, :hooks
-
- def initialize(tools = TOOLS.dup)
- @tools = tools
+ def initialize
+ @tools = {}
+ @aliases = {}
@hooks = { before: Hash.new { |h, k| h[k] = [] }, after: Hash.new { |h, k| h[k] = [] } }
end
- def add(name, tool)
- @tools[name] = tool
+ def add(name, description:, params: {}, required: [], aliases: [], &fn)
+ @tools[name] = { description: description, params: params, required: required, fn: fn }
+ aliases.each { |a| @aliases[a] = name }
end
def before(tool_name, &block)
@@ -62,7 +29,7 @@ module Elelem
end
def run(name, args)
- name = ALIASES.fetch(name, name)
+ name = @aliases.fetch(name, name)
tool = tools[name]
return { error: "unknown tool: #{name}" } unless tool
@@ -89,33 +56,5 @@ module Elelem
}
end
end
-
- def format_result(name, result)
- return if result[:exit_status] && !result[:verify]
-
- parts = []
- format_verify_results(parts, result[:verify]) if result[:verify]
- format_content(parts, result)
- parts.join("\n") unless parts.empty?
- end
-
- private
-
- def format_verify_results(parts, verify)
- verify.each do |cmd, v|
- status = v[:exit_status] == 0 ? "✓" : "✗"
- parts << " #{status} #{cmd}"
- next if v[:exit_status] == 0
-
- parts << v[:content].lines.first(5).map { |l| " #{l}" }.join
- end
- end
-
- def format_content(parts, result)
- text = result[:content] || result[:error] || ""
- return if text.strip.empty?
-
- parts << (result[:error] ? " ! #{text.lines.first&.strip}" : text)
- end
end
end
spec/elelem/toolbox_spec.rb
@@ -3,6 +3,28 @@
RSpec.describe Elelem::Toolbox do
subject { described_class.new }
+ before do
+ subject.add("read",
+ description: "Read file",
+ params: { path: { type: "string" } },
+ required: ["path"],
+ aliases: ["open"]
+ ) { |a| { content: File.read(a["path"]) } }
+
+ subject.add("write",
+ description: "Write file",
+ params: { path: { type: "string" }, content: { type: "string" } },
+ required: ["path", "content"]
+ ) { |a| { bytes: File.write(a["path"], a["content"]) } }
+
+ subject.add("execute",
+ description: "Run shell command",
+ params: { command: { type: "string" } },
+ required: ["command"],
+ aliases: ["bash", "sh", "exec"]
+ ) { |a| { output: `#{a["command"]}` } }
+ end
+
describe "#to_a" do
it "returns all tools in API format" do
tool_names = subject.to_a.map { |t| t.dig(:function, :name) }