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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +93 -16
  4. data/Rakefile +0 -11
  5. data/exe/elelem +1 -78
  6. data/lib/elelem/agent.rb +33 -124
  7. data/lib/elelem/commands.rb +33 -0
  8. data/lib/elelem/conversation.rb +25 -0
  9. data/lib/elelem/mcp/oauth.rb +217 -0
  10. data/lib/elelem/mcp/token_storage.rb +60 -0
  11. data/lib/elelem/mcp.rb +164 -17
  12. data/lib/elelem/net/claude.rb +6 -4
  13. data/lib/elelem/net/ollama.rb +5 -2
  14. data/lib/elelem/net/openai.rb +6 -4
  15. data/lib/elelem/net.rb +0 -3
  16. data/lib/elelem/permissions.rb +45 -0
  17. data/lib/elelem/plugins/builtins.rb +96 -0
  18. data/lib/elelem/plugins/edit.rb +3 -3
  19. data/lib/elelem/plugins/eval.rb +4 -4
  20. data/lib/elelem/plugins/execute.rb +5 -5
  21. data/lib/elelem/plugins/git.rb +20 -0
  22. data/lib/elelem/plugins/glob.rb +13 -0
  23. data/lib/elelem/plugins/grep.rb +21 -0
  24. data/lib/elelem/plugins/list.rb +14 -0
  25. data/lib/elelem/plugins/mcp.rb +14 -8
  26. data/lib/elelem/plugins/permissions.json +6 -0
  27. data/lib/elelem/plugins/read.rb +6 -6
  28. data/lib/elelem/plugins/task.rb +14 -0
  29. data/lib/elelem/plugins/tools.rb +13 -0
  30. data/lib/elelem/plugins/verify.rb +4 -4
  31. data/lib/elelem/plugins/write.rb +17 -6
  32. data/lib/elelem/plugins/zz_confirm.rb +9 -0
  33. data/lib/elelem/plugins.rb +6 -6
  34. data/lib/elelem/system_prompt.rb +123 -29
  35. data/lib/elelem/terminal.rb +7 -1
  36. data/lib/elelem/tool.rb +6 -15
  37. data/lib/elelem/toolbox.rb +13 -4
  38. data/lib/elelem/version.rb +1 -1
  39. data/lib/elelem.rb +96 -5
  40. metadata +99 -3
  41. data/lib/elelem/plugins/confirm.rb +0 -12
  42. data/lib/elelem/templates/system_prompt.erb +0 -53
@@ -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, &block)
19
+ def before(tool_name = :*, &block)
20
20
  @hooks[:before][tool_name] << block
21
21
  end
22
22
 
23
- def after(tool_name, &block)
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
- name = tool_for(name)&.name || "#{name}?"
29
- "\n#{state} #{name}(#{args})"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.9.2"
4
+ VERSION = "0.10.0"
5
5
  end
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
- Plugins.setup!(toolbox)
41
- Agent.new(client, toolbox).repl
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
- Plugins.setup!(toolbox)
46
- agent = Agent.new(client, toolbox, terminal: Terminal.new(quiet: true))
58
+ agent = Agent.new(client, toolbox: toolbox, terminal: Terminal.new(quiet: true))
59
+ Plugins.setup!(agent)
47
60
  agent.turn(prompt)
48
- agent.history.last[:content]
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.9.2
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/confirm.rb
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 %>