elelem 0.8.0 → 0.9.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 +71 -3
- data/README.md +31 -46
- data/exe/elelem +78 -3
- data/lib/elelem/agent.rb +135 -228
- data/lib/elelem/mcp.rb +96 -0
- data/lib/elelem/net/claude.rb +200 -0
- data/lib/elelem/net/ollama.rb +78 -0
- data/lib/elelem/net/openai.rb +86 -0
- data/lib/elelem/net.rb +16 -0
- data/lib/elelem/plugins/confirm.rb +12 -0
- data/lib/elelem/plugins/edit.rb +15 -0
- data/lib/elelem/plugins/eval.rb +20 -0
- data/lib/elelem/plugins/execute.rb +18 -0
- data/lib/elelem/plugins/mcp.rb +14 -0
- data/lib/elelem/plugins/read.rb +21 -0
- data/lib/elelem/plugins/verify.rb +47 -0
- data/lib/elelem/plugins/write.rb +23 -0
- data/lib/elelem/plugins.rb +43 -0
- data/lib/elelem/system_prompt.rb +65 -0
- data/lib/elelem/templates/system_prompt.erb +53 -0
- data/lib/elelem/terminal.rb +55 -65
- data/lib/elelem/tool.rb +30 -32
- data/lib/elelem/toolbox.rb +36 -94
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +28 -36
- metadata +39 -61
- data/lib/elelem/application.rb +0 -45
- data/lib/elelem/conversation.rb +0 -78
- data/lib/elelem/git_context.rb +0 -79
- data/lib/elelem/system_prompt.erb +0 -16
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Terminal coding agent. Be concise. Verify your work.
|
|
2
|
+
|
|
3
|
+
# Tools
|
|
4
|
+
- read(path): file contents
|
|
5
|
+
- write(path, content): create/overwrite file
|
|
6
|
+
- execute(command): shell command
|
|
7
|
+
- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
|
|
8
|
+
- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
|
|
9
|
+
|
|
10
|
+
# Editing
|
|
11
|
+
Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
|
|
12
|
+
Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
|
|
13
|
+
Use write for new files or full rewrites
|
|
14
|
+
|
|
15
|
+
# Search
|
|
16
|
+
Use execute(`rg`) for text search: `rg -n "pattern" .`
|
|
17
|
+
Use execute(`fd`) for file discovery: `fd -e rb .`
|
|
18
|
+
Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
|
|
19
|
+
|
|
20
|
+
# Task Management
|
|
21
|
+
For complex tasks:
|
|
22
|
+
1. State plan before acting
|
|
23
|
+
2. Work through steps one at a time
|
|
24
|
+
3. Summarize what was done
|
|
25
|
+
|
|
26
|
+
# Long Tasks
|
|
27
|
+
For complex multi-step work, write notes to .elelem/scratch.md
|
|
28
|
+
|
|
29
|
+
# Policy
|
|
30
|
+
- Explain before non-trivial commands
|
|
31
|
+
- Verify changes (read file, run tests)
|
|
32
|
+
- No interactive flags (-i, -p)
|
|
33
|
+
- Use `man` when you need to understand how to execute a program
|
|
34
|
+
|
|
35
|
+
# Environment
|
|
36
|
+
pwd: <%= pwd %>
|
|
37
|
+
platform: <%= platform %>
|
|
38
|
+
date: <%= date %>
|
|
39
|
+
self (this agent's source): <%= elelem_source %>
|
|
40
|
+
<%= git_branch %>
|
|
41
|
+
|
|
42
|
+
# Codebase
|
|
43
|
+
<%= repo_map %>
|
|
44
|
+
<% if agents_md %>
|
|
45
|
+
|
|
46
|
+
# Project Instructions
|
|
47
|
+
<%= agents_md %>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% if memory %>
|
|
50
|
+
|
|
51
|
+
# Earlier Context
|
|
52
|
+
<%= memory %>
|
|
53
|
+
<% end %>
|
data/lib/elelem/terminal.rb
CHANGED
|
@@ -2,58 +2,79 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Terminal
|
|
5
|
-
def initialize(commands: [],
|
|
5
|
+
def initialize(commands: [], quiet: false)
|
|
6
6
|
@commands = commands
|
|
7
|
-
@
|
|
8
|
-
@
|
|
9
|
-
|
|
10
|
-
@spinner_thread = nil
|
|
11
|
-
setup_completion
|
|
7
|
+
@quiet = quiet
|
|
8
|
+
@dots_thread = nil
|
|
9
|
+
setup_completion unless @quiet
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def ask(prompt)
|
|
15
13
|
Reline.readline(prompt, true)&.strip
|
|
16
14
|
end
|
|
17
15
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
def think(text)
|
|
17
|
+
return if blank?(text)
|
|
18
|
+
|
|
19
|
+
"\e[2;3m#{text}\e[0m"
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
def markdown(text)
|
|
23
|
+
return if @quiet || blank?(text)
|
|
24
|
+
|
|
25
|
+
newline(n: 2)
|
|
26
|
+
width = $stdout.winsize[1] rescue 80
|
|
27
|
+
IO.popen(["glow", "-s", "dark", "-w", width.to_s, "-"], "r+") do |io|
|
|
28
|
+
io.write(text)
|
|
29
|
+
io.close_write
|
|
30
|
+
io.read
|
|
31
|
+
end
|
|
32
|
+
rescue Errno::ENOENT
|
|
33
|
+
text
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def print(text)
|
|
37
|
+
return if @quiet || blank?(text)
|
|
38
|
+
|
|
39
|
+
stop_dots
|
|
40
|
+
$stdout.print text
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def say(text)
|
|
44
|
+
return if @quiet || blank?(text)
|
|
45
|
+
|
|
46
|
+
stop_dots
|
|
47
|
+
$stdout.puts text
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def newline(n: 1)
|
|
51
|
+
n.times { $stdout.puts("") }
|
|
26
52
|
end
|
|
27
53
|
|
|
28
54
|
def waiting
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
return if @quiet
|
|
56
|
+
|
|
57
|
+
@dots_thread = Thread.new do
|
|
32
58
|
loop do
|
|
33
|
-
$stdout.print "
|
|
59
|
+
$stdout.print "."
|
|
34
60
|
$stdout.flush
|
|
35
|
-
i += 1
|
|
36
61
|
sleep 0.1
|
|
37
62
|
end
|
|
38
63
|
end
|
|
39
64
|
end
|
|
40
65
|
|
|
41
|
-
def select(question, options, &block)
|
|
42
|
-
CLI::UI::Prompt.ask(question) do |handler|
|
|
43
|
-
options.each do |option|
|
|
44
|
-
handler.option(option) { |selected| block.call(selected) }
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
66
|
private
|
|
50
67
|
|
|
51
|
-
def
|
|
52
|
-
|
|
68
|
+
def blank?(text)
|
|
69
|
+
text.nil? || text.strip.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop_dots
|
|
73
|
+
return unless @dots_thread
|
|
53
74
|
|
|
54
|
-
@
|
|
55
|
-
@
|
|
56
|
-
|
|
75
|
+
@dots_thread.kill
|
|
76
|
+
@dots_thread = nil
|
|
77
|
+
newline
|
|
57
78
|
end
|
|
58
79
|
|
|
59
80
|
def setup_completion
|
|
@@ -63,45 +84,14 @@ module Elelem
|
|
|
63
84
|
|
|
64
85
|
def complete(target, preposing)
|
|
65
86
|
line = "#{preposing}#{target}"
|
|
87
|
+
return @commands.select { |c| c.start_with?(line) } if line.start_with?("/") && !preposing.include?(" ")
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
return @commands.select { |c| c.start_with?(line) }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
case preposing.strip
|
|
72
|
-
when '/mode'
|
|
73
|
-
@modes.select { |m| m.start_with?(target) }
|
|
74
|
-
when '/provider'
|
|
75
|
-
@providers.select { |p| p.start_with?(target) }
|
|
76
|
-
when '/env'
|
|
77
|
-
@env_vars.select { |v| v.start_with?(target) }
|
|
78
|
-
when %r{^/env\s+\w+\s+pass(\s+show)?\s*$}
|
|
79
|
-
subcommands = %w[show ls insert generate edit rm]
|
|
80
|
-
matches = subcommands.select { |c| c.start_with?(target) }
|
|
81
|
-
matches.any? ? matches : complete_pass_entries(target)
|
|
82
|
-
when %r{^/env\s+\w+$}
|
|
83
|
-
complete_commands(target)
|
|
84
|
-
else
|
|
85
|
-
complete_files(target)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def complete_commands(target)
|
|
90
|
-
result = Elelem.shell.execute("bash", args: ["-c", "compgen -c #{target}"])
|
|
91
|
-
result["stdout"].lines.map(&:strip).first(20)
|
|
89
|
+
complete_files(target)
|
|
92
90
|
end
|
|
93
91
|
|
|
94
92
|
def complete_files(target)
|
|
95
|
-
result = Elelem.
|
|
96
|
-
result[
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def complete_pass_entries(target)
|
|
100
|
-
store = ENV.fetch("PASSWORD_STORE_DIR", File.expand_path("~/.password-store"))
|
|
101
|
-
result = Elelem.shell.execute("find", args: ["-L", store, "-name", "*.gpg"])
|
|
102
|
-
result["stdout"].lines.map { |l|
|
|
103
|
-
l.strip.sub("#{store}/", "").sub(/\.gpg$/, "")
|
|
104
|
-
}.select { |e| e.start_with?(target) }.first(20)
|
|
93
|
+
result = Elelem.sh("bash", args: ["-c", "compgen -f #{target}"])
|
|
94
|
+
result[:content].lines.map(&:strip).first(20)
|
|
105
95
|
end
|
|
106
96
|
end
|
|
107
97
|
end
|
data/lib/elelem/tool.rb
CHANGED
|
@@ -2,49 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Tool
|
|
5
|
-
attr_reader :name
|
|
5
|
+
attr_reader :name, :description, :params, :required, :aliases
|
|
6
6
|
|
|
7
|
-
def initialize(
|
|
8
|
-
@name =
|
|
9
|
-
@
|
|
10
|
-
@
|
|
7
|
+
def initialize(name, description:, params: {}, required: [], aliases: [], &fn)
|
|
8
|
+
@name = name
|
|
9
|
+
@description = description
|
|
10
|
+
@params = params
|
|
11
|
+
@required = required
|
|
12
|
+
@aliases = aliases
|
|
13
|
+
@fn = fn
|
|
14
|
+
@schema = JSONSchemer.schema(schema_hash)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def call(args)
|
|
14
|
-
|
|
15
|
-
actual = args.keys
|
|
16
|
-
expected = @schema.dig(:function, :parameters)
|
|
17
|
-
return { error: "Invalid args for #{@name}.", actual: actual, expected: expected }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
@block.call(args)
|
|
18
|
+
@fn.call(args)
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
def
|
|
24
|
-
|
|
21
|
+
def validate(args)
|
|
22
|
+
@schema.validate(args || {}).map do |error|
|
|
23
|
+
error["error"]
|
|
24
|
+
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def to_h
|
|
28
|
-
|
|
28
|
+
{
|
|
29
|
+
type: "function",
|
|
30
|
+
function: {
|
|
31
|
+
name: name,
|
|
32
|
+
description: description,
|
|
33
|
+
parameters: schema_hash
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
end
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: properties,
|
|
41
|
-
required: required
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}) do |args|
|
|
45
|
-
yield args
|
|
46
|
-
end
|
|
47
|
-
end
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def schema_hash
|
|
41
|
+
{
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: params,
|
|
44
|
+
required: required
|
|
45
|
+
}
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
data/lib/elelem/toolbox.rb
CHANGED
|
@@ -2,122 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Toolbox
|
|
5
|
-
|
|
6
|
-
path = args["path"]
|
|
7
|
-
full_path = Pathname.new(path).expand_path
|
|
8
|
-
full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
EXEC_TOOL = Tool.build("exec", "Run shell commands. Returns stdout/stderr/exit_status.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string", description: "Working directory (defaults to current)" }, stdin: { type: "string" } }, ["cmd"]) do |args|
|
|
12
|
-
Elelem.shell.execute(
|
|
13
|
-
args["cmd"],
|
|
14
|
-
args: args["args"] || [],
|
|
15
|
-
env: args["env"] || {},
|
|
16
|
-
cwd: args["cwd"].to_s.empty? ? Dir.pwd : args["cwd"],
|
|
17
|
-
stdin: args["stdin"]
|
|
18
|
-
)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
GREP_TOOL = Tool.build("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers.", { query: { type: "string" } }, ["query"]) do |args|
|
|
22
|
-
Elelem.shell.execute("git", args: ["grep", "-nI", args["query"]])
|
|
23
|
-
end
|
|
5
|
+
attr_reader :tools, :hooks, :aliases
|
|
24
6
|
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
def initialize
|
|
8
|
+
@tools = {}
|
|
9
|
+
@aliases = {}
|
|
10
|
+
@hooks = { before: Hash.new { |h, k| h[k] = [] }, after: Hash.new { |h, k| h[k] = [] } }
|
|
27
11
|
end
|
|
28
12
|
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
def add(name, description:, params: {}, required: [], aliases: [], &fn)
|
|
14
|
+
tool = Tool.new(name, description: description, params: params, required: required, aliases: aliases, &fn)
|
|
15
|
+
@tools[name] = tool
|
|
16
|
+
tool.aliases.each { |a| @aliases[a] = name }
|
|
31
17
|
end
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
FileUtils.mkdir_p(full_path.dirname)
|
|
36
|
-
{ bytes_written: full_path.write(args["content"]) }
|
|
19
|
+
def before(tool_name, &block)
|
|
20
|
+
@hooks[:before][tool_name] << block
|
|
37
21
|
end
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
response = client.get(args["url"])
|
|
42
|
-
{ status: response.code.to_i, body: response.body }
|
|
23
|
+
def after(tool_name, &block)
|
|
24
|
+
@hooks[:after][tool_name] << block
|
|
43
25
|
end
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
client = Net::Hippie::Client.new
|
|
49
|
-
response = client.get(url)
|
|
50
|
-
JSON.parse(response.body)
|
|
27
|
+
def header(name, args, state: "+")
|
|
28
|
+
name = tool_for(name)&.name || "#{name}?"
|
|
29
|
+
"\n#{state} #{name}(#{args})"
|
|
51
30
|
end
|
|
52
31
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"ddg" => "search_engine",
|
|
57
|
-
"execute" => "exec",
|
|
58
|
-
"get" => "fetch",
|
|
59
|
-
"open" => "read",
|
|
60
|
-
"search" => "grep",
|
|
61
|
-
"sh" => "exec",
|
|
62
|
-
"web" => "fetch",
|
|
63
|
-
}
|
|
32
|
+
def run(name, args)
|
|
33
|
+
tool = tool_for(name)
|
|
34
|
+
return failure(error: "unknown tool: #{name}. Use 'execute' to run shell commands like rg, fd, git.", tools: to_a) unless tool
|
|
64
35
|
|
|
65
|
-
|
|
36
|
+
errors = tool.validate(args)
|
|
37
|
+
return failure(error: errors.join(", ")) if errors.any?
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
add_tool(EXEC_TOOL, :execute)
|
|
74
|
-
add_tool(FETCH_TOOL, :read)
|
|
75
|
-
add_tool(GREP_TOOL, :read)
|
|
76
|
-
add_tool(LIST_TOOL, :read)
|
|
77
|
-
add_tool(PATCH_TOOL, :write)
|
|
78
|
-
add_tool(READ_TOOL, :read)
|
|
79
|
-
add_tool(WRITE_TOOL, :write)
|
|
39
|
+
@hooks[:before][tool.name].each { |h| h.call(args) }
|
|
40
|
+
result = tool.call(args)
|
|
41
|
+
@hooks[:after][tool.name].each { |h| h.call(args, result) }
|
|
42
|
+
result[:error] ? failure(result) : success(result)
|
|
43
|
+
rescue => e
|
|
44
|
+
failure(error: e.message, name: name, args: args)
|
|
80
45
|
end
|
|
81
46
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
@tools_by_name[tool.name] = tool
|
|
85
|
-
@tool_permissions[tool.name] = permission
|
|
47
|
+
def to_a
|
|
48
|
+
tools.values.map(&:to_h)
|
|
86
49
|
end
|
|
87
50
|
|
|
88
|
-
|
|
89
|
-
add_tool(Tool.build(name, description, properties, required, &block), mode)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def tools_for(permissions)
|
|
93
|
-
Array(permissions).map { |permission| tools[permission].map(&:to_h) }.flatten
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def run_tool(name, args, permissions: [])
|
|
97
|
-
resolved_name = TOOL_ALIASES.fetch(name, name)
|
|
98
|
-
tool = @tools_by_name[resolved_name]
|
|
99
|
-
return { error: "Unknown tool", name: name, args: args } unless tool
|
|
100
|
-
|
|
101
|
-
tool_permission = @tool_permissions[resolved_name]
|
|
102
|
-
unless Array(permissions).include?(tool_permission)
|
|
103
|
-
return { error: "Tool '#{resolved_name}' not available in current mode", name: name }
|
|
104
|
-
end
|
|
51
|
+
private
|
|
105
52
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{ error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
|
|
53
|
+
def tool_for(name)
|
|
54
|
+
tools[@aliases.fetch(name, name)]
|
|
109
55
|
end
|
|
110
56
|
|
|
111
|
-
def
|
|
112
|
-
|
|
57
|
+
def success(payload)
|
|
58
|
+
payload.merge(ok: true)
|
|
113
59
|
end
|
|
114
60
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def eval_tool(target_binding)
|
|
118
|
-
Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required, mode: :execute) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
|
|
119
|
-
{ result: target_binding.eval(args["ruby"]) }
|
|
120
|
-
end
|
|
61
|
+
def failure(payload)
|
|
62
|
+
payload.merge(ok: false)
|
|
121
63
|
end
|
|
122
64
|
end
|
|
123
65
|
end
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
|
@@ -1,58 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "cli/ui"
|
|
3
|
+
require "date"
|
|
5
4
|
require "erb"
|
|
6
5
|
require "fileutils"
|
|
7
6
|
require "json"
|
|
8
|
-
require "
|
|
9
|
-
require "logger"
|
|
10
|
-
require "net/hippie"
|
|
11
|
-
require "net/llm"
|
|
7
|
+
require "json_schemer"
|
|
12
8
|
require "open3"
|
|
13
9
|
require "pathname"
|
|
14
10
|
require "reline"
|
|
15
|
-
require "
|
|
16
|
-
require "
|
|
17
|
-
require "timeout"
|
|
11
|
+
require "stringio"
|
|
12
|
+
require "tempfile"
|
|
18
13
|
|
|
19
14
|
require_relative "elelem/agent"
|
|
20
|
-
require_relative "elelem/
|
|
21
|
-
require_relative "elelem/
|
|
22
|
-
require_relative "elelem/
|
|
15
|
+
require_relative "elelem/mcp"
|
|
16
|
+
require_relative "elelem/net"
|
|
17
|
+
require_relative "elelem/plugins"
|
|
18
|
+
require_relative "elelem/system_prompt"
|
|
23
19
|
require_relative "elelem/terminal"
|
|
24
20
|
require_relative "elelem/tool"
|
|
25
21
|
require_relative "elelem/toolbox"
|
|
26
22
|
require_relative "elelem/version"
|
|
27
23
|
|
|
28
|
-
Reline.input = $stdin
|
|
29
|
-
Reline.output = $stdout
|
|
30
|
-
|
|
31
24
|
module Elelem
|
|
32
|
-
|
|
25
|
+
def self.sh(cmd, args: [], cwd: Dir.pwd, env: {})
|
|
26
|
+
output = StringIO.new
|
|
27
|
+
|
|
28
|
+
Open3.popen2e(env, cmd, *args, chdir: cwd) do |stdin, out, wait_thr|
|
|
29
|
+
stdin.close
|
|
30
|
+
out.each_line do |line|
|
|
31
|
+
yield line if block_given?
|
|
32
|
+
output.write(line)
|
|
33
|
+
end
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
def execute(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
|
|
36
|
-
cmd = command.is_a?(Array) ? command.first : command
|
|
37
|
-
cmd_args = command.is_a?(Array) ? command[1..] + args : args
|
|
38
|
-
stdout, stderr, status = Open3.capture3(
|
|
39
|
-
env,
|
|
40
|
-
cmd,
|
|
41
|
-
*cmd_args,
|
|
42
|
-
chdir: cwd,
|
|
43
|
-
stdin_data: stdin
|
|
44
|
-
)
|
|
45
|
-
{
|
|
46
|
-
"exit_status" => status.exitstatus,
|
|
47
|
-
"stdout" => stdout.to_s,
|
|
48
|
-
"stderr" => stderr.to_s
|
|
49
|
-
}
|
|
35
|
+
{ exit_status: wait_thr.value.exitstatus, content: output.string }
|
|
50
36
|
end
|
|
51
37
|
end
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
def self.start(client, toolbox: Toolbox.new)
|
|
40
|
+
Plugins.setup!(toolbox)
|
|
41
|
+
Agent.new(client, toolbox).repl
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.ask(client, prompt, toolbox: Toolbox.new)
|
|
45
|
+
Plugins.setup!(toolbox)
|
|
46
|
+
agent = Agent.new(client, toolbox, terminal: Terminal.new(quiet: true))
|
|
47
|
+
agent.turn(prompt)
|
|
48
|
+
agent.history.last[:content]
|
|
57
49
|
end
|
|
58
50
|
end
|