elelem 0.9.2 → 0.11.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/README.md +8 -118
- data/Rakefile +0 -11
- data/exe/elelem +8 -79
- data/lib/elelem/agent.rb +51 -142
- data/lib/elelem/cli.rb +76 -0
- data/lib/elelem/commands.rb +49 -0
- data/lib/elelem/config.rb +17 -0
- data/lib/elelem/conversation.rb +25 -0
- data/lib/elelem/input.rb +53 -0
- data/lib/elelem/null_input.rb +8 -0
- data/lib/elelem/null_output.rb +7 -0
- data/lib/elelem/null_tool.rb +18 -0
- data/lib/elelem/output.rb +57 -0
- data/lib/elelem/plugins.rb +10 -21
- data/lib/elelem/prompts/default.erb +3 -0
- data/lib/elelem/registration.rb +37 -0
- data/lib/elelem/registry.rb +69 -0
- data/lib/elelem/result.rb +26 -0
- data/lib/elelem/slash_command.rb +18 -0
- data/lib/elelem/stub_provider.rb +15 -0
- data/lib/elelem/system_prompt.rb +4 -42
- data/lib/elelem/tool.rb +6 -14
- data/lib/elelem/toolbox.rb +23 -23
- data/lib/elelem/version.rb +2 -2
- data/lib/elelem.rb +29 -27
- metadata +49 -65
- data/CHANGELOG.md +0 -280
- data/lib/elelem/mcp.rb +0 -96
- data/lib/elelem/net/claude.rb +0 -200
- data/lib/elelem/net/ollama.rb +0 -78
- data/lib/elelem/net/openai.rb +0 -86
- data/lib/elelem/net.rb +0 -16
- data/lib/elelem/plugins/confirm.rb +0 -12
- data/lib/elelem/plugins/edit.rb +0 -15
- data/lib/elelem/plugins/eval.rb +0 -20
- data/lib/elelem/plugins/execute.rb +0 -18
- data/lib/elelem/plugins/mcp.rb +0 -14
- data/lib/elelem/plugins/read.rb +0 -21
- data/lib/elelem/plugins/verify.rb +0 -47
- data/lib/elelem/plugins/write.rb +0 -23
- data/lib/elelem/templates/system_prompt.erb +0 -53
- data/lib/elelem/terminal.rb +0 -97
data/lib/elelem/net/claude.rb
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
module Net
|
|
5
|
-
class Claude
|
|
6
|
-
def self.anthropic(model:, api_key:, http: Elelem::Net.http)
|
|
7
|
-
new(
|
|
8
|
-
endpoint: "https://api.anthropic.com/v1/messages",
|
|
9
|
-
headers: { "x-api-key" => api_key, "anthropic-version" => "2023-06-01" },
|
|
10
|
-
model:,
|
|
11
|
-
http:
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.vertex(model:, project:, region: "us-east5", http: Elelem::Net.http)
|
|
16
|
-
new(
|
|
17
|
-
endpoint: "https://#{region}-aiplatform.googleapis.com/v1/projects/#{project}/locations/#{region}/publishers/anthropic/models/#{model}:rawPredict",
|
|
18
|
-
headers: -> { { "Authorization" => "Bearer #{`gcloud auth application-default print-access-token`.strip}" } },
|
|
19
|
-
model:,
|
|
20
|
-
version: "vertex-2023-10-16",
|
|
21
|
-
http:
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def initialize(endpoint:, headers:, model:, version: nil, http: Elelem::Net.http)
|
|
26
|
-
@endpoint = endpoint
|
|
27
|
-
@headers_source = headers
|
|
28
|
-
@model = model
|
|
29
|
-
@version = version
|
|
30
|
-
@http = http
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def fetch(messages, tools = [], &block)
|
|
34
|
-
system_prompt, normalized_messages = extract_system(messages)
|
|
35
|
-
tool_calls = []
|
|
36
|
-
|
|
37
|
-
stream(normalized_messages, system_prompt, tools) do |event|
|
|
38
|
-
handle_event(event, tool_calls, &block)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
finalize_tool_calls(tool_calls)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def headers
|
|
47
|
-
@headers_source.respond_to?(:call) ? @headers_source.call : @headers_source
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def handle_event(event, tool_calls, &block)
|
|
51
|
-
case event["type"]
|
|
52
|
-
when "content_block_start"
|
|
53
|
-
handle_content_block_start(event, tool_calls)
|
|
54
|
-
when "content_block_delta"
|
|
55
|
-
handle_content_block_delta(event, tool_calls, &block)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def handle_content_block_start(event, tool_calls)
|
|
60
|
-
content_block = event["content_block"]
|
|
61
|
-
return unless content_block["type"] == "tool_use"
|
|
62
|
-
|
|
63
|
-
tool_calls << {
|
|
64
|
-
id: content_block["id"],
|
|
65
|
-
name: content_block["name"],
|
|
66
|
-
args: String.new
|
|
67
|
-
}
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def handle_content_block_delta(event, tool_calls, &block)
|
|
71
|
-
delta = event["delta"]
|
|
72
|
-
|
|
73
|
-
case delta["type"]
|
|
74
|
-
when "text_delta"
|
|
75
|
-
block.call(content: delta["text"], thinking: nil)
|
|
76
|
-
when "thinking_delta"
|
|
77
|
-
block.call(content: nil, thinking: delta["thinking"])
|
|
78
|
-
when "input_json_delta"
|
|
79
|
-
tool_calls.last[:args] << delta["partial_json"].to_s if tool_calls.any?
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def finalize_tool_calls(tool_calls)
|
|
84
|
-
tool_calls.each do |tool_call|
|
|
85
|
-
args = tool_call.delete(:args)
|
|
86
|
-
tool_call[:arguments] = args.empty? ? {} : JSON.parse(args)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def stream(messages, system_prompt, tools)
|
|
91
|
-
body = build_request_body(messages, system_prompt, tools)
|
|
92
|
-
|
|
93
|
-
@http.post(@endpoint, headers:, body:) do |response|
|
|
94
|
-
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(::Net::HTTPSuccess)
|
|
95
|
-
|
|
96
|
-
read_sse_stream(response) { |event| yield event }
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def build_request_body(messages, system_prompt, tools)
|
|
101
|
-
body = { max_tokens: 64000, messages:, stream: true }
|
|
102
|
-
body[:model] = @model unless @version
|
|
103
|
-
body[:anthropic_version] = @version if @version
|
|
104
|
-
body[:system] = system_prompt if system_prompt
|
|
105
|
-
body[:tools] = unwrap_tools(tools) unless tools.empty?
|
|
106
|
-
body
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def read_sse_stream(response)
|
|
110
|
-
buffer = String.new
|
|
111
|
-
|
|
112
|
-
response.read_body do |chunk|
|
|
113
|
-
buffer << chunk
|
|
114
|
-
|
|
115
|
-
while (index = buffer.index("\n\n"))
|
|
116
|
-
raw_event = buffer.slice!(0, index + 2)
|
|
117
|
-
event = parse_sse(raw_event)
|
|
118
|
-
yield event if event
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def parse_sse(raw)
|
|
124
|
-
line = raw.lines.find { |l| l.start_with?("data: ") }
|
|
125
|
-
return nil unless line
|
|
126
|
-
|
|
127
|
-
data = line.delete_prefix("data: ").strip
|
|
128
|
-
return nil if data == "[DONE]"
|
|
129
|
-
|
|
130
|
-
JSON.parse(data)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def extract_system(messages)
|
|
134
|
-
system_messages, other_messages = messages.partition { |message| message[:role] == "system" }
|
|
135
|
-
system_content = system_messages.first&.dig(:content)
|
|
136
|
-
[system_content, normalize(other_messages)]
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def normalize(messages)
|
|
140
|
-
messages.map { |message| normalize_message(message) }
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def normalize_message(message)
|
|
144
|
-
case message[:role]
|
|
145
|
-
when "tool"
|
|
146
|
-
tool_result_message(message)
|
|
147
|
-
when "assistant"
|
|
148
|
-
message[:tool_calls]&.any? ? assistant_with_tools_message(message) : message
|
|
149
|
-
else
|
|
150
|
-
message
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def tool_result_message(message)
|
|
155
|
-
{
|
|
156
|
-
role: "user",
|
|
157
|
-
content: [{
|
|
158
|
-
type: "tool_result",
|
|
159
|
-
tool_use_id: message[:tool_call_id],
|
|
160
|
-
content: message[:content]
|
|
161
|
-
}]
|
|
162
|
-
}
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def assistant_with_tools_message(message)
|
|
166
|
-
text_content = build_text_content(message[:content])
|
|
167
|
-
tool_content = build_tool_content(message[:tool_calls])
|
|
168
|
-
|
|
169
|
-
{ role: "assistant", content: text_content + tool_content }
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def build_text_content(content)
|
|
173
|
-
return [] if content.to_s.empty?
|
|
174
|
-
|
|
175
|
-
[{ type: "text", text: content }]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def build_tool_content(tool_calls)
|
|
179
|
-
tool_calls.map do |tool_call|
|
|
180
|
-
{
|
|
181
|
-
type: "tool_use",
|
|
182
|
-
id: tool_call[:id],
|
|
183
|
-
name: tool_call[:name],
|
|
184
|
-
input: tool_call[:arguments]
|
|
185
|
-
}
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def unwrap_tools(tools)
|
|
190
|
-
tools.map do |tool|
|
|
191
|
-
{
|
|
192
|
-
name: tool.dig(:function, :name),
|
|
193
|
-
description: tool.dig(:function, :description),
|
|
194
|
-
input_schema: tool.dig(:function, :parameters)
|
|
195
|
-
}
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
data/lib/elelem/net/ollama.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
module Net
|
|
5
|
-
class Ollama
|
|
6
|
-
def initialize(model:, host: "localhost:11434", http: Elelem::Net.http)
|
|
7
|
-
@url = normalize_url(host)
|
|
8
|
-
@model = model
|
|
9
|
-
@http = http
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def fetch(messages, tools = [], &block)
|
|
13
|
-
tool_calls = []
|
|
14
|
-
body = build_request_body(messages, tools)
|
|
15
|
-
|
|
16
|
-
stream(body) do |event|
|
|
17
|
-
handle_event(event, tool_calls, &block)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
tool_calls
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def normalize_url(host)
|
|
26
|
-
base = host.start_with?("http") ? host : "http://#{host}"
|
|
27
|
-
"#{base}/api/chat"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def build_request_body(messages, tools)
|
|
31
|
-
{ model: @model, messages:, tools:, stream: true }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def handle_event(event, tool_calls, &block)
|
|
35
|
-
message = event["message"] || {}
|
|
36
|
-
|
|
37
|
-
unless event["done"]
|
|
38
|
-
block.call(content: message["content"], thinking: message["thinking"])
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
if message["tool_calls"]
|
|
42
|
-
tool_calls.concat(parse_tool_calls(message["tool_calls"]))
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def stream(body)
|
|
47
|
-
@http.post(@url, body:) do |response|
|
|
48
|
-
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(::Net::HTTPSuccess)
|
|
49
|
-
|
|
50
|
-
read_ndjson_stream(response) { |event| yield event }
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def read_ndjson_stream(response)
|
|
55
|
-
buffer = String.new
|
|
56
|
-
|
|
57
|
-
response.read_body do |chunk|
|
|
58
|
-
buffer << chunk
|
|
59
|
-
|
|
60
|
-
while (index = buffer.index("\n"))
|
|
61
|
-
line = buffer.slice!(0, index + 1)
|
|
62
|
-
yield JSON.parse(line)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def parse_tool_calls(tool_calls)
|
|
68
|
-
tool_calls.map do |tool_call|
|
|
69
|
-
{
|
|
70
|
-
id: tool_call["id"],
|
|
71
|
-
name: tool_call.dig("function", "name"),
|
|
72
|
-
arguments: tool_call.dig("function", "arguments") || {}
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
data/lib/elelem/net/openai.rb
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
module Net
|
|
5
|
-
class OpenAI
|
|
6
|
-
def initialize(model:, api_key:, base_url: "https://api.openai.com/v1", http: Elelem::Net.http)
|
|
7
|
-
@url = "#{base_url}/chat/completions"
|
|
8
|
-
@model = model
|
|
9
|
-
@api_key = api_key
|
|
10
|
-
@http = http
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def fetch(messages, tools = [], &block)
|
|
14
|
-
tool_calls = {}
|
|
15
|
-
body = build_request_body(messages, tools)
|
|
16
|
-
|
|
17
|
-
stream(body) do |event|
|
|
18
|
-
handle_event(event, tool_calls, &block)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
finalize_tool_calls(tool_calls)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def build_request_body(messages, tools)
|
|
27
|
-
{ model: @model, messages:, stream: true, tools:, tool_choice: "auto" }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def handle_event(event, tool_calls, &block)
|
|
31
|
-
delta = event.dig("choices", 0, "delta") || {}
|
|
32
|
-
|
|
33
|
-
block.call(content: delta["content"], thinking: nil) if delta["content"]
|
|
34
|
-
|
|
35
|
-
accumulate_tool_calls(delta["tool_calls"], tool_calls) if delta["tool_calls"]
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def accumulate_tool_calls(incoming_tool_calls, tool_calls)
|
|
39
|
-
incoming_tool_calls.each do |tool_call|
|
|
40
|
-
index = tool_call["index"]
|
|
41
|
-
tool_calls[index] ||= { id: nil, name: nil, args: String.new }
|
|
42
|
-
tool_calls[index][:id] ||= tool_call["id"]
|
|
43
|
-
tool_calls[index][:name] ||= tool_call.dig("function", "name")
|
|
44
|
-
tool_calls[index][:args] << tool_call.dig("function", "arguments").to_s
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def stream(body)
|
|
49
|
-
@http.post(@url, headers: headers, body:) do |response|
|
|
50
|
-
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(::Net::HTTPSuccess)
|
|
51
|
-
|
|
52
|
-
read_sse_stream(response) { |event| yield event }
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def headers
|
|
57
|
-
{ "Authorization" => "Bearer #{@api_key}" }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def read_sse_stream(response)
|
|
61
|
-
buffer = String.new
|
|
62
|
-
|
|
63
|
-
response.read_body do |chunk|
|
|
64
|
-
buffer << chunk
|
|
65
|
-
|
|
66
|
-
while (index = buffer.index("\n"))
|
|
67
|
-
line = buffer.slice!(0, index + 1).strip
|
|
68
|
-
next unless line.start_with?("data: ") && line != "data: [DONE]"
|
|
69
|
-
|
|
70
|
-
yield JSON.parse(line.delete_prefix("data: "))
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def finalize_tool_calls(tool_calls)
|
|
76
|
-
tool_calls.values.map do |tool_call|
|
|
77
|
-
{
|
|
78
|
-
id: tool_call[:id],
|
|
79
|
-
name: tool_call[:name],
|
|
80
|
-
arguments: JSON.parse(tool_call[:args])
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
data/lib/elelem/net.rb
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "net/hippie"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
require_relative "net/ollama"
|
|
7
|
-
require_relative "net/openai"
|
|
8
|
-
require_relative "net/claude"
|
|
9
|
-
|
|
10
|
-
module Elelem
|
|
11
|
-
module Net
|
|
12
|
-
def self.http
|
|
13
|
-
@http ||= ::Net::Hippie::Client.new(read_timeout: 3600, open_timeout: 10)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:confirm) do |toolbox|
|
|
4
|
-
toolbox.before("execute") do |args|
|
|
5
|
-
next unless $stdin.tty?
|
|
6
|
-
|
|
7
|
-
cmd = args["command"]
|
|
8
|
-
$stdout.print " Allow? [Y/n] > "
|
|
9
|
-
answer = $stdin.gets&.strip&.downcase
|
|
10
|
-
raise "User denied permission to execute: #{cmd}" if answer == "n"
|
|
11
|
-
end
|
|
12
|
-
end
|
data/lib/elelem/plugins/edit.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:edit) do |toolbox|
|
|
4
|
-
toolbox.add("edit",
|
|
5
|
-
description: "Replace first occurrence of text in file",
|
|
6
|
-
params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
|
|
7
|
-
required: ["path", "old", "new"]
|
|
8
|
-
) do |a|
|
|
9
|
-
path = Pathname.new(a["path"]).expand_path
|
|
10
|
-
content = path.read
|
|
11
|
-
toolbox
|
|
12
|
-
.run("write", { "path" => a["path"], "content" => content.sub(a["old"], a["new"]) })
|
|
13
|
-
.merge(replaced: a["old"], with: a["new"])
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/elelem/plugins/eval.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:eval) do |toolbox|
|
|
4
|
-
description = <<~'DESC'
|
|
5
|
-
Evaluate Ruby code. Available API:
|
|
6
|
-
|
|
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"]}" })
|
|
10
|
-
end
|
|
11
|
-
DESC
|
|
12
|
-
|
|
13
|
-
toolbox.add("eval",
|
|
14
|
-
description: description,
|
|
15
|
-
params: { ruby: { type: "string" } },
|
|
16
|
-
required: ["ruby"]
|
|
17
|
-
) do |args|
|
|
18
|
-
{ result: binding.eval(args["ruby"]) }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:execute) do |toolbox|
|
|
4
|
-
toolbox.add("execute",
|
|
5
|
-
description: "Run shell command (supports pipes and redirections)",
|
|
6
|
-
params: { command: { type: "string" } },
|
|
7
|
-
required: ["command"],
|
|
8
|
-
aliases: ["bash", "sh", "exec", "execute<|channel|>"]
|
|
9
|
-
) do |a|
|
|
10
|
-
Elelem.sh("bash", args: ["-c", a["command"]]) { |x| $stdout.print(x) }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
toolbox.after("execute") do |args, result|
|
|
14
|
-
next if result[:exit_status] == 0
|
|
15
|
-
|
|
16
|
-
$stdout.puts toolbox.header("execute", args, state: "x")
|
|
17
|
-
end
|
|
18
|
-
end
|
data/lib/elelem/plugins/mcp.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:mcp) do |toolbox|
|
|
4
|
-
mcp = Elelem::MCP.new
|
|
5
|
-
at_exit { mcp.close }
|
|
6
|
-
mcp.tools.each do |name, tool|
|
|
7
|
-
toolbox.add(name,
|
|
8
|
-
description: tool[:description],
|
|
9
|
-
params: tool[:params],
|
|
10
|
-
required: tool[:required],
|
|
11
|
-
&tool[:fn]
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
end
|
data/lib/elelem/plugins/read.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:read) do |toolbox|
|
|
4
|
-
toolbox.add("read",
|
|
5
|
-
description: "Read file",
|
|
6
|
-
params: { path: { type: "string" } },
|
|
7
|
-
required: ["path"],
|
|
8
|
-
aliases: ["open"]
|
|
9
|
-
) do |a|
|
|
10
|
-
path = Pathname.new(a["path"]).expand_path
|
|
11
|
-
path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
toolbox.after("read") do |_, result|
|
|
15
|
-
if result[:error]
|
|
16
|
-
$stdout.puts " ! #{result[:error]}"
|
|
17
|
-
elsif !system("bat", "--paging=never", result[:path])
|
|
18
|
-
$stdout.puts result[:content]
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
module Verifiers
|
|
5
|
-
SYNTAX = {
|
|
6
|
-
".rb" => "ruby -c %{path}",
|
|
7
|
-
".erb" => "erb -x %{path} | ruby -c",
|
|
8
|
-
".py" => "python -m py_compile %{path}",
|
|
9
|
-
".go" => "go vet %{path}",
|
|
10
|
-
".rs" => "cargo check --quiet",
|
|
11
|
-
".ts" => "npx tsc --noEmit %{path}",
|
|
12
|
-
".js" => "node --check %{path}",
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
def self.for(path)
|
|
16
|
-
return [] unless path
|
|
17
|
-
|
|
18
|
-
cmds = []
|
|
19
|
-
ext = File.extname(path)
|
|
20
|
-
cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
|
|
21
|
-
cmds << test_runner
|
|
22
|
-
cmds.compact
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.test_runner
|
|
26
|
-
%w[bin/test script/test].find { |s| File.executable?(s) }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
Plugins.register(:verify) do |toolbox|
|
|
31
|
-
toolbox.add("verify",
|
|
32
|
-
description: "Verify file syntax and run tests",
|
|
33
|
-
params: { path: { type: "string" } },
|
|
34
|
-
required: ["path"]
|
|
35
|
-
) do |a|
|
|
36
|
-
path = a["path"]
|
|
37
|
-
Verifiers.for(path).inject({verified: []}) do |memo, cmd|
|
|
38
|
-
$stdout.puts toolbox.header("execute", { "command" => cmd })
|
|
39
|
-
v = toolbox.run("execute", { "command" => cmd })
|
|
40
|
-
break v.merge(path: path, command: cmd) if v[:exit_status] != 0
|
|
41
|
-
|
|
42
|
-
memo[:verified] << cmd
|
|
43
|
-
memo
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
data/lib/elelem/plugins/write.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:write) do |toolbox|
|
|
4
|
-
toolbox.add("write",
|
|
5
|
-
description: "Write file",
|
|
6
|
-
params: { path: { type: "string" }, content: { type: "string" } },
|
|
7
|
-
required: ["path", "content"],
|
|
8
|
-
aliases: ["write<|channel|>"]
|
|
9
|
-
) do |a|
|
|
10
|
-
path = Pathname.new(a["path"]).expand_path
|
|
11
|
-
FileUtils.mkdir_p(path.dirname)
|
|
12
|
-
{ bytes: path.write(a["content"]), path: a["path"] }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
toolbox.after("write") do |_, result|
|
|
16
|
-
if result[:error]
|
|
17
|
-
$stdout.puts " ! #{result[:error]}"
|
|
18
|
-
else
|
|
19
|
-
system("bat", "--paging=never", result[:path]) || $stdout.puts(" -> #{result[:path]}")
|
|
20
|
-
toolbox.run("verify", { "path" => result[:path] })
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
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 %>
|