elelem 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +93 -16
- data/Rakefile +0 -11
- data/exe/elelem +2 -79
- data/lib/elelem/agent.rb +33 -124
- data/lib/elelem/commands.rb +33 -0
- data/lib/elelem/conversation.rb +25 -0
- data/lib/elelem/mcp/oauth.rb +217 -0
- data/lib/elelem/mcp/token_storage.rb +60 -0
- data/lib/elelem/mcp.rb +164 -17
- data/lib/elelem/net/claude.rb +6 -4
- data/lib/elelem/net/ollama.rb +5 -2
- data/lib/elelem/net/openai.rb +6 -4
- data/lib/elelem/net.rb +0 -3
- data/lib/elelem/permissions.rb +45 -0
- data/lib/elelem/plugins/builtins.rb +96 -0
- data/lib/elelem/plugins/edit.rb +3 -3
- data/lib/elelem/plugins/eval.rb +4 -4
- data/lib/elelem/plugins/execute.rb +5 -5
- data/lib/elelem/plugins/git.rb +20 -0
- data/lib/elelem/plugins/glob.rb +13 -0
- data/lib/elelem/plugins/grep.rb +21 -0
- data/lib/elelem/plugins/list.rb +14 -0
- data/lib/elelem/plugins/mcp.rb +14 -8
- data/lib/elelem/plugins/permissions.json +6 -0
- data/lib/elelem/plugins/read.rb +6 -6
- data/lib/elelem/plugins/task.rb +14 -0
- data/lib/elelem/plugins/tools.rb +13 -0
- data/lib/elelem/plugins/verify.rb +4 -4
- data/lib/elelem/plugins/write.rb +17 -6
- data/lib/elelem/plugins/zz_confirm.rb +9 -0
- data/lib/elelem/plugins.rb +6 -6
- data/lib/elelem/system_prompt.rb +123 -29
- data/lib/elelem/terminal.rb +7 -1
- data/lib/elelem/tool.rb +6 -15
- data/lib/elelem/toolbox.rb +13 -4
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +96 -5
- metadata +99 -3
- data/lib/elelem/plugins/confirm.rb +0 -12
- data/lib/elelem/templates/system_prompt.erb +0 -53
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:builtins) do |agent|
|
|
4
|
+
agent.commands.register("exit", description: "Exit elelem") { exit(0) }
|
|
5
|
+
|
|
6
|
+
agent.commands.register("clear", description: "Clear conversation history") do
|
|
7
|
+
agent.conversation.clear!
|
|
8
|
+
agent.terminal.say " → context cleared"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
agent.commands.register("context", description: "Show conversation context") do |args|
|
|
12
|
+
messages = agent.context
|
|
13
|
+
|
|
14
|
+
case args
|
|
15
|
+
when nil, ""
|
|
16
|
+
messages.each_with_index do |msg, i|
|
|
17
|
+
role = msg[:role]
|
|
18
|
+
preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
|
|
19
|
+
preview += "..." if msg[:content].to_s.length > 60
|
|
20
|
+
agent.terminal.say " #{i + 1}. #{role}: #{preview}"
|
|
21
|
+
end
|
|
22
|
+
when "json"
|
|
23
|
+
agent.terminal.say JSON.pretty_generate(messages)
|
|
24
|
+
when /^\d+$/
|
|
25
|
+
index = args.to_i - 1
|
|
26
|
+
if index >= 0 && index < messages.length
|
|
27
|
+
content = messages[index][:content].to_s
|
|
28
|
+
agent.terminal.say(agent.terminal.markdown(content))
|
|
29
|
+
else
|
|
30
|
+
agent.terminal.say " Invalid index: #{args}"
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
agent.terminal.say " Usage: /context [json|<number>]"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
strip_ansi = ->(text) do
|
|
38
|
+
text
|
|
39
|
+
.gsub(/^Script started.*?\n/, "")
|
|
40
|
+
.gsub(/\nScript done.*$/, "")
|
|
41
|
+
.gsub(/\e\].*?(?:\a|\e\\)/, "")
|
|
42
|
+
.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
43
|
+
.gsub(/\e[PX^_].*?\e\\/, "")
|
|
44
|
+
.gsub(/\e./, "")
|
|
45
|
+
.gsub(/[\b]/, "")
|
|
46
|
+
.gsub(/\r/, "")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
agent.commands.register("shell", description: "Start interactive shell") do
|
|
50
|
+
transcript = Tempfile.create do |file|
|
|
51
|
+
system("script", "-q", file.path, chdir: Dir.pwd)
|
|
52
|
+
strip_ansi.call(File.read(file.path))
|
|
53
|
+
end
|
|
54
|
+
agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
agent.commands.register("init", description: "Generate AGENTS.md") do
|
|
58
|
+
system_prompt = <<~PROMPT
|
|
59
|
+
AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
|
|
60
|
+
|
|
61
|
+
# AGENTS.md Spec (https://agents.md/)
|
|
62
|
+
A file providing context and instructions for AI coding agents.
|
|
63
|
+
|
|
64
|
+
## Recommended Sections
|
|
65
|
+
- Commands: build, test, lint commands
|
|
66
|
+
- Code Style: conventions, patterns
|
|
67
|
+
- Architecture: key components and flow
|
|
68
|
+
- Testing: how to run tests
|
|
69
|
+
|
|
70
|
+
## Process
|
|
71
|
+
1. Read README.md if present
|
|
72
|
+
2. Identify language (Gemfile, package.json, go.mod)
|
|
73
|
+
3. Find test scripts (bin/test, npm test)
|
|
74
|
+
4. Check linter configs
|
|
75
|
+
5. Write concise AGENTS.md
|
|
76
|
+
|
|
77
|
+
Keep it minimal. No fluff.
|
|
78
|
+
PROMPT
|
|
79
|
+
|
|
80
|
+
agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
agent.commands.register("reload", description: "Reload plugins and source") do
|
|
84
|
+
lib_dir = File.expand_path("../..", __dir__)
|
|
85
|
+
original_verbose, $VERBOSE = $VERBOSE, nil
|
|
86
|
+
Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
|
|
87
|
+
$VERBOSE = original_verbose
|
|
88
|
+
agent.toolbox = Elelem::Toolbox.new
|
|
89
|
+
agent.commands = Elelem::Commands.new
|
|
90
|
+
Elelem::Plugins.reload!(agent)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
agent.commands.register("help", description: "Show available commands") do
|
|
94
|
+
agent.terminal.say agent.commands.names.join(" ")
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/elelem/plugins/edit.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:edit) do |
|
|
4
|
-
toolbox.add("edit",
|
|
3
|
+
Elelem::Plugins.register(:edit) do |agent|
|
|
4
|
+
agent.toolbox.add("edit",
|
|
5
5
|
description: "Replace first occurrence of text in file",
|
|
6
6
|
params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
|
|
7
7
|
required: ["path", "old", "new"]
|
|
8
8
|
) do |a|
|
|
9
9
|
path = Pathname.new(a["path"]).expand_path
|
|
10
10
|
content = path.read
|
|
11
|
-
toolbox
|
|
11
|
+
agent.toolbox
|
|
12
12
|
.run("write", { "path" => a["path"], "content" => content.sub(a["old"], a["new"]) })
|
|
13
13
|
.merge(replaced: a["old"], with: a["new"])
|
|
14
14
|
end
|
data/lib/elelem/plugins/eval.rb
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:eval) do |
|
|
3
|
+
Elelem::Plugins.register(:eval) do |agent|
|
|
4
4
|
description = <<~'DESC'
|
|
5
5
|
Evaluate Ruby code. Available API:
|
|
6
6
|
|
|
7
7
|
name = "search"
|
|
8
|
-
toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
|
|
9
|
-
toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
|
|
8
|
+
agent.toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
|
|
9
|
+
agent.toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
|
|
10
10
|
end
|
|
11
11
|
DESC
|
|
12
12
|
|
|
13
|
-
toolbox.add("eval",
|
|
13
|
+
agent.toolbox.add("eval",
|
|
14
14
|
description: description,
|
|
15
15
|
params: { ruby: { type: "string" } },
|
|
16
16
|
required: ["ruby"]
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:execute) do |
|
|
4
|
-
toolbox.add("execute",
|
|
3
|
+
Elelem::Plugins.register(:execute) do |agent|
|
|
4
|
+
agent.toolbox.add("execute",
|
|
5
5
|
description: "Run shell command (supports pipes and redirections)",
|
|
6
6
|
params: { command: { type: "string" } },
|
|
7
7
|
required: ["command"],
|
|
8
8
|
aliases: ["bash", "sh", "exec", "execute<|channel|>"]
|
|
9
9
|
) do |a|
|
|
10
|
-
Elelem.sh("bash", args: ["-c", a["command"]]) { |x|
|
|
10
|
+
Elelem.sh("bash", args: ["-c", a["command"]]) { |x| agent.terminal.print(x) }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
toolbox.after("execute") do |args, result|
|
|
13
|
+
agent.toolbox.after("execute") do |args, result|
|
|
14
14
|
next if result[:exit_status] == 0
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
agent.terminal.say agent.toolbox.header("execute", args, state: "x")
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:git) do |agent|
|
|
4
|
+
allowed = %w[status diff log show branch checkout add reset stash].freeze
|
|
5
|
+
|
|
6
|
+
agent.toolbox.add("git",
|
|
7
|
+
description: "Run git command",
|
|
8
|
+
params: { command: { type: "string" }, args: { type: "array" } },
|
|
9
|
+
required: ["command"]
|
|
10
|
+
) do |a|
|
|
11
|
+
cmd = a["command"]
|
|
12
|
+
next { error: "not allowed: #{cmd}" } unless allowed.include?(cmd)
|
|
13
|
+
|
|
14
|
+
agent.toolbox.exec("git", cmd, *(a["args"] || []))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
agent.toolbox.after("git") do |_, result|
|
|
18
|
+
agent.terminal.say " ! #{result[:error]}" if result[:error]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:glob) do |agent|
|
|
4
|
+
agent.toolbox.add("glob",
|
|
5
|
+
description: "Find files matching pattern",
|
|
6
|
+
params: { pattern: { type: "string" }, path: { type: "string" } },
|
|
7
|
+
required: ["pattern"]
|
|
8
|
+
) do |a|
|
|
9
|
+
path = a["path"] || "."
|
|
10
|
+
result = agent.toolbox.exec("fd", "--glob", a["pattern"], path)
|
|
11
|
+
result[:ok] ? result : agent.toolbox.exec("find", path, "-name", a["pattern"])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:grep) do |agent|
|
|
4
|
+
agent.toolbox.add("grep",
|
|
5
|
+
description: "Search file contents",
|
|
6
|
+
params: { pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" } },
|
|
7
|
+
required: ["pattern"]
|
|
8
|
+
) do |a|
|
|
9
|
+
path = a["path"] || "."
|
|
10
|
+
glob = a["glob"]
|
|
11
|
+
rg_args = ["rg", "-n", a["pattern"], path]
|
|
12
|
+
rg_args += ["-g", glob] if glob
|
|
13
|
+
result = agent.toolbox.exec(*rg_args)
|
|
14
|
+
next result if result[:ok]
|
|
15
|
+
|
|
16
|
+
grep_args = ["grep", "-rn"]
|
|
17
|
+
grep_args += ["--include", glob] if glob
|
|
18
|
+
grep_args += [a["pattern"], path]
|
|
19
|
+
agent.toolbox.exec(*grep_args)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:list) do |agent|
|
|
4
|
+
agent.toolbox.add("list",
|
|
5
|
+
description: "List directory contents",
|
|
6
|
+
params: { path: { type: "string" }, recursive: { type: "boolean" } },
|
|
7
|
+
required: [],
|
|
8
|
+
aliases: ["ls"]
|
|
9
|
+
) do |a|
|
|
10
|
+
path = a["path"] || "."
|
|
11
|
+
flags = a["recursive"] ? "-laR" : "-la"
|
|
12
|
+
agent.toolbox.exec("ls", flags, path)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/elelem/plugins/mcp.rb
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:mcp) do |
|
|
3
|
+
Elelem::Plugins.register(:mcp) do |agent|
|
|
4
4
|
mcp = Elelem::MCP.new
|
|
5
5
|
at_exit { mcp.close }
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
|
|
7
|
+
Thread.new do
|
|
8
|
+
mcp.tools.each do |name, tool|
|
|
9
|
+
agent.toolbox.add(
|
|
10
|
+
name,
|
|
11
|
+
description: tool[:description],
|
|
12
|
+
params: tool[:params],
|
|
13
|
+
required: tool[:required],
|
|
14
|
+
&tool[:fn]
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
rescue => e
|
|
18
|
+
warn "MCP failed: #{e.message}"
|
|
13
19
|
end
|
|
14
20
|
end
|
data/lib/elelem/plugins/read.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:read) do |
|
|
4
|
-
toolbox.add("read",
|
|
3
|
+
Elelem::Plugins.register(:read) do |agent|
|
|
4
|
+
agent.toolbox.add("read",
|
|
5
5
|
description: "Read file",
|
|
6
6
|
params: { path: { type: "string" } },
|
|
7
7
|
required: ["path"],
|
|
@@ -11,11 +11,11 @@ Elelem::Plugins.register(:read) do |toolbox|
|
|
|
11
11
|
path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
toolbox.after("read") do |_, result|
|
|
14
|
+
agent.toolbox.after("read") do |_, result|
|
|
15
15
|
if result[:error]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
agent.terminal.say " ! #{result[:error]}"
|
|
17
|
+
else
|
|
18
|
+
agent.terminal.display_file(result[:path], fallback: result[:content])
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:task) do |agent|
|
|
4
|
+
agent.toolbox.add("task",
|
|
5
|
+
description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
|
|
6
|
+
params: { prompt: { type: "string" } },
|
|
7
|
+
required: ["prompt"]
|
|
8
|
+
) do |a|
|
|
9
|
+
sub = Elelem::Agent.new(agent.client, toolbox: agent.toolbox, terminal: agent.terminal,
|
|
10
|
+
system_prompt: "Research agent. Search, analyze, report. Be concise.")
|
|
11
|
+
sub.turn(a["prompt"])
|
|
12
|
+
{ result: sub.conversation.last[:content] }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Elelem::Plugins.register(:tools) do |agent|
|
|
4
|
+
agent.commands.register("tools", description: "List available tools") do
|
|
5
|
+
agent.toolbox.tools.each_value do |tool|
|
|
6
|
+
agent.terminal.say ""
|
|
7
|
+
agent.terminal.say " #{tool.name}"
|
|
8
|
+
agent.terminal.say " #{tool.description}"
|
|
9
|
+
tool.params.each { |k, v| agent.terminal.say " #{k}: #{v[:type] || v["type"]}" }
|
|
10
|
+
agent.terminal.say " aliases: #{tool.aliases.join(", ")}" if tool.aliases.any?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -27,16 +27,16 @@ module Elelem
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
Plugins.register(:verify) do |
|
|
31
|
-
toolbox.add("verify",
|
|
30
|
+
Plugins.register(:verify) do |agent|
|
|
31
|
+
agent.toolbox.add("verify",
|
|
32
32
|
description: "Verify file syntax and run tests",
|
|
33
33
|
params: { path: { type: "string" } },
|
|
34
34
|
required: ["path"]
|
|
35
35
|
) do |a|
|
|
36
36
|
path = a["path"]
|
|
37
37
|
Verifiers.for(path).inject({verified: []}) do |memo, cmd|
|
|
38
|
-
|
|
39
|
-
v = toolbox.run("execute", { "command" => cmd })
|
|
38
|
+
agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
|
|
39
|
+
v = agent.toolbox.run("execute", { "command" => cmd })
|
|
40
40
|
break v.merge(path: path, command: cmd) if v[:exit_status] != 0
|
|
41
41
|
|
|
42
42
|
memo[:verified] << cmd
|
data/lib/elelem/plugins/write.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Elelem::Plugins.register(:write) do |
|
|
4
|
-
toolbox.add("write",
|
|
3
|
+
Elelem::Plugins.register(:write) do |agent|
|
|
4
|
+
agent.toolbox.add("write",
|
|
5
5
|
description: "Write file",
|
|
6
6
|
params: { path: { type: "string" }, content: { type: "string" } },
|
|
7
7
|
required: ["path", "content"],
|
|
@@ -12,12 +12,23 @@ Elelem::Plugins.register(:write) do |toolbox|
|
|
|
12
12
|
{ bytes: path.write(a["content"]), path: a["path"] }
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
toolbox.
|
|
15
|
+
agent.toolbox.before("write") do |args|
|
|
16
|
+
path = Pathname.new(args["path"]).expand_path
|
|
17
|
+
next unless path.exist? && $stdin.tty?
|
|
18
|
+
|
|
19
|
+
Tempfile.create(["elelem", File.extname(path)]) do |t|
|
|
20
|
+
t.write(args["content"])
|
|
21
|
+
t.flush
|
|
22
|
+
system("diff", "--color=always", "-u", path.to_s, t.path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
agent.toolbox.after("write") do |_, result|
|
|
16
27
|
if result[:error]
|
|
17
|
-
|
|
28
|
+
agent.terminal.say " ! #{result[:error]}"
|
|
18
29
|
else
|
|
19
|
-
|
|
20
|
-
toolbox.run("verify", { "path" => result[:path] })
|
|
30
|
+
agent.terminal.display_file(result[:path], fallback: " -> #{result[:path]}")
|
|
31
|
+
agent.toolbox.run("verify", { "path" => result[:path] })
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
end
|
data/lib/elelem/plugins.rb
CHANGED
|
@@ -8,15 +8,15 @@ module Elelem
|
|
|
8
8
|
".elelem/plugins"
|
|
9
9
|
].freeze
|
|
10
10
|
|
|
11
|
-
def self.setup!(
|
|
11
|
+
def self.setup!(agent)
|
|
12
12
|
load_plugins
|
|
13
|
-
registry.each_value { |plugin| plugin.call(
|
|
13
|
+
registry.each_value { |plugin| plugin.call(agent) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def self.reload!(
|
|
17
|
-
|
|
16
|
+
def self.reload!(agent)
|
|
17
|
+
registry.clear
|
|
18
18
|
load_plugins
|
|
19
|
-
registry.each_value { |plugin| plugin.call(
|
|
19
|
+
registry.each_value { |plugin| plugin.call(agent) }
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.load_plugins
|
|
@@ -33,7 +33,7 @@ module Elelem
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def self.register(name, &block)
|
|
36
|
-
|
|
36
|
+
registry[name] = block
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def self.registry
|
data/lib/elelem/system_prompt.rb
CHANGED
|
@@ -2,56 +2,150 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class SystemPrompt
|
|
5
|
-
|
|
5
|
+
TEMPLATE = <<~ERB
|
|
6
|
+
Terminal coding agent. Be concise. Verify your work.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
# Tools
|
|
9
|
+
- read(path): file contents
|
|
10
|
+
- write(path, content): create/overwrite file
|
|
11
|
+
- execute(command): shell command
|
|
12
|
+
- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
|
|
13
|
+
- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
# Editing
|
|
16
|
+
Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
|
|
17
|
+
Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
|
|
18
|
+
Use write for new files or full rewrites
|
|
19
|
+
|
|
20
|
+
# Search
|
|
21
|
+
Use execute(`rg`) for text search: `rg -n "pattern" .`
|
|
22
|
+
Use execute(`fd`) for file discovery: `fd -e rb .`
|
|
23
|
+
Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
|
|
24
|
+
|
|
25
|
+
# Task Management
|
|
26
|
+
For complex tasks:
|
|
27
|
+
1. State plan before acting
|
|
28
|
+
2. Work through steps one at a time
|
|
29
|
+
3. Summarize what was done
|
|
30
|
+
|
|
31
|
+
# Long Tasks
|
|
32
|
+
For complex multi-step work, write notes to .elelem/scratch.md
|
|
33
|
+
|
|
34
|
+
# Policy
|
|
35
|
+
- Explain before non-trivial commands
|
|
36
|
+
- Verify changes (read file, run tests)
|
|
37
|
+
- No interactive flags (-i, -p)
|
|
38
|
+
- Use `man` when you need to understand how to execute a program
|
|
39
|
+
|
|
40
|
+
# Environment
|
|
41
|
+
pwd: <%= pwd %>
|
|
42
|
+
platform: <%= platform %>
|
|
43
|
+
date: <%= date %>
|
|
44
|
+
self: <%= elelem_source %>
|
|
45
|
+
<%= git_info %>
|
|
46
|
+
|
|
47
|
+
<% if repo_map && !repo_map.empty? %>
|
|
48
|
+
# Codebase
|
|
49
|
+
```
|
|
50
|
+
<%= repo_map %>```
|
|
51
|
+
<% end %>
|
|
52
|
+
<%= agents_md %>
|
|
53
|
+
ERB
|
|
12
54
|
|
|
13
55
|
def render
|
|
14
|
-
ERB.new(
|
|
56
|
+
ERB.new(TEMPLATE, trim_mode: "-").result(binding)
|
|
15
57
|
end
|
|
16
58
|
|
|
17
59
|
private
|
|
18
60
|
|
|
19
|
-
def
|
|
20
|
-
|
|
61
|
+
def pwd = Dir.pwd
|
|
62
|
+
def platform = RUBY_PLATFORM.split("-").last
|
|
63
|
+
def date = Date.today
|
|
64
|
+
|
|
65
|
+
def elelem_source
|
|
66
|
+
spec = Gem.loaded_specs["elelem"]
|
|
67
|
+
spec ? spec.gem_dir : File.expand_path("../..", __dir__)
|
|
21
68
|
end
|
|
22
69
|
|
|
23
|
-
def
|
|
24
|
-
|
|
70
|
+
def git_info
|
|
71
|
+
return unless File.exist?(".git")
|
|
72
|
+
"branch: #{`git branch --show-current`.strip}"
|
|
73
|
+
rescue Errno::ENOENT
|
|
74
|
+
nil
|
|
25
75
|
end
|
|
26
76
|
|
|
27
|
-
def
|
|
28
|
-
|
|
77
|
+
def repo_map
|
|
78
|
+
files = `git ls-files '*.rb' 2>/dev/null`.lines.map(&:strip)
|
|
79
|
+
return "" if files.empty?
|
|
80
|
+
|
|
81
|
+
symbols = extract_symbols(files)
|
|
82
|
+
format_symbols(symbols, budget: 2000)
|
|
29
83
|
end
|
|
30
84
|
|
|
31
|
-
def
|
|
32
|
-
|
|
85
|
+
def extract_symbols(files)
|
|
86
|
+
output, status = Open3.capture2("sg", "run", "-p", "def $NAME", "-l", "ruby", "--json=compact", ".", err: File::NULL)
|
|
87
|
+
return ctags_fallback(files) unless status.success?
|
|
88
|
+
|
|
89
|
+
parse_sg_output(output, files)
|
|
33
90
|
end
|
|
34
91
|
|
|
35
|
-
def
|
|
36
|
-
|
|
92
|
+
def parse_sg_output(output, tracked_files)
|
|
93
|
+
JSON.parse(output).filter_map do |match|
|
|
94
|
+
file = match["file"]
|
|
95
|
+
next unless tracked_files.include?(file)
|
|
96
|
+
{ file: file, name: match.dig("metaVariables", "single", "NAME", "text") }
|
|
97
|
+
end
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
[]
|
|
37
100
|
end
|
|
38
101
|
|
|
39
|
-
def
|
|
40
|
-
return
|
|
102
|
+
def ctags_fallback(files)
|
|
103
|
+
return [] if files.empty?
|
|
41
104
|
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
105
|
+
output = IO.popen(["ctags", "-x", "--languages=Ruby", "--kinds-Ruby=cfm", "-L", "-"], "r+") do |io|
|
|
106
|
+
io.puts(files)
|
|
107
|
+
io.close_write
|
|
108
|
+
io.read
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
output.lines.map do |line|
|
|
112
|
+
parts = line.split(/\s+/, 4)
|
|
113
|
+
{ file: parts[2], name: parts[0] }
|
|
114
|
+
end
|
|
115
|
+
rescue Errno::ENOENT
|
|
116
|
+
[]
|
|
45
117
|
end
|
|
46
118
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
119
|
+
def format_symbols(symbols, budget:)
|
|
120
|
+
tree = build_tree(symbols)
|
|
121
|
+
render_tree(tree, budget: budget)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_tree(symbols)
|
|
125
|
+
tree = {}
|
|
126
|
+
symbols.group_by { |s| s[:file] }.each do |file, syms|
|
|
127
|
+
parts = file.split("/")
|
|
128
|
+
node = tree
|
|
129
|
+
parts[0..-2].each { |dir| node = (node[dir + "/"] ||= {}) }
|
|
130
|
+
node[parts.last] = syms.map { |s| s[:name] }.uniq
|
|
131
|
+
end
|
|
132
|
+
tree
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def render_tree(node, indent: 0, budget:, result: String.new)
|
|
136
|
+
node.each do |key, value|
|
|
137
|
+
if value.is_a?(Hash)
|
|
138
|
+
line = " " * indent + key + "\n"
|
|
139
|
+
return result if result.length + line.length > budget
|
|
140
|
+
result << line
|
|
141
|
+
render_tree(value, indent: indent + 1, budget: budget, result: result)
|
|
142
|
+
else
|
|
143
|
+
line = " " * indent + key.sub(/\.rb$/, "") + ": " + value.join(" ") + "\n"
|
|
144
|
+
return result if result.length + line.length > budget
|
|
145
|
+
result << line
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
result
|
|
55
149
|
end
|
|
56
150
|
|
|
57
151
|
def agents_md
|
data/lib/elelem/terminal.rb
CHANGED
|
@@ -51,6 +51,12 @@ module Elelem
|
|
|
51
51
|
n.times { $stdout.puts("") }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def display_file(path, fallback: nil)
|
|
55
|
+
return if @quiet
|
|
56
|
+
|
|
57
|
+
system("bat", "--paging=never", path) || say(fallback || path)
|
|
58
|
+
end
|
|
59
|
+
|
|
54
60
|
def waiting
|
|
55
61
|
return if @quiet
|
|
56
62
|
|
|
@@ -66,7 +72,7 @@ module Elelem
|
|
|
66
72
|
private
|
|
67
73
|
|
|
68
74
|
def blank?(text)
|
|
69
|
-
text.nil? || text.strip.empty?
|
|
75
|
+
text.nil? || text.to_s.strip.empty?
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
def stop_dots
|
data/lib/elelem/tool.rb
CHANGED
|
@@ -7,11 +7,12 @@ module Elelem
|
|
|
7
7
|
def initialize(name, description:, params: {}, required: [], aliases: [], &fn)
|
|
8
8
|
@name = name
|
|
9
9
|
@description = description
|
|
10
|
-
@params = params
|
|
11
|
-
@required = required
|
|
12
|
-
@aliases = aliases
|
|
10
|
+
@params = params.freeze
|
|
11
|
+
@required = required.freeze
|
|
12
|
+
@aliases = aliases.freeze
|
|
13
13
|
@fn = fn
|
|
14
|
-
@
|
|
14
|
+
@schema_hash = { type: "object", properties: @params, required: @required }.freeze
|
|
15
|
+
@schema = JSONSchemer.schema(@schema_hash)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def call(args)
|
|
@@ -30,19 +31,9 @@ module Elelem
|
|
|
30
31
|
function: {
|
|
31
32
|
name: name,
|
|
32
33
|
description: description,
|
|
33
|
-
parameters: schema_hash
|
|
34
|
+
parameters: @schema_hash
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def schema_hash
|
|
41
|
-
{
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: params,
|
|
44
|
-
required: required
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
38
|
end
|
|
48
39
|
end
|