elelem 0.9.2 → 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 +29 -0
- data/README.md +93 -16
- data/Rakefile +0 -11
- data/exe/elelem +1 -78
- 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
data/lib/elelem/toolbox.rb
CHANGED
|
@@ -16,17 +16,19 @@ module Elelem
|
|
|
16
16
|
tool.aliases.each { |a| @aliases[a] = name }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def before(tool_name
|
|
19
|
+
def before(tool_name = :*, &block)
|
|
20
20
|
@hooks[:before][tool_name] << block
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def after(tool_name
|
|
23
|
+
def after(tool_name = :*, &block)
|
|
24
24
|
@hooks[:after][tool_name] << block
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def header(name, args, state: "+")
|
|
28
|
-
|
|
29
|
-
"
|
|
28
|
+
tool = tool_for(name)
|
|
29
|
+
color = tool ? "36" : "33"
|
|
30
|
+
name = tool&.name || "#{name}?"
|
|
31
|
+
"\n#{state} \e[#{color}m#{name}\e[0m(#{args})"
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def run(name, args)
|
|
@@ -36,14 +38,21 @@ module Elelem
|
|
|
36
38
|
errors = tool.validate(args)
|
|
37
39
|
return failure(error: errors.join(", ")) if errors.any?
|
|
38
40
|
|
|
41
|
+
@hooks[:before][:*].each { |h| h.call(args, tool_name: tool.name) }
|
|
39
42
|
@hooks[:before][tool.name].each { |h| h.call(args) }
|
|
40
43
|
result = tool.call(args)
|
|
44
|
+
@hooks[:after][:*].each { |h| h.call(args, result, tool_name: tool.name) }
|
|
41
45
|
@hooks[:after][tool.name].each { |h| h.call(args, result) }
|
|
42
46
|
result[:error] ? failure(result) : success(result)
|
|
43
47
|
rescue => e
|
|
44
48
|
failure(error: e.message, name: name, args: args)
|
|
45
49
|
end
|
|
46
50
|
|
|
51
|
+
def exec(*args)
|
|
52
|
+
command = args.flatten.map { |a| Shellwords.escape(a.to_s) }.join(" ")
|
|
53
|
+
run("execute", { "command" => command })
|
|
54
|
+
end
|
|
55
|
+
|
|
47
56
|
def to_a
|
|
48
57
|
tools.values.map(&:to_h)
|
|
49
58
|
end
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "base64"
|
|
3
4
|
require "date"
|
|
5
|
+
require "digest"
|
|
4
6
|
require "erb"
|
|
5
7
|
require "fileutils"
|
|
6
8
|
require "json"
|
|
7
9
|
require "json_schemer"
|
|
10
|
+
require "logger"
|
|
11
|
+
require "net/hippie"
|
|
8
12
|
require "open3"
|
|
13
|
+
require "optparse"
|
|
9
14
|
require "pathname"
|
|
10
15
|
require "reline"
|
|
16
|
+
require "securerandom"
|
|
11
17
|
require "stringio"
|
|
12
18
|
require "tempfile"
|
|
19
|
+
require "uri"
|
|
20
|
+
require "webrick"
|
|
13
21
|
|
|
14
22
|
require_relative "elelem/agent"
|
|
23
|
+
require_relative "elelem/commands"
|
|
24
|
+
require_relative "elelem/conversation"
|
|
15
25
|
require_relative "elelem/mcp"
|
|
16
26
|
require_relative "elelem/net"
|
|
27
|
+
require_relative "elelem/permissions"
|
|
17
28
|
require_relative "elelem/plugins"
|
|
18
29
|
require_relative "elelem/system_prompt"
|
|
19
30
|
require_relative "elelem/terminal"
|
|
@@ -37,14 +48,94 @@ module Elelem
|
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
def self.start(client, toolbox: Toolbox.new)
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
agent = Agent.new(client, toolbox: toolbox)
|
|
52
|
+
Plugins.setup!(agent)
|
|
53
|
+
agent.terminal = Terminal.new(commands: agent.commands.names)
|
|
54
|
+
agent.repl
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def self.ask(client, prompt, toolbox: Toolbox.new)
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
agent = Agent.new(client, toolbox: toolbox, terminal: Terminal.new(quiet: true))
|
|
59
|
+
Plugins.setup!(agent)
|
|
47
60
|
agent.turn(prompt)
|
|
48
|
-
agent.
|
|
61
|
+
agent.conversation.last[:content]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class CLI
|
|
65
|
+
MODELS = {
|
|
66
|
+
"ollama" => "gpt-oss:latest",
|
|
67
|
+
"anthropic" => "claude-opus-4-5-20250514",
|
|
68
|
+
"vertex" => "claude-opus-4-5@20251101",
|
|
69
|
+
"openai" => "gpt-4o"
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
PROVIDERS = {
|
|
73
|
+
"ollama" => ->(model) { Elelem::Net::Ollama.new(model: model, host: ENV.fetch("OLLAMA_HOST", "localhost:11434")) },
|
|
74
|
+
"anthropic" => ->(model) { Elelem::Net::Claude.anthropic(model: model, api_key: ENV.fetch("ANTHROPIC_API_KEY")) },
|
|
75
|
+
"vertex" => ->(model) { Elelem::Net::Claude.vertex(model: model, project: ENV.fetch("GOOGLE_CLOUD_PROJECT"), region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")) },
|
|
76
|
+
"openai" => ->(model) { Elelem::Net::OpenAI.new(model: model, api_key: ENV.fetch("OPENAI_API_KEY")) }
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
def initialize(args)
|
|
80
|
+
@provider = "ollama"
|
|
81
|
+
@model = nil
|
|
82
|
+
@args = parse(args)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def run
|
|
86
|
+
command = @args.shift || "chat"
|
|
87
|
+
send(command.tr("-", "_"))
|
|
88
|
+
rescue NoMethodError
|
|
89
|
+
abort "Unknown command: #{command}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def parse(args)
|
|
95
|
+
@parser = OptionParser.new do |o|
|
|
96
|
+
o.banner = "Usage: elelem [command] [options] [args]"
|
|
97
|
+
o.separator "\nCommands:"
|
|
98
|
+
o.separator " chat Interactive REPL (default)"
|
|
99
|
+
o.separator " ask <prompt> One-shot query (reads stdin if piped)"
|
|
100
|
+
o.separator " files Output files as XML (no options)"
|
|
101
|
+
o.separator " help Show this help"
|
|
102
|
+
o.separator "\nOptions:"
|
|
103
|
+
o.on("-p", "--provider NAME", "ollama, anthropic, vertex, openai") { |p| @provider = p }
|
|
104
|
+
o.on("-m", "--model NAME", "Override default model") { |m| @model = m }
|
|
105
|
+
o.on("-h", "--help") { puts o; exit }
|
|
106
|
+
end
|
|
107
|
+
@parser.parse!(args)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def help
|
|
111
|
+
puts @parser
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def client
|
|
115
|
+
model = @model || MODELS.fetch(@provider)
|
|
116
|
+
PROVIDERS.fetch(@provider).call(model)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def chat
|
|
120
|
+
Elelem.start(client)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ask
|
|
124
|
+
abort "Usage: elelem ask <prompt>" if @args.empty?
|
|
125
|
+
prompt = @args.join(" ")
|
|
126
|
+
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
|
|
127
|
+
Elelem::Terminal.new.markdown Elelem.ask(client, prompt)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def files
|
|
131
|
+
files = $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
|
|
132
|
+
puts "<documents>"
|
|
133
|
+
files.each_with_index do |line, i|
|
|
134
|
+
path = line.strip
|
|
135
|
+
next if path.empty? || !File.file?(path)
|
|
136
|
+
puts %Q{<document index="#{i + 1}"><source>#{path}</source><content><![CDATA[#{File.read(path)}]]></content></document>}
|
|
137
|
+
end
|
|
138
|
+
puts "</documents>"
|
|
139
|
+
end
|
|
49
140
|
end
|
|
50
141
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: elelem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mo khan
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: date
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +37,20 @@ dependencies:
|
|
|
23
37
|
- - "~>"
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
39
|
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: digest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
55
|
name: erb
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +107,20 @@ dependencies:
|
|
|
79
107
|
- - "~>"
|
|
80
108
|
- !ruby/object:Gem::Version
|
|
81
109
|
version: '2.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: logger
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '1.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '1.0'
|
|
82
124
|
- !ruby/object:Gem::Dependency
|
|
83
125
|
name: net-hippie
|
|
84
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,6 +191,34 @@ dependencies:
|
|
|
149
191
|
- - "~>"
|
|
150
192
|
- !ruby/object:Gem::Version
|
|
151
193
|
version: '0.6'
|
|
194
|
+
- !ruby/object:Gem::Dependency
|
|
195
|
+
name: securerandom
|
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - "~>"
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: '0.1'
|
|
201
|
+
type: :runtime
|
|
202
|
+
prerelease: false
|
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
204
|
+
requirements:
|
|
205
|
+
- - "~>"
|
|
206
|
+
- !ruby/object:Gem::Version
|
|
207
|
+
version: '0.1'
|
|
208
|
+
- !ruby/object:Gem::Dependency
|
|
209
|
+
name: shellwords
|
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - "~>"
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '0.2'
|
|
215
|
+
type: :runtime
|
|
216
|
+
prerelease: false
|
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
218
|
+
requirements:
|
|
219
|
+
- - "~>"
|
|
220
|
+
- !ruby/object:Gem::Version
|
|
221
|
+
version: '0.2'
|
|
152
222
|
- !ruby/object:Gem::Dependency
|
|
153
223
|
name: stringio
|
|
154
224
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -191,6 +261,20 @@ dependencies:
|
|
|
191
261
|
- - "~>"
|
|
192
262
|
- !ruby/object:Gem::Version
|
|
193
263
|
version: '1.0'
|
|
264
|
+
- !ruby/object:Gem::Dependency
|
|
265
|
+
name: webrick
|
|
266
|
+
requirement: !ruby/object:Gem::Requirement
|
|
267
|
+
requirements:
|
|
268
|
+
- - "~>"
|
|
269
|
+
- !ruby/object:Gem::Version
|
|
270
|
+
version: '1.9'
|
|
271
|
+
type: :runtime
|
|
272
|
+
prerelease: false
|
|
273
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
274
|
+
requirements:
|
|
275
|
+
- - "~>"
|
|
276
|
+
- !ruby/object:Gem::Version
|
|
277
|
+
version: '1.9'
|
|
194
278
|
description: A minimal coding agent supporting Ollama, Anthropic, OpenAI, and VertexAI.
|
|
195
279
|
email:
|
|
196
280
|
- mo@mokhan.ca
|
|
@@ -206,22 +290,34 @@ files:
|
|
|
206
290
|
- exe/elelem
|
|
207
291
|
- lib/elelem.rb
|
|
208
292
|
- lib/elelem/agent.rb
|
|
293
|
+
- lib/elelem/commands.rb
|
|
294
|
+
- lib/elelem/conversation.rb
|
|
209
295
|
- lib/elelem/mcp.rb
|
|
296
|
+
- lib/elelem/mcp/oauth.rb
|
|
297
|
+
- lib/elelem/mcp/token_storage.rb
|
|
210
298
|
- lib/elelem/net.rb
|
|
211
299
|
- lib/elelem/net/claude.rb
|
|
212
300
|
- lib/elelem/net/ollama.rb
|
|
213
301
|
- lib/elelem/net/openai.rb
|
|
302
|
+
- lib/elelem/permissions.rb
|
|
214
303
|
- lib/elelem/plugins.rb
|
|
215
|
-
- lib/elelem/plugins/
|
|
304
|
+
- lib/elelem/plugins/builtins.rb
|
|
216
305
|
- lib/elelem/plugins/edit.rb
|
|
217
306
|
- lib/elelem/plugins/eval.rb
|
|
218
307
|
- lib/elelem/plugins/execute.rb
|
|
308
|
+
- lib/elelem/plugins/git.rb
|
|
309
|
+
- lib/elelem/plugins/glob.rb
|
|
310
|
+
- lib/elelem/plugins/grep.rb
|
|
311
|
+
- lib/elelem/plugins/list.rb
|
|
219
312
|
- lib/elelem/plugins/mcp.rb
|
|
313
|
+
- lib/elelem/plugins/permissions.json
|
|
220
314
|
- lib/elelem/plugins/read.rb
|
|
315
|
+
- lib/elelem/plugins/task.rb
|
|
316
|
+
- lib/elelem/plugins/tools.rb
|
|
221
317
|
- lib/elelem/plugins/verify.rb
|
|
222
318
|
- lib/elelem/plugins/write.rb
|
|
319
|
+
- lib/elelem/plugins/zz_confirm.rb
|
|
223
320
|
- lib/elelem/system_prompt.rb
|
|
224
|
-
- lib/elelem/templates/system_prompt.erb
|
|
225
321
|
- lib/elelem/terminal.rb
|
|
226
322
|
- lib/elelem/tool.rb
|
|
227
323
|
- lib/elelem/toolbox.rb
|
|
@@ -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
|
|
@@ -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 %>
|