elelem 0.9.1 → 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 +34 -0
- data/README.md +93 -16
- data/Rakefile +0 -11
- data/exe/elelem +2 -79
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e95bbe823f4cf39a8a801bd32240caa47dcc3bdea6ce803f7c744f5f192f74c
|
|
4
|
+
data.tar.gz: 685b6b52ff14408979bd61a0ea42e87d24602f398392ac845fcc893eceabccae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc605e03c30962a7f75d0846cde1bfb46d41c73ffbd07ee49f933fe51076ae08df7f9fb86b933d16864fd5aad7de9a29297d3b7da01337a9aa62d80aa9fa5940
|
|
7
|
+
data.tar.gz: 5d2580db0a5e607c360bb1f691d2eabaec7a37af7ca4f705950fed4048a567be46077c04e5a289163473f679ec95b76b8b04e5c56d84a3bb4eb42be6cc1b4da6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## [0.10.0] - 2026-01-27
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **Async MCP loading** for faster startup - tools load in background thread
|
|
5
|
+
- **HTTP MCP servers** with SSE support and session management
|
|
6
|
+
- **OAuth authentication** for MCP servers with PKCE, automatic token refresh
|
|
7
|
+
- **Global hooks** - `toolbox.before`/`toolbox.after` without tool name applies to all tools
|
|
8
|
+
- **`/context` improvements**: `/context <n>` to view entry, `/context json` for full dump
|
|
9
|
+
- **ast-grep (`sg`) support** for building repo maps - faster and more accurate than ctags
|
|
10
|
+
- **New tools**: `glob`, `grep`, `list`, `git`, `task`, `/tools` command
|
|
11
|
+
- **Permissions system** (`lib/elelem/permissions.rb`) for tool access control
|
|
12
|
+
- **OpenAI reasoning mode** - enables `Reasoning: high` for o-series models
|
|
13
|
+
- **Test coverage** for OAuth, token storage, HTTP MCP, SSE parsing, global hooks
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **BREAKING: Plugin API** - plugins now receive `agent` instead of `toolbox`
|
|
17
|
+
- Old: `Elelem::Plugins.register(:name) { |toolbox| toolbox.add(...) }`
|
|
18
|
+
- New: `Elelem::Plugins.register(:name) { |agent| agent.toolbox.add(...) }`
|
|
19
|
+
- Plugins can now access `agent.terminal`, `agent.commands`, `agent.conversation`
|
|
20
|
+
- Extracted `Conversation` class from `Agent` for better separation of concerns
|
|
21
|
+
- Extracted `Commands` class for slash command handling
|
|
22
|
+
- Refactored LLM fetch interface to emit separate events for thinking/content/tool_calls
|
|
23
|
+
- Simplified system prompt with inline ERB template
|
|
24
|
+
- Renamed confirm plugin to `zz_confirm` to ensure it loads last
|
|
25
|
+
- MCP logs now write to `~/.elelem/mcp.log` instead of working directory
|
|
26
|
+
- Tool schema now frozen to prevent mutation
|
|
27
|
+
- Uses `Open3.capture2` instead of backticks for thread safety
|
|
28
|
+
- Improved ANSI escape sequence stripping in `/shell` transcripts
|
|
29
|
+
|
|
30
|
+
## [0.9.2] - 2026-01-22
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Suppress pathname gem constant redefinition warnings on Ruby 4.0
|
|
34
|
+
|
|
1
35
|
## [0.9.1] - 2026-01-22
|
|
2
36
|
|
|
3
37
|
### Fixed
|
data/README.md
CHANGED
|
@@ -31,17 +31,18 @@ Elelem relies on several external tools. Install the ones you need:
|
|
|
31
31
|
|
|
32
32
|
| Tool | Purpose | Install |
|
|
33
33
|
|------|---------|---------|
|
|
34
|
-
| [
|
|
35
|
-
| [glow](https://github.com/charmbracelet/glow) | Markdown rendering | `brew install glow` / `go install github.com/charmbracelet/glow@latest` |
|
|
34
|
+
| [ast-grep](https://ast-grep.github.io/) | Structural search (`sg`) | `brew install ast-grep` / `cargo install ast-grep` |
|
|
36
35
|
| [ctags](https://ctags.io/) | Repo map generation | `brew install universal-ctags` / `apt install universal-ctags` |
|
|
37
|
-
| [ripgrep](https://github.com/BurntSushi/ripgrep) | Text search (`rg`) | `brew install ripgrep` / `apt install ripgrep` |
|
|
38
36
|
| [fd](https://github.com/sharkdp/fd) | File discovery | `brew install fd` / `apt install fd-find` |
|
|
39
|
-
| [
|
|
40
|
-
| [
|
|
37
|
+
| [git](https://git-scm.com/) | Version control | `brew install git` / `apt install git` |
|
|
38
|
+
| [glow](https://github.com/charmbracelet/glow) | Markdown rendering | `brew install glow` / `go install github.com/charmbracelet/glow@latest` |
|
|
39
|
+
| [jq](https://jqlang.github.io/jq/) | JSON processing | `brew install jq` / `apt install jq` |
|
|
40
|
+
| [ollama](https://ollama.ai/) | Default LLM provider | https://ollama.ai/download |
|
|
41
|
+
| [ripgrep](https://github.com/BurntSushi/ripgrep) | Text search (`rg`) | `brew install ripgrep` / `apt install ripgrep` |
|
|
41
42
|
|
|
42
43
|
**Required:** Git, Ollama (or another LLM provider)
|
|
43
44
|
|
|
44
|
-
**Recommended:** glow, ctags, ripgrep, fd
|
|
45
|
+
**Recommended:** glow, jq, ctags, ripgrep, fd
|
|
45
46
|
|
|
46
47
|
**Optional:** ast-grep (for structural code search)
|
|
47
48
|
|
|
@@ -128,18 +129,94 @@ Each provider reads its configuration from environment variables:
|
|
|
128
129
|
* **Conversation History** – persists across turns; can be cleared.
|
|
129
130
|
* **Context Dump** – `/context` shows the current conversation state.
|
|
130
131
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
## Tools
|
|
133
|
+
|
|
134
|
+
Built-in tools available to the LLM:
|
|
135
|
+
|
|
136
|
+
| Tool | Purpose | Parameters |
|
|
137
|
+
| --------- | -------------------------- | ------------------------- |
|
|
138
|
+
| `read` | Read file contents | `path` |
|
|
139
|
+
| `write` | Write file | `path`, `content` |
|
|
140
|
+
| `edit` | Replace text in file | `path`, `old`, `new` |
|
|
141
|
+
| `execute` | Run shell command | `command` |
|
|
142
|
+
| `eval` | Execute Ruby code | `ruby` |
|
|
143
|
+
| `glob` | Find files by pattern | `pattern`, `path` |
|
|
144
|
+
| `grep` | Search file contents | `pattern`, `path`, `glob` |
|
|
145
|
+
| `list` | List directory | `path`, `recursive` |
|
|
146
|
+
| `git` | Run git command | `command`, `args` |
|
|
147
|
+
| `task` | Delegate to sub-agent | `prompt` |
|
|
148
|
+
| `verify` | Check syntax and run tests | `path` |
|
|
149
|
+
|
|
150
|
+
Aliases: `bash`, `sh`, `exec` → `execute`; `open` → `read`; `ls` → `list`
|
|
151
|
+
|
|
152
|
+
## Plugins
|
|
153
|
+
|
|
154
|
+
Plugins extend elelem with custom tools and commands. They are loaded from:
|
|
155
|
+
- `lib/elelem/plugins/` (built-in)
|
|
156
|
+
- `~/.elelem/plugins/` (user global)
|
|
157
|
+
- `.elelem/plugins/` (project local)
|
|
158
|
+
|
|
159
|
+
### Writing a Plugin
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# ~/.elelem/plugins/hello.rb
|
|
163
|
+
Elelem::Plugins.register(:hello) do |agent|
|
|
164
|
+
# Add a tool
|
|
165
|
+
agent.toolbox.add("hello",
|
|
166
|
+
description: "Say hello",
|
|
167
|
+
params: { name: { type: "string" } },
|
|
168
|
+
required: ["name"]
|
|
169
|
+
) do |args|
|
|
170
|
+
{ message: "Hello, #{args["name"]}!" }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Add a command
|
|
174
|
+
agent.commands.register("greet", description: "Greet the user") do
|
|
175
|
+
agent.terminal.say "Hello!"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Add hooks
|
|
179
|
+
agent.toolbox.before("execute") { |args| puts "Running: #{args["command"]}" }
|
|
180
|
+
agent.toolbox.after("execute") { |args, result| puts "Exit: #{result[:exit_status]}" }
|
|
181
|
+
|
|
182
|
+
# Global hook (runs for all tools)
|
|
183
|
+
agent.toolbox.before { |args, tool_name:| puts "Calling #{tool_name}" }
|
|
184
|
+
end
|
|
185
|
+
```
|
|
135
186
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
### Plugin API
|
|
188
|
+
|
|
189
|
+
Plugins receive an `agent` object with access to:
|
|
190
|
+
- `agent.toolbox` - add tools, register hooks
|
|
191
|
+
- `agent.terminal` - output to the user (`say`, `ask`, `markdown`)
|
|
192
|
+
- `agent.commands` - register slash commands
|
|
193
|
+
- `agent.conversation` - access message history
|
|
194
|
+
- `agent.client` - the LLM client
|
|
195
|
+
- `agent.fork(system_prompt:)` - create a sub-agent
|
|
196
|
+
|
|
197
|
+
## MCP Configuration
|
|
198
|
+
|
|
199
|
+
Configure MCP servers in `~/.elelem/mcp.json` or `.elelem/mcp.json`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"mcpServers": {
|
|
204
|
+
"gitlab": {
|
|
205
|
+
"command": "npx",
|
|
206
|
+
"args": ["-y", "@anthropics/gitlab-mcp"],
|
|
207
|
+
"env": {
|
|
208
|
+
"GITLAB_TOKEN": "${GITLAB_TOKEN}"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"remote": {
|
|
212
|
+
"type": "http",
|
|
213
|
+
"url": "https://mcp.example.com/sse"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
141
218
|
|
|
142
|
-
|
|
219
|
+
HTTP servers support OAuth authentication automatically.
|
|
143
220
|
|
|
144
221
|
## Known Limitations
|
|
145
222
|
|
data/Rakefile
CHANGED
|
@@ -5,15 +5,4 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
task :files do
|
|
9
|
-
IO.popen(%w[git ls-files], chdir: __dir__, err: IO::NULL) do |ls|
|
|
10
|
-
ls.readlines.each do |f|
|
|
11
|
-
next if f.start_with?(*%w[bin/ spec/ pkg/ .git .rspec Gemfile Rakefile])
|
|
12
|
-
next if f.strip.end_with?(*%w[.toml .txt .md])
|
|
13
|
-
|
|
14
|
-
puts f
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
8
|
task default: %i[spec]
|
data/exe/elelem
CHANGED
|
@@ -1,85 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
1
|
+
#!/usr/bin/env -S ruby -W0
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "elelem"
|
|
5
|
-
require "optparse"
|
|
6
5
|
|
|
7
6
|
Signal.trap("INT") { exit 1 }
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
MODELS = {
|
|
11
|
-
"ollama" => "gpt-oss:latest",
|
|
12
|
-
"anthropic" => "claude-opus-4-5-20250514",
|
|
13
|
-
"vertex" => "claude-opus-4-5@20251101",
|
|
14
|
-
"openai" => "gpt-4o"
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
PROVIDERS = {
|
|
18
|
-
"ollama" => ->(model) { Elelem::Net::Ollama.new(model: model, host: ENV.fetch("OLLAMA_HOST", "localhost:11434")) },
|
|
19
|
-
"anthropic" => ->(model) { Elelem::Net::Claude.anthropic(model: model, api_key: ENV.fetch("ANTHROPIC_API_KEY")) },
|
|
20
|
-
"vertex" => ->(model) { Elelem::Net::Claude.vertex(model: model, project: ENV.fetch("GOOGLE_CLOUD_PROJECT"), region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")) },
|
|
21
|
-
"openai" => ->(model) { Elelem::Net::OpenAI.new(model: model, api_key: ENV.fetch("OPENAI_API_KEY")) }
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
def initialize(args)
|
|
25
|
-
@provider = "ollama"
|
|
26
|
-
@model = nil
|
|
27
|
-
@args = parse(args)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def run
|
|
31
|
-
command = @args.shift || "chat"
|
|
32
|
-
send(command.tr("-", "_"))
|
|
33
|
-
rescue NoMethodError
|
|
34
|
-
abort "Unknown command: #{command}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def parse(args)
|
|
40
|
-
@parser = OptionParser.new do |o|
|
|
41
|
-
o.banner = "Usage: elelem [command] [options] [args]"
|
|
42
|
-
o.separator "\nCommands:"
|
|
43
|
-
o.separator " chat Interactive REPL (default)"
|
|
44
|
-
o.separator " ask <prompt> One-shot query (reads stdin if piped)"
|
|
45
|
-
o.separator " files Output files as XML (no options)"
|
|
46
|
-
o.separator " help Show this help"
|
|
47
|
-
o.separator "\nOptions:"
|
|
48
|
-
o.on("-p", "--provider NAME", "ollama, anthropic, vertex, openai") { |p| @provider = p }
|
|
49
|
-
o.on("-m", "--model NAME", "Override default model") { |m| @model = m }
|
|
50
|
-
o.on("-h", "--help") { puts o; exit }
|
|
51
|
-
end
|
|
52
|
-
@parser.parse!(args)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def help
|
|
56
|
-
puts @parser
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def client
|
|
60
|
-
model = @model || MODELS.fetch(@provider)
|
|
61
|
-
PROVIDERS.fetch(@provider).call(model)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def chat = Elelem.start(client)
|
|
65
|
-
|
|
66
|
-
def ask
|
|
67
|
-
abort "Usage: elelem ask <prompt>" if @args.empty?
|
|
68
|
-
prompt = @args.join(" ")
|
|
69
|
-
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
|
|
70
|
-
Elelem::Terminal.new.markdown Elelem.ask(client, prompt)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def files
|
|
74
|
-
files = $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
|
|
75
|
-
puts "<documents>"
|
|
76
|
-
files.each_with_index do |line, i|
|
|
77
|
-
path = line.strip
|
|
78
|
-
next if path.empty? || !File.file?(path)
|
|
79
|
-
puts %Q{<document index="#{i + 1}"><source>#{path}</source><content><![CDATA[#{File.read(path)}]]></content></document>}
|
|
80
|
-
end
|
|
81
|
-
puts "</documents>"
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
App.new(ARGV).run
|
|
8
|
+
Elelem::CLI.new(ARGV).run
|
data/lib/elelem/agent.rb
CHANGED
|
@@ -2,40 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Agent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
INIT_PROMPT = <<~PROMPT
|
|
8
|
-
AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
|
|
5
|
+
attr_reader :conversation, :client, :toolbox, :terminal, :commands
|
|
6
|
+
attr_writer :terminal, :toolbox, :commands
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
A file providing context and instructions for AI coding agents.
|
|
12
|
-
|
|
13
|
-
## Recommended Sections
|
|
14
|
-
- Commands: build, test, lint commands
|
|
15
|
-
- Code Style: conventions, patterns
|
|
16
|
-
- Architecture: key components and flow
|
|
17
|
-
- Testing: how to run tests
|
|
18
|
-
|
|
19
|
-
## Process
|
|
20
|
-
1. Read README.md if present
|
|
21
|
-
2. Identify language (Gemfile, package.json, go.mod)
|
|
22
|
-
3. Find test scripts (bin/test, npm test)
|
|
23
|
-
4. Check linter configs
|
|
24
|
-
5. Write concise AGENTS.md
|
|
25
|
-
|
|
26
|
-
Keep it minimal. No fluff.
|
|
27
|
-
PROMPT
|
|
28
|
-
|
|
29
|
-
attr_reader :history, :client, :toolbox, :terminal
|
|
30
|
-
|
|
31
|
-
def initialize(client, toolbox, terminal: nil, history: nil, system_prompt: nil)
|
|
8
|
+
def initialize(client, toolbox: Toolbox.new, terminal: nil, system_prompt: nil, commands: nil)
|
|
32
9
|
@client = client
|
|
33
10
|
@toolbox = toolbox
|
|
34
|
-
@
|
|
35
|
-
@
|
|
11
|
+
@commands = commands || Commands.new
|
|
12
|
+
@terminal = terminal
|
|
13
|
+
@conversation = Conversation.new
|
|
36
14
|
@system_prompt = system_prompt
|
|
37
|
-
@memory = nil
|
|
38
|
-
register_task_tool
|
|
39
15
|
end
|
|
40
16
|
|
|
41
17
|
def repl
|
|
@@ -49,27 +25,21 @@ module Elelem
|
|
|
49
25
|
end
|
|
50
26
|
|
|
51
27
|
def command(input)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
when "/context"
|
|
64
|
-
terminal.say JSON.pretty_generate(combined_history)
|
|
65
|
-
else
|
|
66
|
-
terminal.say COMMANDS.join(" ")
|
|
67
|
-
end
|
|
28
|
+
parts = input.delete_prefix("/").split(" ", 2)
|
|
29
|
+
name, args = parts[0], parts[1]
|
|
30
|
+
commands.run(name, args) || terminal.say(commands.names.join(" "))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def context
|
|
34
|
+
@conversation.to_a(system_prompt: system_prompt)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fork(system_prompt:)
|
|
38
|
+
Agent.new(client, toolbox: toolbox, terminal: terminal, system_prompt: system_prompt)
|
|
68
39
|
end
|
|
69
40
|
|
|
70
41
|
def turn(input)
|
|
71
|
-
|
|
72
|
-
history << { role: "user", content: input }
|
|
42
|
+
@conversation.add(role: "user", content: input)
|
|
73
43
|
ctx = []
|
|
74
44
|
content = nil
|
|
75
45
|
|
|
@@ -85,7 +55,7 @@ module Elelem
|
|
|
85
55
|
end
|
|
86
56
|
end
|
|
87
57
|
|
|
88
|
-
|
|
58
|
+
@conversation.add(role: "assistant", content: content)
|
|
89
59
|
content
|
|
90
60
|
end
|
|
91
61
|
|
|
@@ -97,90 +67,29 @@ module Elelem
|
|
|
97
67
|
toolbox.run(name.to_s, args)
|
|
98
68
|
end
|
|
99
69
|
|
|
100
|
-
def register_task_tool
|
|
101
|
-
@toolbox.add("task",
|
|
102
|
-
description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
|
|
103
|
-
params: { prompt: { type: "string" } },
|
|
104
|
-
required: ["prompt"]
|
|
105
|
-
) do |a|
|
|
106
|
-
sub = Agent.new(client, toolbox, terminal: terminal,
|
|
107
|
-
system_prompt: "Research agent. Search, analyze, report. Be concise.")
|
|
108
|
-
sub.turn(a["prompt"])
|
|
109
|
-
{ result: sub.history.last[:content] }
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def init_agents_md
|
|
114
|
-
sub = Agent.new(client, toolbox, terminal: terminal, system_prompt: INIT_PROMPT)
|
|
115
|
-
sub.turn("Generate AGENTS.md for this project")
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def reload_source!
|
|
119
|
-
lib_dir = File.expand_path("..", __dir__)
|
|
120
|
-
original_verbose, $VERBOSE = $VERBOSE, nil
|
|
121
|
-
Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
|
|
122
|
-
$VERBOSE = original_verbose
|
|
123
|
-
@toolbox = Toolbox.new
|
|
124
|
-
Plugins.reload!(@toolbox)
|
|
125
|
-
register_task_tool
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def start_shell
|
|
129
|
-
Tempfile.create do |file|
|
|
130
|
-
system("script", "-q", file.path, chdir: Dir.pwd)
|
|
131
|
-
strip_ansi(File.read(file.path))
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def strip_ansi(text)
|
|
136
|
-
text.gsub(/^Script started.*?\n/, "")
|
|
137
|
-
.gsub(/\nScript done.*$/, "")
|
|
138
|
-
.gsub(/\e\[[0-9;]*[a-zA-Z]/, "")
|
|
139
|
-
.gsub(/\e\[\?[0-9]+[hl]/, "")
|
|
140
|
-
.gsub(/[\b]/, "")
|
|
141
|
-
.gsub(/\r/, "")
|
|
142
|
-
end
|
|
143
|
-
|
|
144
70
|
def fetch_response(ctx)
|
|
145
|
-
content =
|
|
146
|
-
tool_calls =
|
|
147
|
-
|
|
148
|
-
|
|
71
|
+
content = String.new
|
|
72
|
+
tool_calls = []
|
|
73
|
+
|
|
74
|
+
client.fetch(@conversation.to_a(system_prompt: system_prompt) + ctx, toolbox.to_a) do |event|
|
|
75
|
+
case event[:type]
|
|
76
|
+
when "saying"
|
|
77
|
+
content << event[:text].to_s
|
|
78
|
+
when "thinking"
|
|
79
|
+
terminal.print(terminal.think(event[:text]))
|
|
80
|
+
when "tool_call"
|
|
81
|
+
tool_calls << { id: event[:id], name: event[:name], arguments: event[:arguments] }
|
|
82
|
+
end
|
|
149
83
|
end
|
|
84
|
+
|
|
150
85
|
[content, tool_calls]
|
|
151
86
|
rescue => e
|
|
152
87
|
terminal.say "\n ✗ #{e.message}"
|
|
153
88
|
["Error: #{e.message} #{e.backtrace.join("\n")}", []]
|
|
154
89
|
end
|
|
155
90
|
|
|
156
|
-
def combined_history
|
|
157
|
-
[{ role: "system", content: system_prompt }] + history
|
|
158
|
-
end
|
|
159
|
-
|
|
160
91
|
def system_prompt
|
|
161
|
-
@system_prompt || SystemPrompt.new
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def compact_if_needed
|
|
165
|
-
return if history.length <= MAX_CONTEXT_MESSAGES
|
|
166
|
-
|
|
167
|
-
terminal.say " → compacting context"
|
|
168
|
-
keep = MAX_CONTEXT_MESSAGES / 2
|
|
169
|
-
old = history.first(history.length - keep)
|
|
170
|
-
|
|
171
|
-
to_summarize = @memory ? [{ role: "memory", content: @memory }, *old] : old
|
|
172
|
-
@memory = summarize(to_summarize)
|
|
173
|
-
@history = history.last(keep)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def summarize(messages)
|
|
177
|
-
text = messages.map { |message| { role: message[:role], content: message[:content] } }.to_json
|
|
178
|
-
|
|
179
|
-
String.new.tap do |buffer|
|
|
180
|
-
client.fetch([{ role: "user", content: "Summarize key facts:\n#{text}" }], []) do |d|
|
|
181
|
-
buffer << d[:content].to_s
|
|
182
|
-
end
|
|
183
|
-
end
|
|
92
|
+
@system_prompt || SystemPrompt.new.render
|
|
184
93
|
end
|
|
185
94
|
end
|
|
186
95
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Commands
|
|
5
|
+
def initialize
|
|
6
|
+
@registry = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def register(name, description: "", &handler)
|
|
10
|
+
@registry[name] = { description: description, handler: handler }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(name, args = nil)
|
|
14
|
+
entry = @registry[name]
|
|
15
|
+
return false unless entry
|
|
16
|
+
|
|
17
|
+
entry[:handler].arity == 0 ? entry[:handler].call : entry[:handler].call(args)
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def names
|
|
22
|
+
@registry.keys.map { |name| "/#{name}" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each
|
|
26
|
+
@registry.each { |name, entry| yield "/#{name}", entry[:description] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def include?(name)
|
|
30
|
+
@registry.key?(name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Conversation
|
|
5
|
+
ROLES = %w[user assistant tool].freeze
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@messages = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(role:, content:, **extra)
|
|
12
|
+
raise ArgumentError, "invalid role: #{role}" unless ROLES.include?(role)
|
|
13
|
+
@messages << { role: role, content: content, **extra }.compact
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def last = @messages.last
|
|
17
|
+
def length = @messages.length
|
|
18
|
+
def clear! = @messages.clear
|
|
19
|
+
|
|
20
|
+
def to_a(system_prompt: nil)
|
|
21
|
+
base = system_prompt ? [{ role: "system", content: system_prompt }] : []
|
|
22
|
+
base + @messages
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|