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 +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +25 -10
- data/lib/elelem/agent.rb +215 -50
- data/lib/elelem/application.rb +12 -13
- data/lib/elelem/system_prompt.erb +9 -12
- data/lib/elelem/terminal.rb +83 -0
- data/lib/elelem/tool.rb +3 -1
- data/lib/elelem/toolbox.rb +13 -3
- data/lib/elelem/version.rb +2 -2
- data/lib/elelem.rb +2 -0
- metadata +75 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a97a3be43a2528518770e2881fef86138b19ce9394601de922822f9748fe9c8
|
|
4
|
+
data.tar.gz: e87dc58d17f701d9b2fa0a15b05b7f2fbad24a7f71b5836a47f01fe6b216df55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
* `--
|
|
75
|
-
* `--model` –
|
|
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
|
|
80
|
+
# Default (Ollama)
|
|
82
81
|
elelem chat
|
|
83
82
|
|
|
84
|
-
#
|
|
85
|
-
elelem chat --
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
attr_reader :conversation, :client, :toolbox, :provider, :terminal
|
|
13
|
+
|
|
14
|
+
def initialize(provider, model, toolbox, terminal: nil)
|
|
8
15
|
@conversation = Conversation.new
|
|
9
|
-
@
|
|
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
|
|
31
|
+
input = terminal.ask("User> ")
|
|
18
32
|
break if input.nil?
|
|
19
33
|
if input.start_with?("/")
|
|
20
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/elelem/application.rb
CHANGED
|
@@ -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 :
|
|
7
|
-
aliases: "
|
|
8
|
+
method_option :provider,
|
|
9
|
+
aliases: "-p",
|
|
8
10
|
type: :string,
|
|
9
|
-
desc: "
|
|
10
|
-
default: ENV.fetch("
|
|
11
|
+
desc: "LLM provider (#{PROVIDERS.join(', ')})",
|
|
12
|
+
default: ENV.fetch("ELELEM_PROVIDER", "ollama")
|
|
11
13
|
method_option :model,
|
|
12
|
-
aliases: "
|
|
14
|
+
aliases: "-m",
|
|
13
15
|
type: :string,
|
|
14
|
-
desc: "
|
|
15
|
-
default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
|
|
16
|
+
desc: "Model name (uses provider default if not specified)"
|
|
16
17
|
def chat(*)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/elelem/toolbox.rb
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
data/lib/elelem/version.rb
CHANGED
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
|
+
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:
|
|
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:
|
|
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
|
|
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://
|
|
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://
|
|
207
|
-
source_code_uri: https://
|
|
208
|
-
changelog_uri: https://
|
|
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.
|
|
244
|
+
rubygems_version: 3.6.9
|
|
224
245
|
specification_version: 4
|
|
225
|
-
summary: A
|
|
246
|
+
summary: A minimal coding agent for LLMs.
|
|
226
247
|
test_files: []
|