elelem 0.4.2 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c9f3ee321bf585693ae2be5186a3b84f0122f282bcd8cb6d320b92931eb0d13
4
- data.tar.gz: f9200f6646a49666394493f46a99ec5ebb157d415e35208a8cdb405736c822c1
3
+ metadata.gz: 9a97a3be43a2528518770e2881fef86138b19ce9394601de922822f9748fe9c8
4
+ data.tar.gz: e87dc58d17f701d9b2fa0a15b05b7f2fbad24a7f71b5836a47f01fe6b216df55
5
5
  SHA512:
6
- metadata.gz: b62630ba5787d5b7daa55113c01012ade1075f4d9618b43c6a4ce46643b4a29ce3d380c864070f6aa43e26732ae6e20b07f4ce7eb7b4b251b90fd83f5ff5c781
7
- data.tar.gz: 48929614d89e694d9d153baa195276042d586a537f1197c588156cf8d192afd4b6b134bad5bd30c836a05358ff26607ae6b7531f5f71e33f9e0291f0d5331e2d
6
+ metadata.gz: e7b58a5575c8065b1dd9bd020615dc6ec85660b8f5df431f01d28fca90d199608a845aae8e915fd6508c23817f98b2f8c5396e79595d293c1eedbe7f4e9146a7
7
+ data.tar.gz: 6556959f36182acd496d99bc64001bb1ccdff79a95d14ec3116dec1f86a4da928fc35e5f8bc32e84b93161d3013f98f374c2f4188efe9374121e80829e2ca157
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-01-12
4
+
5
+ ### Added
6
+ - `/env` slash command to capture environment variables for provider connections
7
+ - `/shell` slash command
8
+ - `/provider` and `/model` slash commands
9
+ - Tab completion for commands
10
+ - Help output for `/mode` and `/env` commands
11
+
12
+ ### Changed
13
+ - Renamed `bash` tool to `exec`
14
+ - Tuned system prompt
15
+ - Changed thinking prompt to ellipsis
16
+ - Removed username from system prompt
17
+ - Use pessimistic constraint on net-llm dependency
18
+ - Extracted Terminal class for IO abstraction (enables E2E testing)
19
+
20
+ ### Fixed
21
+ - Prevent infinite looping errors
22
+ - Provide function schema when tool is called with invalid arguments
23
+ - Tab completion for `pass` entries without requiring `show` subcommand
24
+ - Password store symlink support in tab completion
25
+
26
+ ## [0.5.0] - 2025-01-07
27
+
28
+ ### Added
29
+ - Multi-provider support: Ollama, Anthropic, OpenAI, and VertexAI
30
+ - `--provider` CLI option to select LLM provider (default: ollama)
31
+ - `--model` CLI option to override default model
32
+ - Tool aliases (`bash` also accepts `exec`, `shell`, `command`, `terminal`, `run`)
33
+ - Thinking text output for models that support extended thinking
34
+
35
+ ### Changed
36
+ - Requires net-llm >= 0.5.0 with unified fetch interface
37
+ - Updated gem description to reflect multi-provider support
38
+
3
39
  ## [0.4.2] - 2025-12-01
4
40
 
5
41
  ### Changed
data/README.md CHANGED
@@ -63,7 +63,7 @@ gem install elelem
63
63
 
64
64
  ## Usage
65
65
 
66
- Start an interactive chat session with an Ollama model:
66
+ Start an interactive chat session:
67
67
 
68
68
  ```bash
69
69
  elelem chat
@@ -71,20 +71,36 @@ elelem chat
71
71
 
72
72
  ### Options
73
73
 
74
- * `--host` – Ollama host (default: `localhost:11434`).
75
- * `--model` – Ollama model (default: `gpt-oss`).
76
- * `--token` – Authentication token.
74
+ * `--provider` – LLM provider: `ollama`, `anthropic`, `openai`, or `vertex-ai` (default: `ollama`).
75
+ * `--model` – Override the default model for the selected provider.
77
76
 
78
77
  ### Examples
79
78
 
80
79
  ```bash
81
- # Default model
80
+ # Default (Ollama)
82
81
  elelem chat
83
82
 
