Commit d4a6d11
Changed files (12)
lib/elelem/tools/compact.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:compact) do |agent|
+ agent.commands.register("compact", description: "Compress context") do
+ response = agent.turn("Summarize: accomplishments, state, next steps. Brief.")
+ agent.conversation.clear!
+ agent.conversation.add(role: "user", content: "Context: #{response}")
+ agent.terminal.say " → compacted"
+ end
+end
lib/elelem/tools/edit.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "pathname"
+
+Elelem::Plugins.register(:edit) do |agent|
+ agent.toolbox.add("edit",
+ description: "Replace first occurrence of text in file",
+ params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
+ required: ["path", "old", "new"]
+ ) do |a|
+ path = Pathname.new(a["path"]).expand_path
+ content = path.read
+ agent.toolbox
+ .run("write", { "path" => a["path"], "content" => content.sub(a["old"], a["new"]) })
+ .merge(replaced: a["old"], with: a["new"])
+ end
+end
lib/elelem/tools/eval.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:eval) do |agent|
+ description = <<~'DESC'
+ Evaluate Ruby code. Available API:
+
+ name = "search"
+ agent.toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
+ agent.toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
+ end
+ DESC
+
+ agent.toolbox.add("eval",
+ description: description,
+ params: { ruby: { type: "string" } },
+ required: ["ruby"]
+ ) do |args|
+ { result: binding.eval(args["ruby"]) }
+ end
+end
lib/elelem/tools/git.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:git) do |agent|
+ allowed = %w[status diff log show branch checkout add reset stash].freeze
+
+ agent.toolbox.add("git",
+ description: "Run git command",
+ params: { command: { type: "string" }, args: { type: "array", items: { type: "string" } } },
+ required: ["command"]
+ ) do |a|
+ cmd = a["command"]
+ next { error: "not allowed: #{cmd}" } unless allowed.include?(cmd)
+
+ agent.toolbox.exec("git", cmd, *(a["args"] || []))
+ end
+
+ agent.toolbox.after("git") do |_, result|
+ agent.terminal.say " ! #{result[:error]}" if result[:error]
+ end
+end
lib/elelem/tools/glob.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:glob) do |agent|
+ agent.toolbox.add("glob",
+ description: "Find files matching pattern",
+ params: { pattern: { type: "string" }, path: { type: "string" } },
+ required: ["pattern"]
+ ) do |a|
+ path = a["path"].to_s.empty? ? "." : a["path"]
+ result = agent.toolbox.exec("fd", "--glob", a["pattern"], path)
+ result[:ok] ? result : agent.toolbox.exec("find", path, "-name", a["pattern"])
+ end
+end
lib/elelem/tools/grep.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:grep) do |agent|
+ agent.toolbox.add("grep",
+ description: "Search file contents",
+ params: { pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" } },
+ required: ["pattern"]
+ ) do |a|
+ path = a["path"].to_s.empty? ? "." : a["path"]
+ glob = a["glob"]
+ rg_args = ["rg", "-n", a["pattern"], path]
+ rg_args += ["-g", glob] if glob
+ result = agent.toolbox.exec(*rg_args)
+ next result if result[:ok]
+
+ grep_args = ["grep", "-rn"]
+ grep_args += ["--include", glob] if glob
+ grep_args += [a["pattern"], path]
+ agent.toolbox.exec(*grep_args)
+ end
+end
lib/elelem/tools/init.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:init) do |agent|
+ agent.commands.register("init", description: "Generate AGENTS.md") do
+ system_prompt = <<~PROMPT
+ AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
+
+ # AGENTS.md Spec (https://agents.md/)
+ A file providing context and instructions for AI coding agents.
+
+ ## Recommended Sections
+ - Commands: build, test, lint commands
+ - Code Style: conventions, patterns
+ - Architecture: key components and flow
+ - Testing: how to run tests
+
+ ## Process
+ 1. Read README.md if present
+ 2. Identify language (Gemfile, package.json, go.mod)
+ 3. Find test scripts (bin/test, npm test)
+ 4. Check linter configs
+ 5. Write concise AGENTS.md
+
+ Keep it minimal. No fluff.
+ PROMPT
+
+ agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
+ end
+end
lib/elelem/tools/interview.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:interview) do |agent|
+ agent.toolbox.add("interview",
+ description: "Ask the user a question and wait for their response",
+ params: {
+ question: { type: "string", description: "The question to ask the user" },
+ },
+ required: ["question"]
+ ) do |args|
+ agent.terminal.say(agent.terminal.markdown(args["question"]))
+ { answer: agent.terminal.ask("> ") }
+ end
+end
lib/elelem/tools/list.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:list) do |agent|
+ agent.toolbox.add("list",
+ description: "List directory contents",
+ params: { path: { type: "string" }, recursive: { type: "boolean" } },
+ required: [],
+ aliases: ["ls"]
+ ) do |a|
+ path = a["path"] && !a["path"].empty? ? a["path"] : "."
+ flags = a["recursive"] ? "-laR" : "-la"
+ agent.toolbox.exec("ls", flags, path)
+ end
+end
lib/elelem/tools/shell.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "tempfile"
+
+Elelem::Plugins.register(:shell) do |agent|
+ strip_ansi = ->(text) do
+ text
+ .gsub(/^Script started.*?\n/, "")
+ .gsub(/\nScript done.*$/, "")
+ .gsub(/\e\].*?(?:\a|\e\\)/, "")
+ .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
+ .gsub(/\e[PX^_].*?\e\\/, "")
+ .gsub(/\e./, "")
+ .gsub(/[\b]/, "")
+ .gsub(/\r/, "")
+ end
+
+ agent.commands.register("shell", description: "Start interactive shell") do
+ transcript = Tempfile.create do |file|
+ system("script", "-q", file.path, chdir: Dir.pwd)
+ strip_ansi.call(File.read(file.path))
+ end
+ agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
+ end
+end
lib/elelem/tools/task.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:task) do |agent|
+ agent.toolbox.add("task",
+ description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
+ params: { prompt: { type: "string" } },
+ required: ["prompt"]
+ ) do |a|
+ sub = Elelem::Agent.new(agent.client, toolbox: agent.toolbox, terminal: agent.terminal,
+ system_prompt: "Research agent. Search, analyze, report. Be concise.")
+ sub.turn(a["prompt"])
+ { result: sub.conversation.last[:content] }
+ end
+end
lib/elelem/tools/verify.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Elelem
+ module Contrib
+ module Verifiers
+ SYNTAX = {
+ ".rb" => "ruby -c %{path}",
+ ".erb" => "erb -x %{path} | ruby -c",
+ ".py" => "python -m py_compile %{path}",
+ ".go" => "go vet %{path}",
+ ".rs" => "cargo check --quiet",
+ ".ts" => "npx tsc --noEmit %{path}",
+ ".js" => "node --check %{path}",
+ }.freeze
+
+ def self.for(path)
+ return [] unless path
+
+ cmds = []
+ ext = File.extname(path)
+ cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
+ cmds << test_runner
+ cmds.compact
+ end
+
+ def self.test_runner
+ %w[bin/test script/test].find { |s| File.executable?(s) }
+ end
+ end
+ end
+
+ Plugins.register(:verify) do |agent|
+ agent.toolbox.add("verify",
+ description: "Verify file syntax and run tests",
+ params: { path: { type: "string" } },
+ required: ["path"]
+ ) do |a|
+ path = a["path"]
+ Elelem::Contrib::Verifiers.for(path).inject({ verified: [] }) do |memo, cmd|
+ agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
+ v = agent.toolbox.run("execute", { "command" => cmd })
+ break v.merge(path: path, command: cmd) if v[:exit_status] != 0
+
+ memo[:verified] << cmd
+ memo
+ end
+ end
+ end
+end