elelem 0.10.0 → 0.12.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/README.md +8 -195
- data/exe/elelem +7 -1
- data/lib/elelem/agent.rb +68 -62
- data/lib/elelem/cli.rb +116 -0
- data/lib/elelem/commands.rb +26 -10
- data/lib/elelem/config.rb +17 -0
- data/lib/elelem/conversation.rb +1 -0
- data/lib/elelem/input.rb +51 -0
- data/lib/elelem/null_input.rb +8 -0
- data/lib/elelem/null_output.rb +7 -0
- data/lib/elelem/null_tool.rb +18 -0
- data/lib/elelem/output.rb +55 -0
- data/lib/elelem/plugins.rb +20 -27
- data/lib/elelem/prompts/default.erb +3 -0
- data/lib/elelem/registration.rb +37 -0
- data/lib/elelem/registry.rb +68 -0
- data/lib/elelem/repl.rb +34 -0
- data/lib/elelem/result.rb +27 -0
- data/lib/elelem/schema.rb +20 -0
- data/lib/elelem/session.rb +17 -0
- data/lib/elelem/slash_command.rb +18 -0
- data/lib/elelem/stub_provider.rb +15 -0
- data/lib/elelem/system_prompt.rb +12 -135
- data/lib/elelem/tool.rb +7 -12
- data/lib/elelem/toolbox.rb +40 -31
- data/lib/elelem/transcript.rb +26 -0
- data/lib/elelem/version.rb +2 -2
- data/lib/elelem.rb +28 -113
- metadata +48 -155
- data/CHANGELOG.md +0 -309
- data/lib/elelem/mcp/oauth.rb +0 -217
- data/lib/elelem/mcp/token_storage.rb +0 -60
- data/lib/elelem/mcp.rb +0 -243
- data/lib/elelem/net/claude.rb +0 -202
- data/lib/elelem/net/ollama.rb +0 -81
- data/lib/elelem/net/openai.rb +0 -88
- data/lib/elelem/net.rb +0 -13
- data/lib/elelem/permissions.rb +0 -45
- data/lib/elelem/plugins/builtins.rb +0 -96
- data/lib/elelem/plugins/edit.rb +0 -15
- data/lib/elelem/plugins/eval.rb +0 -20
- data/lib/elelem/plugins/execute.rb +0 -18
- data/lib/elelem/plugins/git.rb +0 -20
- data/lib/elelem/plugins/glob.rb +0 -13
- data/lib/elelem/plugins/grep.rb +0 -21
- data/lib/elelem/plugins/list.rb +0 -14
- data/lib/elelem/plugins/mcp.rb +0 -20
- data/lib/elelem/plugins/permissions.json +0 -6
- data/lib/elelem/plugins/read.rb +0 -21
- data/lib/elelem/plugins/task.rb +0 -14
- data/lib/elelem/plugins/tools.rb +0 -13
- data/lib/elelem/plugins/verify.rb +0 -47
- data/lib/elelem/plugins/write.rb +0 -34
- data/lib/elelem/plugins/zz_confirm.rb +0 -9
- data/lib/elelem/terminal.rb +0 -103
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class NullTool
|
|
5
|
+
def initialize(name, available: [])
|
|
6
|
+
@name = name
|
|
7
|
+
@available = available
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
"#{@name}?"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate(_args)
|
|
15
|
+
["unknown tool: #{@name.inspect}. Available: #{@available.join(", ")}."]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Output
|
|
5
|
+
RENDERERS = {
|
|
6
|
+
error: ->(text) { "error: #{text}" },
|
|
7
|
+
markdown: ->(text) { "```\n#{text}\n```\n" }
|
|
8
|
+
}.freeze
|
|
9
|
+
private_constant :RENDERERS
|
|
10
|
+
|
|
11
|
+
def initialize(stream: $stdout, **)
|
|
12
|
+
@stream = stream
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def say(text, as: nil)
|
|
16
|
+
return if blank?(text)
|
|
17
|
+
|
|
18
|
+
write "#{render(text, as)}\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def print(text)
|
|
22
|
+
return if blank?(text)
|
|
23
|
+
|
|
24
|
+
write text
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def thinking(text)
|
|
28
|
+
print("\e[2;3m#{text}\e[0m") unless blank?(text)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def doing(name, args, state: "+")
|
|
32
|
+
say "#{state} \e[36m#{name}\e[0m(#{args})"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def waiting; end
|
|
36
|
+
|
|
37
|
+
def display_file(path, fallback: nil)
|
|
38
|
+
say(fallback || path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def render(text, as)
|
|
44
|
+
RENDERERS[as]&.call(text) || text
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def write(text)
|
|
48
|
+
@stream.print text
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def blank?(text)
|
|
52
|
+
text.nil? || text.to_s.strip.empty?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/elelem/plugins.rb
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
|
-
|
|
4
|
+
class Plugins
|
|
5
5
|
LOAD_PATHS = [
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
".elelem/plugins"
|
|
6
|
+
"~/.agents/plugins",
|
|
7
|
+
".agents/plugins"
|
|
9
8
|
].freeze
|
|
10
9
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def initialize(load_paths: LOAD_PATHS)
|
|
11
|
+
@loaded = false
|
|
12
|
+
@load_paths = load_paths
|
|
14
13
|
end
|
|
15
14
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
load_plugins
|
|
19
|
-
registry.each_value { |plugin| plugin.call(agent) }
|
|
20
|
-
end
|
|
15
|
+
def load!(force: false)
|
|
16
|
+
return if @loaded && !force
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dir = File.expand_path(path)
|
|
25
|
-
next unless File.directory?(dir)
|
|
26
|
-
|
|
27
|
-
Dir["#{dir}/*.rb"].sort.each do |file|
|
|
28
|
-
load(file)
|
|
29
|
-
rescue => e
|
|
30
|
-
warn "elelem: failed to load plugin #{file}: #{e.message}"
|
|
31
|
-
end
|
|
32
|
-
end
|
|
18
|
+
@load_paths.each { |path| load_dir(File.expand_path(path)) }
|
|
19
|
+
@loaded = true
|
|
33
20
|
end
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_dir(dir)
|
|
25
|
+
return unless File.directory?(dir)
|
|
26
|
+
|
|
27
|
+
Dir["#{dir}/*.rb"].each { |file| load_plugin(file) }
|
|
37
28
|
end
|
|
38
29
|
|
|
39
|
-
def
|
|
40
|
-
|
|
30
|
+
def load_plugin(file)
|
|
31
|
+
load(file)
|
|
32
|
+
rescue StandardError, ScriptError => e
|
|
33
|
+
warn "elelem: failed to load plugin #{file}: #{e.message}"
|
|
41
34
|
end
|
|
42
35
|
end
|
|
43
36
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Registration
|
|
5
|
+
attr_reader :providers, :setups, :factories
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@providers = {}
|
|
9
|
+
@setups = {}
|
|
10
|
+
@factories = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def provider(name, &factory)
|
|
14
|
+
@providers[name.to_s] = factory
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup(name, &block)
|
|
19
|
+
@setups[name.to_s] = block
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def input(&factory)
|
|
24
|
+
@factories[:input] = factory
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def output(&factory)
|
|
29
|
+
@factories[:output] = factory
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear_setups!
|
|
34
|
+
@setups.clear
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(plugins: Plugins.new)
|
|
6
|
+
@registration = Registration.new
|
|
7
|
+
@plugins = plugins
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def configure
|
|
11
|
+
yield @registration
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_provider(name)
|
|
16
|
+
@plugins.load!
|
|
17
|
+
@registration.providers.fetch(name.to_s) { require_gem(name) }.call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply(session)
|
|
21
|
+
@plugins.load!
|
|
22
|
+
@registration.setups.each_value { |block| block.call(session) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reload(session)
|
|
26
|
+
@registration.clear_setups!
|
|
27
|
+
session.toolbox.clear!
|
|
28
|
+
session.commands.clear!
|
|
29
|
+
@plugins.load!(force: true)
|
|
30
|
+
apply(session)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def names
|
|
34
|
+
@registration.providers.keys
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_session(provider, toolbox: Toolbox.new, input: nil, output: nil)
|
|
38
|
+
commands = Commands.new
|
|
39
|
+
agent = Agent.new(build_provider(provider), toolbox: toolbox, output: output || build_output)
|
|
40
|
+
session = Session.new(agent, input: input || build_input(commands: commands), commands: commands)
|
|
41
|
+
apply(session)
|
|
42
|
+
session
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_input(**)
|
|
48
|
+
@plugins.load!
|
|
49
|
+
(@registration.factories[:input] || ->(**o) { Input.new(**o) }).call(**)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_output(**)
|
|
53
|
+
@plugins.load!
|
|
54
|
+
(@registration.factories[:output] || ->(**o) { Output.new(**o) }).call(**)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def require_gem(name)
|
|
58
|
+
require "elelem/#{name}"
|
|
59
|
+
@registration.providers.fetch(name.to_s) { unregistered_provider(name) }
|
|
60
|
+
rescue LoadError
|
|
61
|
+
raise "unknown provider: #{name.inspect}. Available: #{names.join(", ")}. Run: gem install elelem-#{name}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def unregistered_provider(name)
|
|
65
|
+
raise "elelem-#{name} did not register a provider named #{name.inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/elelem/repl.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Repl
|
|
5
|
+
def initialize(session)
|
|
6
|
+
@session = session
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
@session.output.say "elelem v#{VERSION}"
|
|
11
|
+
while (line = @session.input.ask("> "))
|
|
12
|
+
dispatch_line(line) unless line.empty?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def dispatch_line(line)
|
|
19
|
+
line.start_with?("/") ? command(line) : @session.turn(line)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def command(line)
|
|
23
|
+
name, args = line.delete_prefix("/").split(" ", 2)
|
|
24
|
+
@session.commands.run(name, args) || @session.output.say(@session.commands.names.join(" "))
|
|
25
|
+
rescue StandardError, ScriptError => e
|
|
26
|
+
warn_and_say(e)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def warn_and_say(error)
|
|
30
|
+
Elelem.logger.warn("repl: #{error.message}")
|
|
31
|
+
@session.output.say(error.message, as: :error)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Elelem
|
|
6
|
+
class Result
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def_delegators :@payload, :[], :each, :to_json, :to_h, :key?, :merge
|
|
10
|
+
|
|
11
|
+
def self.success(payload)
|
|
12
|
+
new(payload.merge(ok: true))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.failure(payload)
|
|
16
|
+
new(payload.merge(ok: false))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(payload)
|
|
20
|
+
@payload = payload
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ok? = !!@payload[:ok]
|
|
24
|
+
def error? = !!@payload[:error]
|
|
25
|
+
def error = @payload[:error]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Schema
|
|
5
|
+
def initialize(params: {}, required: [])
|
|
6
|
+
@params = params.freeze
|
|
7
|
+
@required = required.freeze
|
|
8
|
+
@schema_hash = { type: "object", properties: @params, required: @required }.freeze
|
|
9
|
+
@json_schemer = JSONSchemer.schema(@schema_hash)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_h
|
|
13
|
+
@schema_hash
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate(args)
|
|
17
|
+
@json_schemer.validate(args || {}).map { |error| error["error"] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Session
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
attr_reader :agent, :input, :commands
|
|
8
|
+
|
|
9
|
+
def_delegators :@agent, :provider, :provider=, :toolbox, :output, :conversation, :context, :turn
|
|
10
|
+
|
|
11
|
+
def initialize(agent, input: NullInput.new, commands: Commands.new)
|
|
12
|
+
@agent = agent
|
|
13
|
+
@input = input
|
|
14
|
+
@commands = commands
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class SlashCommand
|
|
5
|
+
attr_reader :name, :description, :completions
|
|
6
|
+
|
|
7
|
+
def initialize(name, description = "", completions = nil, &handler)
|
|
8
|
+
@name = name
|
|
9
|
+
@description = description
|
|
10
|
+
@completions = completions
|
|
11
|
+
@handler = handler
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(args)
|
|
15
|
+
@handler.call(args)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class StubProvider
|
|
5
|
+
def fetch(messages, tools = [], &block)
|
|
6
|
+
block.call(type: "thinking", text: "considering the request")
|
|
7
|
+
return block.call(type: "saying", text: "done.") if messages.last[:role] == "tool"
|
|
8
|
+
|
|
9
|
+
block.call(type: "saying", text: "let me check.")
|
|
10
|
+
return unless (tool = tools.first)
|
|
11
|
+
|
|
12
|
+
block.call(type: "doing", id: "stub-1", name: tool[:function][:name], arguments: {})
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/elelem/system_prompt.rb
CHANGED
|
@@ -2,153 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class SystemPrompt
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
DEFAULT = File.read(File.expand_path("prompts/default.erb", __dir__)).freeze
|
|
6
|
+
USER_AGENTS_MD = Pathname.new(Dir.home).join(".agents/AGENTS.md").freeze
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
- read(path): file contents
|
|
10
|
-
- write(path, content): create/overwrite file
|
|
11
|
-
- execute(command): shell command
|
|
12
|
-
- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
|
|
13
|
-
- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
|
|
8
|
+
attr_accessor :template
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Use write for new files or full rewrites
|
|
19
|
-
|
|
20
|
-
# Search
|
|
21
|
-
Use execute(`rg`) for text search: `rg -n "pattern" .`
|
|
22
|
-
Use execute(`fd`) for file discovery: `fd -e rb .`
|
|
23
|
-
Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
|
|
24
|
-
|
|
25
|
-
# Task Management
|
|
26
|
-
For complex tasks:
|
|
27
|
-
1. State plan before acting
|
|
28
|
-
2. Work through steps one at a time
|
|
29
|
-
3. Summarize what was done
|
|
30
|
-
|
|
31
|
-
# Long Tasks
|
|
32
|
-
For complex multi-step work, write notes to .elelem/scratch.md
|
|
33
|
-
|
|
34
|
-
# Policy
|
|
35
|
-
- Explain before non-trivial commands
|
|
36
|
-
- Verify changes (read file, run tests)
|
|
37
|
-
- No interactive flags (-i, -p)
|
|
38
|
-
- Use `man` when you need to understand how to execute a program
|
|
39
|
-
|
|
40
|
-
# Environment
|
|
41
|
-
pwd: <%= pwd %>
|
|
42
|
-
platform: <%= platform %>
|
|
43
|
-
date: <%= date %>
|
|
44
|
-
self: <%= elelem_source %>
|
|
45
|
-
<%= git_info %>
|
|
46
|
-
|
|
47
|
-
<% if repo_map && !repo_map.empty? %>
|
|
48
|
-
# Codebase
|
|
49
|
-
```
|
|
50
|
-
<%= repo_map %>```
|
|
51
|
-
<% end %>
|
|
52
|
-
<%= agents_md %>
|
|
53
|
-
ERB
|
|
10
|
+
def initialize(template = nil)
|
|
11
|
+
@template = template || DEFAULT
|
|
12
|
+
end
|
|
54
13
|
|
|
55
14
|
def render
|
|
56
|
-
ERB.new(
|
|
15
|
+
ERB.new(template, trim_mode: "-").result(binding)
|
|
57
16
|
end
|
|
58
17
|
|
|
59
18
|
private
|
|
60
19
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
def date = Date.today
|
|
64
|
-
|
|
65
|
-
def elelem_source
|
|
66
|
-
spec = Gem.loaded_specs["elelem"]
|
|
67
|
-
spec ? spec.gem_dir : File.expand_path("../..", __dir__)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def git_info
|
|
71
|
-
return unless File.exist?(".git")
|
|
72
|
-
"branch: #{`git branch --show-current`.strip}"
|
|
73
|
-
rescue Errno::ENOENT
|
|
74
|
-
nil
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def repo_map
|
|
78
|
-
files = `git ls-files '*.rb' 2>/dev/null`.lines.map(&:strip)
|
|
79
|
-
return "" if files.empty?
|
|
80
|
-
|
|
81
|
-
symbols = extract_symbols(files)
|
|
82
|
-
format_symbols(symbols, budget: 2000)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def extract_symbols(files)
|
|
86
|
-
output, status = Open3.capture2("sg", "run", "-p", "def $NAME", "-l", "ruby", "--json=compact", ".", err: File::NULL)
|
|
87
|
-
return ctags_fallback(files) unless status.success?
|
|
88
|
-
|
|
89
|
-
parse_sg_output(output, files)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def parse_sg_output(output, tracked_files)
|
|
93
|
-
JSON.parse(output).filter_map do |match|
|
|
94
|
-
file = match["file"]
|
|
95
|
-
next unless tracked_files.include?(file)
|
|
96
|
-
{ file: file, name: match.dig("metaVariables", "single", "NAME", "text") }
|
|
97
|
-
end
|
|
98
|
-
rescue JSON::ParserError
|
|
99
|
-
[]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def ctags_fallback(files)
|
|
103
|
-
return [] if files.empty?
|
|
104
|
-
|
|
105
|
-
output = IO.popen(["ctags", "-x", "--languages=Ruby", "--kinds-Ruby=cfm", "-L", "-"], "r+") do |io|
|
|
106
|
-
io.puts(files)
|
|
107
|
-
io.close_write
|
|
108
|
-
io.read
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
output.lines.map do |line|
|
|
112
|
-
parts = line.split(/\s+/, 4)
|
|
113
|
-
{ file: parts[2], name: parts[0] }
|
|
114
|
-
end
|
|
115
|
-
rescue Errno::ENOENT
|
|
116
|
-
[]
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def format_symbols(symbols, budget:)
|
|
120
|
-
tree = build_tree(symbols)
|
|
121
|
-
render_tree(tree, budget: budget)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def build_tree(symbols)
|
|
125
|
-
tree = {}
|
|
126
|
-
symbols.group_by { |s| s[:file] }.each do |file, syms|
|
|
127
|
-
parts = file.split("/")
|
|
128
|
-
node = tree
|
|
129
|
-
parts[0..-2].each { |dir| node = (node[dir + "/"] ||= {}) }
|
|
130
|
-
node[parts.last] = syms.map { |s| s[:name] }.uniq
|
|
131
|
-
end
|
|
132
|
-
tree
|
|
20
|
+
def agents_md
|
|
21
|
+
[global_agents_md, project_agents_md].compact.then { |parts| parts.empty? ? nil : parts.join("\n\n") }
|
|
133
22
|
end
|
|
134
23
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
if value.is_a?(Hash)
|
|
138
|
-
line = " " * indent + key + "\n"
|
|
139
|
-
return result if result.length + line.length > budget
|
|
140
|
-
result << line
|
|
141
|
-
render_tree(value, indent: indent + 1, budget: budget, result: result)
|
|
142
|
-
else
|
|
143
|
-
line = " " * indent + key.sub(/\.rb$/, "") + ": " + value.join(" ") + "\n"
|
|
144
|
-
return result if result.length + line.length > budget
|
|
145
|
-
result << line
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
result
|
|
24
|
+
def global_agents_md
|
|
25
|
+
USER_AGENTS_MD.read if USER_AGENTS_MD.exist?
|
|
149
26
|
end
|
|
150
27
|
|
|
151
|
-
def
|
|
28
|
+
def project_agents_md
|
|
152
29
|
Pathname.pwd.ascend.each do |dir|
|
|
153
30
|
file = dir / "AGENTS.md"
|
|
154
31
|
return file.read if file.exist?
|
data/lib/elelem/tool.rb
CHANGED
|
@@ -2,27 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Tool
|
|
5
|
-
attr_reader :name, :description, :
|
|
5
|
+
attr_reader :name, :description, :aliases
|
|
6
6
|
|
|
7
|
-
def initialize(name, description:,
|
|
7
|
+
def initialize(name, description:, schema: Schema.new, aliases: [], &handler)
|
|
8
8
|
@name = name
|
|
9
9
|
@description = description
|
|
10
|
-
@
|
|
11
|
-
@required = required.freeze
|
|
10
|
+
@schema = schema
|
|
12
11
|
@aliases = aliases.freeze
|
|
13
|
-
@
|
|
14
|
-
@schema_hash = { type: "object", properties: @params, required: @required }.freeze
|
|
15
|
-
@schema = JSONSchemer.schema(@schema_hash)
|
|
12
|
+
@handler = handler
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def call(args)
|
|
19
|
-
@
|
|
16
|
+
@handler.call(args)
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
def validate(args)
|
|
23
|
-
@schema.validate(args
|
|
24
|
-
error["error"]
|
|
25
|
-
end
|
|
20
|
+
@schema.validate(args)
|
|
26
21
|
end
|
|
27
22
|
|
|
28
23
|
def to_h
|
|
@@ -31,7 +26,7 @@ module Elelem
|
|
|
31
26
|
function: {
|
|
32
27
|
name: name,
|
|
33
28
|
description: description,
|
|
34
|
-
parameters: @
|
|
29
|
+
parameters: @schema.to_h
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
end
|