84
- # Specific model and host
85
- elelem chat --model llama2 --host remote-host:11434
83
+ # Anthropic Claude
84
+ ANTHROPIC_API_KEY=sk-... elelem chat --provider anthropic
85
+
86
+ # OpenAI
87
+ OPENAI_API_KEY=sk-... elelem chat --provider openai
88
+
89
+ # VertexAI (uses gcloud ADC)
90
+ elelem chat --provider vertex-ai --model claude-sonnet-4@20250514
86
91
  ```
87
92
 
93
+ ### Provider Configuration
94
+
95
+ Each provider reads its configuration from environment variables:
96
+
97
+ | Provider | Environment Variables |
98
+ |-------------|---------------------------------------------------|
99
+ | ollama | `OLLAMA_HOST` (default: localhost:11434) |
100
+ | anthropic | `ANTHROPIC_API_KEY` |
101
+ | openai | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
102
+ | vertex-ai | `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_REGION` |
103
+
88
104
  ## Mode System
89
105
 
90
106
  The agent exposes seven built‑in tools. You can switch which ones are
@@ -125,7 +141,7 @@ seven tools, each represented by a JSON schema that the LLM can call.
125
141
 
126
142
  | Tool | Purpose | Parameters |
127
143
  | ---- | ------- | ---------- |
128
- | `bash` | Run shell commands | `cmd`, `args`, `env`, `cwd`, `stdin` |
144
+ | `exec` | Run shell commands | `cmd`, `args`, `env`, `cwd`, `stdin` |
129
145
  | `eval` | Dynamically create new tools | `code` |
130
146
  | `grep` | Search Git‑tracked files | `query` |
131
147
  | `list` | List tracked files | `path` (optional) |
@@ -148,8 +164,7 @@ arguments as a hash.
148
164
 
149
165
  ## Contributing
150
166
 
151
- Feel free to open issues or pull requests. The repository follows the
152
- GitHub Flow.
167
+ Send me an email. For instructions see https://git-send-email.io/.
153
168
 
154
169
  ## License
155
170
 
data/lib/elelem/agent.rb CHANGED
@@ -2,45 +2,36 @@
2
2
 
3
3
  module Elelem
4
4
  class Agent
5
- attr_reader :conversation, :client, :toolbox
5
+ PROVIDERS = %w[ollama anthropic openai vertex-ai].freeze
6
+ ANTHROPIC_MODELS = %w[claude-sonnet-4-20250514 claude-opus-4-20250514 claude-haiku-3-5-20241022].freeze
7
+ VERTEX_MODELS = %w[claude-sonnet-4@20250514 claude-opus-4-5@20251101].freeze
8
+ COMMANDS = %w[/env /mode /provider /model /shell /clear /context /exit /help].freeze
9
+ MODES = %w[auto build plan verify].freeze
10
+ ENV_VARS = %w[ANTHROPIC_API_KEY OPENAI_API_KEY OPENAI_BASE_URL OLLAMA_HOST GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_REGION].freeze
6
11
 
7
- def initialize(client, toolbox)
12
+ attr_reader :conversation, :client, :toolbox, :provider, :terminal
13
+
14
+ def initialize(provider, model, toolbox, terminal: nil)
8
15
  @conversation = Conversation.new
9
- @client = client
16
+ @provider = provider
10
17
  @toolbox = toolbox
18
+ @client = build_client(provider, model)
19
+ @terminal = terminal || Terminal.new(
20
+ commands: COMMANDS,
21
+ modes: MODES,
22
+ providers: PROVIDERS,
23
+ env_vars: ENV_VARS
24
+ )
11
25
  end
12
26
 
13
27
  def repl
14
28
  mode = Set.new([:read])
15
29
 
16
30
  loop do
17
- input = ask?("User> ")
31
+ input = terminal.ask("User> ")
18
32
  break if input.nil?
19
33
  if input.start_with?("/")
20
- case input
21
- when "/mode auto"
22
- mode = Set[:read, :write, :execute]
23
- puts " → Mode: auto (all tools enabled)"
24
- when "/mode build"
25
- mode = Set[:read, :write]
26
- puts " → Mode: build (read + write)"
27
- when "/mode plan"
28
- mode = Set[:read]
29
- puts " → Mode: plan (read-only)"
30
- when "/mode verify"
31
- mode = Set[:read, :execute]
32
- puts " → Mode: verify (read + execute)"
33
- when "/mode"
34
- puts " Mode: #{mode.to_a.inspect}"
35
- puts " Tools: #{toolbox.tools_for(mode).map { |t| t.dig(:function, :name) }}"
36
- when "/exit" then exit
37
- when "/clear"
38
- conversation.clear
39
- puts " → Conversation cleared"
40
- when "/context" then puts conversation.dump(mode)
41
- else
42
- puts help_banner
43
- end
34
+ handle_command(input, mode)
44
35
  else
45
36
  conversation.add(role: :user, content: input)
46
37
  result = execute_turn(conversation.history_for(mode), tools: toolbox.tools_for(mode))
@@ -51,13 +42,111 @@ module Elelem
51
42
 
52
43
  private
53
44
 
54
- def ask?(text)
55
- Reline.readline(text, true)&.strip
45
+ def handle_command(input, mode)
46
+ case input
47
+ when "/mode auto"
48
+ mode.replace([:read, :write, :execute])
49
+ terminal.say " → Mode: auto (all tools enabled)"
50
+ when "/mode build"
51
+ mode.replace([:read, :write])
52
+ terminal.say " → Mode: build (read + write)"
53
+ when "/mode plan"
54
+ mode.replace([:read])
55
+ terminal.say " → Mode: plan (read-only)"
56
+ when "/mode verify"
57
+ mode.replace([:read, :execute])
58
+ terminal.say " → Mode: verify (read + execute)"
59
+ when "/mode"
60
+ terminal.say " Usage: /mode [auto|build|plan|verify]"
61
+ terminal.say ""
62
+ terminal.say " Provider: #{provider}/#{client.model}"
63
+ terminal.say " Mode: #{mode.to_a.inspect}"
64
+ terminal.say " Tools: #{toolbox.tools_for(mode).map { |t| t.dig(:function, :name) }}"
65
+ when "/exit" then exit
66
+ when "/clear"
67
+ conversation.clear
68
+ terminal.say " → Conversation cleared"
69
+ when "/context"
70
+ terminal.say conversation.dump(mode)
71
+ when "/shell"
72
+ transcript = start_shell
73
+ conversation.add(role: :user, content: transcript) unless transcript.strip.empty?
74
+ terminal.say " → Shell session captured"
75
+ when "/provider"
76
+ terminal.select("Provider?", PROVIDERS) do |selected_provider|
77
+ models = models_for(selected_provider)
78
+ if models.empty?
79
+ terminal.say " ✗ No models available for #{selected_provider}"
80
+ else
81
+ terminal.select("Model?", models) do |m|
82
+ switch_client(selected_provider, m)
83
+ end
84
+ end
85
+ end
86
+ when "/model"
87
+ models = models_for(provider)
88
+ if models.empty?
89
+ terminal.say " ✗ No models available for #{provider}"
90
+ else
91
+ terminal.select("Model?", models) do |m|
92
+ switch_model(m)
93
+ end
94
+ end
95
+ when "/env"
96
+ terminal.say " Usage: /env VAR cmd..."
97
+ terminal.say ""
98
+ ENV_VARS.each do |var|
99
+ value = ENV[var]
100
+ if value
101
+ masked = value.length > 8 ? "#{value[0..3]}...#{value[-4..]}" : "****"
102
+ terminal.say " #{var}=#{masked}"
103
+ else
104
+ terminal.say " #{var}=(not set)"
105
+ end
106
+ end
107
+ when %r{^/env\s+(\w+)\s+(.+)$}
108
+ var_name = $1
109
+ command = $2
110
+ result = Elelem.shell.execute("sh", args: ["-c", command])
111
+ if result["exit_status"].zero?
112
+ value = result["stdout"].lines.first&.strip
113
+ if value && !value.empty?
114
+ ENV[var_name] = value
115
+ terminal.say " → Set #{var_name}"
116
+ else
117
+ terminal.say " ⚠ Command produced no output"
118
+ end
119
+ else
120
+ terminal.say " ⚠ Command failed: #{result['stderr']}"
121
+ end
122
+ else
123
+ terminal.say help_banner
124
+ end
125
+ end
126
+
127
+ def strip_ansi(text)
128
+ text.gsub(/^Script started.*?\n/, '')
129
+ .gsub(/\nScript done.*$/, '')
130
+ .gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
131
+ .gsub(/\e\[\?[0-9]+[hl]/, '')
132
+ .gsub(/[\b]/, '')
133
+ .gsub(/\r/, '')
134
+ end
135
+
136
+ def start_shell
137
+ Tempfile.create do |file|
138
+ system("script -q #{file.path}", chdir: Dir.pwd)
139
+ strip_ansi(File.read(file.path))
140
+ end
56
141
  end
57
142
 
58
143
  def help_banner
59
144
  <<~HELP
145
+ /env VAR cmd...
60
146
  /mode auto build plan verify
147
+ /provider
148
+ /model
149
+ /shell
61
150
  /clear
62
151
  /context
63
152
  /exit
@@ -65,6 +154,53 @@ module Elelem
65
154
  HELP
66
155
  end
67
156
 
157
+ def build_client(provider_name, model = nil)
158
+ model_opts = model ? { model: model } : {}
159
+
160
+ case provider_name
161
+ when "ollama" then Net::Llm::Ollama.new(**model_opts)
162
+ when "anthropic" then Net::Llm::Anthropic.new(**model_opts)
163
+ when "openai" then Net::Llm::OpenAI.new(**model_opts)
164
+ when "vertex-ai" then Net::Llm::VertexAI.new(**model_opts)
165
+ else
166
+ raise Error, "Unknown provider: #{provider_name}"
167
+ end
168
+ end
169
+
170
+ def models_for(provider_name)
171
+ case provider_name
172
+ when "ollama"
173
+ client_for_models = provider_name == provider ? client : build_client(provider_name)
174
+ client_for_models.tags["models"]&.map { |m| m["name"] } || []
175
+ when "openai"
176
+ client_for_models = provider_name == provider ? client : build_client(provider_name)
177
+ client_for_models.models["data"]&.map { |m| m["id"] } || []
178
+ when "anthropic"
179
+ ANTHROPIC_MODELS
180
+ when "vertex-ai"
181
+ VERTEX_MODELS
182
+ else
183
+ []
184
+ end
185
+ rescue KeyError => e
186
+ terminal.say " ⚠ Missing credentials: #{e.message}"
187
+ []
188
+ rescue => e
189
+ terminal.say " ⚠ Could not fetch models: #{e.message}"
190
+ []
191
+ end
192
+
193
+ def switch_client(new_provider, model)
194
+ @provider = new_provider
195
+ @client = build_client(new_provider, model)
196
+ terminal.say " → Switched to #{new_provider}/#{client.model}"
197
+ end
198
+
199
+ def switch_model(model)
200
+ @client = build_client(provider, model)
201
+ terminal.say " → Switched to #{provider}/#{client.model}"
202
+ end
203
+
68
204
  def format_tool_call_result(result)
69
205
  return if result.nil?
70
206
  return result["stdout"] if result["stdout"]
@@ -74,42 +210,71 @@ module Elelem
74
210
  ""
75
211
  end
76
212
 
213
+ def truncate_output(text, max_lines: 30)
214
+ return text if text.nil? || text.empty?
215
+
216
+ lines = text.to_s.lines
217
+ if lines.size > max_lines
218
+ lines.first(max_lines).join + "\n... (#{lines.size - max_lines} more lines)"
219
+ else
220
+ text
221
+ end
222
+ end
223
+
224
+ def format_tool_calls_for_api(tool_calls)
225
+ tool_calls.map do |tc|
226
+ args = openai_client? ? JSON.dump(tc[:arguments]) : tc[:arguments]
227
+ {
228
+ id: tc[:id],
229
+ type: "function",
230
+ function: { name: tc[:name], arguments: args }
231
+ }
232
+ end
233
+ end
234
+
235
+ def openai_client?
236
+ client.is_a?(Net::Llm::OpenAI)
237
+ end
238
+
77
239
  def execute_turn(messages, tools:)
78
240
  turn_context = []
241
+ errors = 0
79
242
 
80
243
  loop do
81
244
  content = ""
82
245
  tool_calls = []
83
246
 
84
- print "Assistant> Thinking..."
85
- client.chat(messages + turn_context, tools) do |chunk|
86
- msg = chunk["message"]
87
- if msg
88
- if msg["content"] && !msg["content"].empty?
89
- print "\r\e[K" if content.empty?
90
- print msg["content"]
91
- content += msg["content"]
247
+ terminal.write "Thinking... "
248
+ begin
249
+ client.fetch(messages + turn_context, tools) do |chunk|
250
+ case chunk[:type]
251
+ when :delta
252
+ terminal.write chunk[:thinking] if chunk[:thinking]
253
+ content += chunk[:content] if chunk[:content]
254
+ when :complete
255
+ content = chunk[:content] if chunk[:content]
256
+ tool_calls = chunk[:tool_calls] || []
92
257
  end
93
-
94
- tool_calls += msg["tool_calls"] if msg["tool_calls"]
95
258
  end
259
+ rescue => e
260
+ terminal.say "\n ✗ API Error: #{e.message}"
261
+ return { role: "assistant", content: "[Error: #{e.message}]" }
96
262
  end
97
263
 
98
- puts
99
- turn_context << { role: "assistant", content: content, tool_calls: tool_calls }.compact
264
+ terminal.say "\nAssistant> #{content}" unless content.to_s.empty?
265
+ api_tool_calls = tool_calls.any? ? format_tool_calls_for_api(tool_calls) : nil
266
+ turn_context << { role: "assistant", content: content, tool_calls: api_tool_calls }.compact
100
267
 
101
268
  if tool_calls.any?
102
269
  tool_calls.each do |call|
103
- name = call.dig("function", "name")
104
- args = call.dig("function", "arguments")
105
-
106
- puts "Tool> #{name}(#{args})"
270
+ name, args = call[:name], call[:arguments]
271
+ terminal.say "\nTool> #{name}(#{args})"
107
272
  result = toolbox.run_tool(name, args)
108
- puts format_tool_call_result(result)
109
- turn_context << { role: "tool", content: JSON.dump(result) }
273
+ terminal.say truncate_output(format_tool_call_result(result))
274
+ turn_context << { role: "tool", tool_call_id: call[:id], content: JSON.dump(result) }
275
+ errors += 1 if result[:error]
110
276
  end
111
-
112
- tool_calls = []
277
+ return { role: "assistant", content: "[Stopped: too many errors]" } if errors >= 3
113
278
  next
114
279
  end
115
280
 
@@ -2,24 +2,23 @@
2
2
 
3
3
  module Elelem
4
4
  class Application < Thor
5
+ PROVIDERS = %w[ollama anthropic openai vertex-ai].freeze
6
+
5
7
  desc "chat", "Start the REPL"
6
- method_option :host,
7
- aliases: "--host",
8
+ method_option :provider,
9
+ aliases: "-p",
8
10
  type: :string,
9
- desc: "Ollama host",
10
- default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
11
+ desc: "LLM provider (#{PROVIDERS.join(', ')})",
12
+ default: ENV.fetch("ELELEM_PROVIDER", "ollama")
11
13
  method_option :model,
12
- aliases: "--model",
14
+ aliases: "-m",
13
15
  type: :string,
14
- desc: "Ollama model",
15
- default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
16
+ desc: "Model name (uses provider default if not specified)"
16
17
  def chat(*)
17
- client = Net::Llm::Ollama.new(
18
- host: options[:host],
19
- model: options[:model],
20
- )
21
- say "Agent (#{options[:model]})", :green
22
- agent = Agent.new(client, Toolbox.new)
18
+ provider = options[:provider]
19
+ model = options[:model]
20
+ say "Agent (#{provider})", :green
21
+ agent = Agent.new(provider, model, Toolbox.new)
23
22
  agent.repl
24
23
  end
25
24
 
@@ -1,15 +1,12 @@
1
- You are a reasoning coding and system agent.
1
+ You are a trusted terminal agent. You act on behalf of the user - executing tasks directly through bash, files, and git. Be capable, be direct, be done.
2
+
3
+ ## Principles
4
+
5
+ - Act, don't explain. Execute the task.
6
+ - Read before write. Understand existing code first.
7
+ - Small focused changes. One thing at a time.
8
+ - Verify your work. Run tests, check output.
2
9
 
3
10
  ## System
4
11
 
5
- Operating System: <%= `uname -a` %>
6
- USER: <%= ENV['USER'] %>
7
- HOME: <%= ENV['HOME'] %>
8
- SHELL: <%= ENV['SHELL'] %>
9
- PATH: <%= ENV['PATH'] %>
10
- PWD: <%= ENV['PWD'] %>
11
- LANG: <%= ENV['LANG'] %>
12
- EDITOR: <%= ENV['EDITOR'] %>
13
- LOGNAME: <%= ENV['LOGNAME'] %>
14
- TERM: <%= ENV['TERM'] %>
15
- MAIL: <%= ENV['MAIL'] %>
12
+ <%= `uname -s`.strip %> · <%= ENV['PWD'] %>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Terminal
5
+ def initialize(commands: [], modes: [], providers: [], env_vars: [])
6
+ @commands = commands
7
+ @modes = modes
8
+ @providers = providers
9
+ @env_vars = env_vars
10
+ setup_completion
11
+ end
12
+
13
+ def ask(prompt)
14
+ Reline.readline(prompt, true)&.strip
15
+ end
16
+
17
+ def say(message)
18
+ $stdout.puts message
19
+ end
20
+
21
+ def write(message)
22
+ $stdout.print message
23
+ end
24
+
25
+ def select(question, options, &block)
26
+ CLI::UI::Prompt.ask(question) do |handler|
27
+ options.each do |option|
28
+ handler.option(option) { |selected| block.call(selected) }
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def setup_completion
36
+ Reline.autocompletion = true
37
+ Reline.completion_proc = ->(target, preposing) { complete(target, preposing) }
38
+ end
39
+
40
+ def complete(target, preposing)
41
+ line = "#{preposing}#{target}"
42
+
43
+ if line.start_with?('/') && !preposing.include?(' ')
44
+ return @commands.select { |c| c.start_with?(line) }
45
+ end
46
+
47
+ case preposing.strip
48
+ when '/mode'
49
+ @modes.select { |m| m.start_with?(target) }
50
+ when '/provider'
51
+ @providers.select { |p| p.start_with?(target) }
52
+ when '/env'
53
+ @env_vars.select { |v| v.start_with?(target) }
54
+ when %r{^/env\s+\w+\s+pass(\s+show)?\s*$}
55
+ subcommands = %w[show ls insert generate edit rm]
56
+ matches = subcommands.select { |c| c.start_with?(target) }
57
+ matches.any? ? matches : complete_pass_entries(target)
58
+ when %r{^/env\s+\w+$}
59
+ complete_commands(target)
60
+ else
61
+ complete_files(target)
62
+ end
63
+ end
64
+
65
+ def complete_commands(target)
66
+ result = Elelem.shell.execute("bash", args: ["-c", "compgen -c #{target}"])
67
+ result["stdout"].lines.map(&:strip).first(20)
68
+ end
69
+
70
+ def complete_files(target)
71
+ result = Elelem.shell.execute("bash", args: ["-c", "compgen -f #{target}"])
72
+ result["stdout"].lines.map(&:strip).first(20)
73
+ end
74
+
75
+ def complete_pass_entries(target)
76
+ store = ENV.fetch("PASSWORD_STORE_DIR", File.expand_path("~/.password-store"))
77
+ result = Elelem.shell.execute("find", args: ["-L", store, "-name", "*.gpg"])
78
+ result["stdout"].lines.map { |l|
79
+ l.strip.sub("#{store}/", "").sub(/\.gpg$/, "")
80
+ }.select { |e| e.start_with?(target) }.first(20)
81
+ end
82
+ end
83
+ end
data/lib/elelem/tool.rb CHANGED
@@ -12,7 +12,9 @@ module Elelem
12
12
 
13
13
  def call(args)
14
14
  unless valid?(args)
15
- return { error: "Invalid args for #{@name}", received: args.keys, expected: @schema.dig(:function, :parameters, :required) }
15
+ actual = args.keys
16
+ expected = @schema.dig(:function, :parameters)
17
+ return { error: "Invalid args for #{@name}.", actual: actual, expected: expected }
16
18
  end
17
19
 
18
20
  @block.call(args)
@@ -2,13 +2,14 @@
2
2
 
3
3
  module Elelem
4
4
  class Toolbox
5
+
5
6
  READ_TOOL = Tool.build("read", "Read complete contents of a file. Requires exact file path.", { path: { type: "string" } }, ["path"]) do |args|
6
7
  path = args["path"]
7
8
  full_path = Pathname.new(path).expand_path
8
9
  full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
9
10
  end
10
11
 
11
- BASH_TOOL = Tool.build("bash", "Run shell commands. For git: bash({\"cmd\": \"git\", \"args\": [\"log\", \"--oneline\"]}). 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
+ 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
13
  Elelem.shell.execute(
13
14
  args["cmd"],
14
15
  args: args["args"] || [],
@@ -36,13 +37,21 @@ module Elelem
36
37
  { bytes_written: full_path.write(args["content"]) }
37
38
  end
38
39
 
40
+ TOOL_ALIASES = {
41
+ "bash" => "exec",
42
+ "execute" => "exec",
43
+ "open" => "read",
44
+ "search" => "grep",
45
+ "sh" => "exec",
46
+ }
47
+
39
48
  attr_reader :tools
40
49
 
41
50
  def initialize
42
51
  @tools_by_name = {}
43
52
  @tools = { read: [], write: [], execute: [] }
44
53
  add_tool(eval_tool(binding), :execute)
45
- add_tool(BASH_TOOL, :execute)
54
+ add_tool(EXEC_TOOL, :execute)
46
55
  add_tool(GREP_TOOL, :read)
47
56
  add_tool(LIST_TOOL, :read)
48
57
  add_tool(PATCH_TOOL, :write)
@@ -64,7 +73,8 @@ module Elelem
64
73
  end
65
74
 
66
75
  def run_tool(name, args)
67
- @tools_by_name[name]&.call(args) || { error: "Unknown tool", name: name, args: args }
76
+ resolved_name = TOOL_ALIASES.fetch(name, name)
77
+ @tools_by_name[resolved_name]&.call(args) || { error: "Unknown tool", name: name, args: args }
68
78
  rescue => error
69
79
  { error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
70
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.4.2"
5
- end
4
+ VERSION = "0.6.0"
5
+ end
data/lib/elelem.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cli/ui"
3
4
  require "erb"
4
5
  require "fileutils"
5
6
  require "json"
@@ -16,6 +17,7 @@ require "timeout"
16
17
  require_relative "elelem/agent"
17
18
  require_relative "elelem/application"
18
19
  require_relative "elelem/conversation"
20
+ require_relative "elelem/terminal"
19
21
  require_relative "elelem/tool"
20
22
  require_relative "elelem/toolbox"
21
23
  require_relative "elelem/version"
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.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -9,175 +9,195 @@ 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: cli-ui
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: erb
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
16
- - - ">="
30
+ - - "~>"
17
31
  - !ruby/object:Gem::Version
18
- version: '0'
32
+ version: '6.0'
19
33
  type: :runtime
20
34
  prerelease: false
21
35
  version_requirements: !ruby/object:Gem::Requirement
22
36
  requirements:
23
- - - ">="
37
+ - - "~>"
24
38
  - !ruby/object:Gem::Version
25
- version: '0'
39
+ version: '6.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: fileutils
28
42
  requirement: !ruby/object:Gem::Requirement
29
43
  requirements:
30
- - - ">="
44
+ - - "~>"
31
45
  - !ruby/object:Gem::Version
32
- version: '0'
46
+ version: '1.0'
33
47
  type: :runtime
34
48
  prerelease: false
35
49
  version_requirements: !ruby/object:Gem::Requirement
36
50
  requirements:
37
- - - ">="
51
+ - - "~>"
38
52
  - !ruby/object:Gem::Version
39
- version: '0'
53
+ version: '1.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: json
42
56
  requirement: !ruby/object:Gem::Requirement
43
57
  requirements:
44
- - - ">="
58
+ - - "~>"
45
59
  - !ruby/object:Gem::Version
46
- version: '0'
60
+ version: '2.0'
47
61
  type: :runtime
48
62
  prerelease: false
49
63
  version_requirements: !ruby/object:Gem::Requirement
50
64
  requirements:
51
- - - ">="
65
+ - - "~>"
52
66
  - !ruby/object:Gem::Version
53
- version: '0'
67
+ version: '2.0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: json-schema
56
70
  requirement: !ruby/object:Gem::Requirement
57
71
  requirements:
58
- - - ">="
72
+ - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '0'
74
+ version: '6.0'
61
75
  type: :runtime
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
- - - ">="
79
+ - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: '0'
81
+ version: '6.0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: logger
70
84
  requirement: !ruby/object:Gem::Requirement
71
85
  requirements:
72
- - - ">="
86
+ - - "~>"
73
87
  - !ruby/object:Gem::Version
74
- version: '0'
88
+ version: '1.0'
75
89
  type: :runtime
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
- - - ">="
93
+ - - "~>"
80
94
  - !ruby/object:Gem::Version
81
- version: '0'
95
+ version: '1.0'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: net-llm
84
98
  requirement: !ruby/object:Gem::Requirement
85
99
  requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.5'
86
103
  - - ">="
87
104
  - !ruby/object:Gem::Version
88
- version: '0'
105
+ version: 0.5.0
89
106
  type: :runtime
90
107
  prerelease: false
91
108
  version_requirements: !ruby/object:Gem::Requirement
92
109
  requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '0.5'
93
113
  - - ">="
94
114
  - !ruby/object:Gem::Version
95
- version: '0'
115
+ version: 0.5.0
96
116
  - !ruby/object:Gem::Dependency
97
117
  name: open3
98
118
  requirement: !ruby/object:Gem::Requirement
99
119
  requirements:
100
- - - ">="
120
+ - - "~>"
101
121
  - !ruby/object:Gem::Version
102
- version: '0'
122
+ version: '0.1'
103
123
  type: :runtime
104
124
  prerelease: false
105
125
  version_requirements: !ruby/object:Gem::Requirement
106
126
  requirements:
107
- - - ">="
127
+ - - "~>"
108
128
  - !ruby/object:Gem::Version
109
- version: '0'
129
+ version: '0.1'
110
130
  - !ruby/object:Gem::Dependency
111
131
  name: pathname
112
132
  requirement: !ruby/object:Gem::Requirement
113
133
  requirements:
114
- - - ">="
134
+ - - "~>"
115
135
  - !ruby/object:Gem::Version
116
- version: '0'
136
+ version: '0.1'
117
137
  type: :runtime
118
138
  prerelease: false
119
139
  version_requirements: !ruby/object:Gem::Requirement
120
140
  requirements:
121
- - - ">="
141
+ - - "~>"
122
142
  - !ruby/object:Gem::Version
123
- version: '0'
143
+ version: '0.1'
124
144
  - !ruby/object:Gem::Dependency
125
145
  name: reline
126
146
  requirement: !ruby/object:Gem::Requirement
127
147
  requirements:
128
- - - ">="
148
+ - - "~>"
129
149
  - !ruby/object:Gem::Version
130
- version: '0'
150
+ version: '0.6'
131
151
  type: :runtime
132
152
  prerelease: false
133
153
  version_requirements: !ruby/object:Gem::Requirement
134
154
  requirements:
135
- - - ">="
155
+ - - "~>"
136
156
  - !ruby/object:Gem::Version
137
- version: '0'
157
+ version: '0.6'
138
158
  - !ruby/object:Gem::Dependency
139
159
  name: set
140
160
  requirement: !ruby/object:Gem::Requirement
141
161
  requirements:
142
- - - ">="
162
+ - - "~>"
143
163
  - !ruby/object:Gem::Version
144
- version: '0'
164
+ version: '1.0'
145
165
  type: :runtime
146
166
  prerelease: false
147
167
  version_requirements: !ruby/object:Gem::Requirement
148
168
  requirements:
149
- - - ">="
169
+ - - "~>"
150
170
  - !ruby/object:Gem::Version
151
- version: '0'
171
+ version: '1.0'
152
172
  - !ruby/object:Gem::Dependency
153
173
  name: thor
154
174
  requirement: !ruby/object:Gem::Requirement
155
175
  requirements:
156
- - - ">="
176
+ - - "~>"
157
177
  - !ruby/object:Gem::Version
158
- version: '0'
178
+ version: '1.0'
159
179
  type: :runtime
160
180
  prerelease: false
161
181
  version_requirements: !ruby/object:Gem::Requirement
162
182
  requirements:
163
- - - ">="
183
+ - - "~>"
164
184
  - !ruby/object:Gem::Version
165
- version: '0'
185
+ version: '1.0'
166
186
  - !ruby/object:Gem::Dependency
167
187
  name: timeout
168
188
  requirement: !ruby/object:Gem::Requirement
169
189
  requirements:
170
- - - ">="
190
+ - - "~>"
171
191
  - !ruby/object:Gem::Version
172
- version: '0'
192
+ version: '0.1'
173
193
  type: :runtime
174
194
  prerelease: false
175
195
  version_requirements: !ruby/object:Gem::Requirement
176
196
  requirements:
177
- - - ">="
197
+ - - "~>"
178
198
  - !ruby/object:Gem::Version
179
- version: '0'
180
- description: A REPL for Ollama.
199
+ version: '0.1'
200
+ description: A minimal coding agent supporting Ollama, Anthropic, OpenAI, and VertexAI.
181
201
  email:
182
202
  - mo@mokhan.ca
183
203
  executables:
@@ -195,17 +215,18 @@ files:
195
215
  - lib/elelem/application.rb
196
216
  - lib/elelem/conversation.rb
197
217
  - lib/elelem/system_prompt.erb
218
+ - lib/elelem/terminal.rb
198
219
  - lib/elelem/tool.rb
199
220
  - lib/elelem/toolbox.rb
200
221
  - lib/elelem/version.rb
201
- homepage: https://github.com/xlgmokha/elelem
222
+ homepage: https://src.mokhan.ca/xlgmokha/elelem
202
223
  licenses:
203
224
  - MIT
204
225
  metadata:
205
226
  allowed_push_host: https://rubygems.org
206
- homepage_uri: https://github.com/xlgmokha/elelem
207
- source_code_uri: https://github.com/xlgmokha/elelem
208
- changelog_uri: https://github.com/xlgmokha/elelem/blob/main/CHANGELOG.md
227
+ homepage_uri: https://src.mokhan.ca/xlgmokha/elelem
228
+ source_code_uri: https://src.mokhan.ca/xlgmokha/elelem
229
+ changelog_uri: https://src.mokhan.ca/xlgmokha/elelem/blob/main/CHANGELOG.md.html
209
230
  rdoc_options: []
210
231
  require_paths:
211
232
  - lib
@@ -220,7 +241,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
241
  - !ruby/object:Gem::Version
221
242
  version: 3.3.11
222
243
  requirements: []
223
- rubygems_version: 3.7.2
244
+ rubygems_version: 3.6.9
224
245
  specification_version: 4
225
- summary: A REPL for Ollama.
246
+ summary: A minimal coding agent for LLMs.
226
247
  test_files: []