Commit d4a6d11

mo khan <mo@mokhan.ca>
2026-09-05 20:58:09
refactor: split features into separate gems
1 parent cebb104
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