Commit d2c254f
Changed files (5)
lib
elelem
lib/elelem/plugins/builtins.rb
@@ -8,8 +8,30 @@ Elelem::Plugins.register(:builtins) do |agent|
agent.terminal.say " → context cleared"
end
- agent.commands.register("context", description: "Show conversation context") do
- agent.terminal.say JSON.pretty_generate(agent.context)
+ agent.commands.register("context", description: "Show conversation context") do |args|
+ messages = agent.context
+
+ case args
+ when nil, ""
+ messages.each_with_index do |msg, i|
+ role = msg[:role]
+ preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
+ preview += "..." if msg[:content].to_s.length > 60
+ agent.terminal.say " #{i + 1}. #{role}: #{preview}"
+ end
+ when "json"
+ agent.terminal.say JSON.pretty_generate(messages)
+ when /^\d+$/
+ index = args.to_i - 1
+ if index >= 0 && index < messages.length
+ content = messages[index][:content].to_s
+ agent.terminal.say(agent.terminal.markdown(content))
+ else
+ agent.terminal.say " Invalid index: #{args}"
+ end
+ else
+ agent.terminal.say " Usage: /context [json|<number>]"
+ end
end
agent.commands.register("shell", description: "Start interactive shell") do
lib/elelem/templates/system_prompt.erb
@@ -1,49 +0,0 @@
-Terminal coding agent. Be concise. Verify your work.
-
-# Tools
-- read(path): file contents
-- write(path, content): create/overwrite file
-- execute(command): shell command
-- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
-- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
-
-# Editing
-Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
-Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
-Use write for new files or full rewrites
-
-# Search
-Use execute(`rg`) for text search: `rg -n "pattern" .`
-Use execute(`fd`) for file discovery: `fd -e rb .`
-Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
-
-# Task Management
-For complex tasks:
-1. State plan before acting
-2. Work through steps one at a time
-3. Summarize what was done
-
-# Long Tasks
-For complex multi-step work, write notes to .elelem/scratch.md
-
-# Policy
-- Explain before non-trivial commands
-- Verify changes (read file, run tests)
-- No interactive flags (-i, -p)
-- Use `man` when you need to understand how to execute a program
-
-# Environment
-pwd: <%= pwd %>
-platform: <%= platform %>
-date: <%= date %>
-Reasoning: high
-self (this agent's source): <%= elelem_source %>
-<%= git_branch %>
-
-# Codebase
-<%= repo_map %>
-<% if agents_md %>
-
-# Project Instructions
-<%= agents_md %>
-<% end %>
lib/elelem/agent.rb
@@ -25,8 +25,9 @@ module Elelem
end
def command(input)
- name = input.delete_prefix("/")
- commands.run(name) || terminal.say(commands.names.join(" "))
+ parts = input.delete_prefix("/").split(" ", 2)
+ name, args = parts[0], parts[1]
+ commands.run(name, args) || terminal.say(commands.names.join(" "))
end
def context
lib/elelem/commands.rb
@@ -10,11 +10,11 @@ module Elelem
@registry[name] = { description: description, handler: handler }
end
- def run(name)
+ def run(name, args = nil)
entry = @registry[name]
return false unless entry
- entry[:handler].call
+ entry[:handler].arity == 0 ? entry[:handler].call : entry[:handler].call(args)
true
end
lib/elelem/system_prompt.rb
@@ -2,80 +2,145 @@
module Elelem
class SystemPrompt
- TEMPLATE_PATH = File.expand_path("templates/system_prompt.erb", __dir__)
+ TEMPLATE = <<~ERB
+ Terminal coding agent. Be concise. Verify your work.
+
+ # Tools
+ - read(path): file contents
+ - write(path, content): create/overwrite file
+ - execute(command): shell command
+ - eval(ruby): execute Ruby code; use to create tools for repetitive tasks
+ - task(prompt): delegate complex searches or multi-file analysis to a focused subagent
+
+ # Editing
+ Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
+ Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
+ Use write for new files or full rewrites
+
+ # Search
+ Use execute(`rg`) for text search: `rg -n "pattern" .`
+ Use execute(`fd`) for file discovery: `fd -e rb .`
+ Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
+
+ # Task Management
+ For complex tasks:
+ 1. State plan before acting
+ 2. Work through steps one at a time
+ 3. Summarize what was done
+
+ # Long Tasks
+ For complex multi-step work, write notes to .elelem/scratch.md
+
+ # Policy
+ - Explain before non-trivial commands
+ - Verify changes (read file, run tests)
+ - No interactive flags (-i, -p)
+ - Use `man` when you need to understand how to execute a program
+
+ # Environment
+ pwd: <%= pwd %>
+ platform: <%= platform %>
+ date: <%= date %>
+ self: <%= elelem_source %>
+ <%= git_info %>
+
+ <% if repo_map && !repo_map.empty? %>
+ # Codebase
+ ```
+ <%= repo_map %>```
+ <% end %>
+ <%= agents_md %>
+ ERB
def render
- ERB.new(template, trim_mode: "-").result(binding)
+ ERB.new(TEMPLATE, trim_mode: "-").result(binding)
end
private
- def template
- File.read(TEMPLATE_PATH)
- end
-
- def pwd
- Dir.pwd
- end
+ def pwd = Dir.pwd
+ def platform = RUBY_PLATFORM.split("-").last
+ def date = Date.today
def elelem_source
- File.expand_path("../..", __dir__)
- end
-
- def platform
- RUBY_PLATFORM.split("-").last
- end
-
- def date
- Date.today
+ spec = Gem.loaded_specs["elelem"]
+ spec ? spec.gem_dir : File.expand_path("../..", __dir__)
end
- def git_branch
+ def git_info
return unless File.exist?(".git")
-
"branch: #{`git branch --show-current`.strip}"
rescue Errno::ENOENT
nil
end
def repo_map
- symbols = extract_with_sg
- return ctags_fallback if symbols.nil?
+ files = `git ls-files '*.rb' 2>/dev/null`.lines.map(&:strip)
+ return "" if files.empty?
+ symbols = extract_symbols(files)
format_symbols(symbols, budget: 2000)
end
- def extract_with_sg
+ def extract_symbols(files)
output = `sg run -p 'def $NAME' -l ruby --json=compact . 2>/dev/null`
- return nil unless $?.success?
-
- JSON.parse(output).map do |match|
- {
- file: match["file"],
- name: match.dig("metaVariables", "single", "NAME", "text")
- }
- end.reject { |m| m[:file].include?("spec/") || m[:file].include?("vendor/") }
- rescue Errno::ENOENT, JSON::ParserError
- nil
+ return ctags_fallback(files) unless $?.success?
+
+ parse_sg_output(output, files)
end
- def format_symbols(symbols, budget:)
- result = String.new
- symbols.group_by { |s| s[:file] }.each do |file, syms|
- line = "#{file}: #{syms.map { |s| s[:name] }.uniq.join(", ")}\n"
- break if result.length + line.length > budget
- result << line
+ def parse_sg_output(output, tracked_files)
+ JSON.parse(output).filter_map do |match|
+ file = match["file"]
+ next unless tracked_files.include?(file)
+ { file: file, name: match.dig("metaVariables", "single", "NAME", "text") }
end
- result
+ rescue JSON::ParserError
+ []
end
- def ctags_fallback
- symbols = `ctags -x --sort=no --languages=Ruby --kinds-Ruby=cfS --exclude=spec --exclude=vendor -R . 2>/dev/null`.lines
- .map { |l| parts = l.split(/\s+/, 4); {file: parts[3]&.split&.first, name: parts[0]} }
+ def ctags_fallback(files)
+ output = `ctags -x --languages=Ruby --kinds-Ruby=cfm -L - 2>/dev/null <<< "#{files.join("\n")}"`
+ return [] unless $?.success?
- format_symbols(symbols, budget: 2000)
+ output.lines.map do |line|
+ parts = line.split(/\s+/, 4)
+ { file: parts[3]&.split&.first, name: parts[0] }
+ end
rescue Errno::ENOENT
- ""
+ []
+ end
+
+ def format_symbols(symbols, budget:)
+ tree = build_tree(symbols)
+ render_tree(tree, budget: budget)
+ end
+
+ def build_tree(symbols)
+ tree = {}
+ symbols.group_by { |s| s[:file] }.each do |file, syms|
+ parts = file.split("/")
+ node = tree
+ parts[0..-2].each { |dir| node = (node[dir + "/"] ||= {}) }
+ node[parts.last] = syms.map { |s| s[:name] }.uniq
+ end
+ tree
+ end
+
+ def render_tree(node, indent: 0, budget:, result: String.new)
+ node.each do |key, value|
+ if value.is_a?(Hash)
+ line = " " * indent + key + "\n"
+ return result if result.length + line.length > budget
+ result << line
+ render_tree(value, indent: indent + 1, budget: budget, result: result)
+ else
+ line = " " * indent + key.sub(/\.rb$/, "") + ": " + value.join(" ") + "\n"
+ return result if result.length + line.length > budget
+ result << line
+ end
+ end
+ result
end
def agents_md