Commit cafe700

mo khan <mo@mokhan.ca>
2026-09-07 19:34:33
feat: add ToolMarkdown, absorbing Tool#summary/#to_markdown from core
Presentation logic for the /tools command; only caller of both methods, so they moved here instead of living on core's Tool domain object.
1 parent fd4b68f
Changed files (2)
lib
elelem
builtins
spec
elelem
lib/elelem/builtins/tools.rb
@@ -1,5 +1,26 @@
 # frozen_string_literal: true
 
+module Elelem
+  module Builtins
+    module ToolMarkdown
+      def self.summary(tool)
+        "| #{tool.name} | #{tool.description.lines.first.chomp} |"
+      end
+
+      def self.to_markdown(tool)
+        lines = ["## #{tool.name}", "", tool.description, "", "### Parameters", ""]
+        tool.params.each do |param, spec|
+          req = tool.required.include?(param.to_s) ? ", required" : ""
+          desc = spec[:description] ? " - #{spec[:description]}" : ""
+          lines << "- `#{param}` (#{spec[:type]}#{req})#{desc}"
+        end
+        lines << "" << "*aliases: #{tool.aliases.join(", ")}*" if tool.aliases.any?
+        lines.join("\n")
+      end
+    end
+  end
+end
+
 Elelem.configure do |config|
   config.setup(:tools) do |agent|
     completions = -> { agent.toolbox.tools.keys }
@@ -12,9 +33,9 @@ Elelem.configure do |config|
           next
         end
 
-        agent.output.say tool.to_markdown, as: :markdown
+        agent.output.say Elelem::Builtins::ToolMarkdown.to_markdown(tool), as: :markdown
       else
-        rows = agent.toolbox.tools.each_value.map(&:summary)
+        rows = agent.toolbox.tools.each_value.map { |tool| Elelem::Builtins::ToolMarkdown.summary(tool) }
 
         md = String.new("| Tool | Description |\n")
         md << "|------|-------------|\n"
spec/elelem/builtins/tool_markdown_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Builtins::ToolMarkdown do
+  subject(:tool) do
+    Elelem::Tool.new("greet",
+      description: "Greets someone.\nMore detail.",
+      params: { name: { type: "string", description: "who to greet" } },
+      required: ["name"],
+      aliases: ["hi"]
+    ) { |args| { greeting: "hi #{args["name"]}" } }
+  end
+
+  describe ".summary" do
+    it "renders a single table row with the first description line" do
+      expect(described_class.summary(tool)).to eq("| greet | Greets someone. |")
+    end
+  end
+
+  describe ".to_markdown" do
+    it "renders name, description, params, and aliases" do
+      markdown = described_class.to_markdown(tool)
+
+      expect(markdown).to include("## greet")
+      expect(markdown).to include("Greets someone.\nMore detail.")
+      expect(markdown).to include("- `name` (string, required) - who to greet")
+      expect(markdown).to include("*aliases: hi*")
+    end
+
+    it "omits the aliases line when there are none" do
+      plain_tool = Elelem::Tool.new("greet", description: "Greets someone.") { }
+
+      expect(described_class.to_markdown(plain_tool)).not_to include("aliases:")
+    end
+  end
+end