elelem 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +93 -16
  4. data/Rakefile +0 -11
  5. data/exe/elelem +1 -78
  6. data/lib/elelem/agent.rb +33 -124
  7. data/lib/elelem/commands.rb +33 -0
  8. data/lib/elelem/conversation.rb +25 -0
  9. data/lib/elelem/mcp/oauth.rb +217 -0
  10. data/lib/elelem/mcp/token_storage.rb +60 -0
  11. data/lib/elelem/mcp.rb +164 -17
  12. data/lib/elelem/net/claude.rb +6 -4
  13. data/lib/elelem/net/ollama.rb +5 -2
  14. data/lib/elelem/net/openai.rb +6 -4
  15. data/lib/elelem/net.rb +0 -3
  16. data/lib/elelem/permissions.rb +45 -0
  17. data/lib/elelem/plugins/builtins.rb +96 -0
  18. data/lib/elelem/plugins/edit.rb +3 -3
  19. data/lib/elelem/plugins/eval.rb +4 -4
  20. data/lib/elelem/plugins/execute.rb +5 -5
  21. data/lib/elelem/plugins/git.rb +20 -0
  22. data/lib/elelem/plugins/glob.rb +13 -0
  23. data/lib/elelem/plugins/grep.rb +21 -0
  24. data/lib/elelem/plugins/list.rb +14 -0
  25. data/lib/elelem/plugins/mcp.rb +14 -8
  26. data/lib/elelem/plugins/permissions.json +6 -0
  27. data/lib/elelem/plugins/read.rb +6 -6
  28. data/lib/elelem/plugins/task.rb +14 -0
  29. data/lib/elelem/plugins/tools.rb +13 -0
  30. data/lib/elelem/plugins/verify.rb +4 -4
  31. data/lib/elelem/plugins/write.rb +17 -6
  32. data/lib/elelem/plugins/zz_confirm.rb +9 -0
  33. data/lib/elelem/plugins.rb +6 -6
  34. data/lib/elelem/system_prompt.rb +123 -29
  35. data/lib/elelem/terminal.rb +7 -1
  36. data/lib/elelem/tool.rb +6 -15
  37. data/lib/elelem/toolbox.rb +13 -4
  38. data/lib/elelem/version.rb +1 -1
  39. data/lib/elelem.rb +96 -5
  40. metadata +99 -3
  41. data/lib/elelem/plugins/confirm.rb +0 -12
  42. data/lib/elelem/templates/system_prompt.erb +0 -53
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:builtins) do |agent|
4
+ agent.commands.register("exit", description: "Exit elelem") { exit(0) }
5
+
6
+ agent.commands.register("clear", description: "Clear conversation history") do
7
+ agent.conversation.clear!
8
+ agent.terminal.say " → context cleared"
9
+ end
10
+
11
+ agent.commands.register("context", description: "Show conversation context") do |args|
12
+ messages = agent.context
13
+
14
+ case args
15
+ when nil, ""
16
+ messages.each_with_index do |msg, i|
17
+ role = msg[:role]
18
+ preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
19
+ preview += "..." if msg[:content].to_s.length > 60
20
+ agent.terminal.say " #{i + 1}. #{role}: #{preview}"
21
+ end
22
+ when "json"
23
+ agent.terminal.say JSON.pretty_generate(messages)
24
+ when /^\d+$/
25
+ index = args.to_i - 1
26
+ if index >= 0 && index < messages.length
27
+ content = messages[index][:content].to_s
28
+ agent.terminal.say(agent.terminal.markdown(content))
29
+ else
30
+ agent.terminal.say " Invalid index: #{args}"
31
+ end
32
+ else
33
+ agent.terminal.say " Usage: /context [json|<number>]"
34
+ end
35
+ end
36
+
37
+ strip_ansi = ->(text) do
38
+ text
39
+ .gsub(/^Script started.*?\n/, "")
40
+ .gsub(/\nScript done.*$/, "")
41
+ .gsub(/\e\].*?(?:\a|\e\\)/, "")
42
+ .gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
43
+ .gsub(/\e[PX^_].*?\e\\/, "")
44
+ .gsub(/\e./, "")
45
+ .gsub(/[\b]/, "")
46
+ .gsub(/\r/, "")
47
+ end
48
+
49
+ agent.commands.register("shell", description: "Start interactive shell") do
50
+ transcript = Tempfile.create do |file|
51
+ system("script", "-q", file.path, chdir: Dir.pwd)
52
+ strip_ansi.call(File.read(file.path))
53
+ end
54
+ agent.conversation.add(role: "user", content: transcript) unless transcript.strip.empty?
55
+ end
56
+
57
+ agent.commands.register("init", description: "Generate AGENTS.md") do
58
+ system_prompt = <<~PROMPT
59
+ AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
60
+
61
+ # AGENTS.md Spec (https://agents.md/)
62
+ A file providing context and instructions for AI coding agents.
63
+
64
+ ## Recommended Sections
65
+ - Commands: build, test, lint commands
66
+ - Code Style: conventions, patterns
67
+ - Architecture: key components and flow
68
+ - Testing: how to run tests
69
+
70
+ ## Process
71
+ 1. Read README.md if present
72
+ 2. Identify language (Gemfile, package.json, go.mod)
73
+ 3. Find test scripts (bin/test, npm test)
74
+ 4. Check linter configs
75
+ 5. Write concise AGENTS.md
76
+
77
+ Keep it minimal. No fluff.
78
+ PROMPT
79
+
80
+ agent.fork(system_prompt: system_prompt).turn("Generate AGENTS.md for this project")
81
+ end
82
+
83
+ agent.commands.register("reload", description: "Reload plugins and source") do
84
+ lib_dir = File.expand_path("../..", __dir__)
85
+ original_verbose, $VERBOSE = $VERBOSE, nil
86
+ Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
87
+ $VERBOSE = original_verbose
88
+ agent.toolbox = Elelem::Toolbox.new
89
+ agent.commands = Elelem::Commands.new
90
+ Elelem::Plugins.reload!(agent)
91
+ end
92
+
93
+ agent.commands.register("help", description: "Show available commands") do
94
+ agent.terminal.say agent.commands.names.join(" ")
95
+ end
96
+ end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:edit) do |toolbox|
4
- toolbox.add("edit",
3
+ Elelem::Plugins.register(:edit) do |agent|
4
+ agent.toolbox.add("edit",
5
5
  description: "Replace first occurrence of text in file",
6
6
  params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
7
7
  required: ["path", "old", "new"]
8
8
  ) do |a|
9
9
  path = Pathname.new(a["path"]).expand_path
10
10
  content = path.read
11
- toolbox
11
+ agent.toolbox
12
12
  .run("write", { "path" => a["path"], "content" => content.sub(a["old"], a["new"]) })
13
13
  .merge(replaced: a["old"], with: a["new"])
14
14
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:eval) do |toolbox|
3
+ Elelem::Plugins.register(:eval) do |agent|
4
4
  description = <<~'DESC'
5
5
  Evaluate Ruby code. Available API:
6
6
 
7
7
  name = "search"
8
- toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
9
- toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
8
+ agent.toolbox.add(name, description: "Search using rg", params: { query: { type: "string" } }, required: ["query"], aliases: []) do |args|
9
+ agent.toolbox.run("execute", { "command" => "rg --json -nI -F #{args["query"]}" })
10
10
  end
11
11
  DESC
12
12
 
13
- toolbox.add("eval",
13
+ agent.toolbox.add("eval",
14
14
  description: description,
15
15
  params: { ruby: { type: "string" } },
16
16
  required: ["ruby"]
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:execute) do |toolbox|
4
- toolbox.add("execute",
3
+ Elelem::Plugins.register(:execute) do |agent|
4
+ agent.toolbox.add("execute",
5
5
  description: "Run shell command (supports pipes and redirections)",
6
6
  params: { command: { type: "string" } },
7
7
  required: ["command"],
8
8
  aliases: ["bash", "sh", "exec", "execute<|channel|>"]
9
9
  ) do |a|
10
- Elelem.sh("bash", args: ["-c", a["command"]]) { |x| $stdout.print(x) }
10
+ Elelem.sh("bash", args: ["-c", a["command"]]) { |x| agent.terminal.print(x) }
11
11
  end
12
12
 
13
- toolbox.after("execute") do |args, result|
13
+ agent.toolbox.after("execute") do |args, result|
14
14
  next if result[:exit_status] == 0
15
15
 
16
- $stdout.puts toolbox.header("execute", args, state: "x")
16
+ agent.terminal.say agent.toolbox.header("execute", args, state: "x")
17
17
  end
18
18
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:git) do |agent|
4
+ allowed = %w[status diff log show branch checkout add reset stash].freeze
5
+
6
+ agent.toolbox.add("git",
7
+ description: "Run git command",
8
+ params: { command: { type: "string" }, args: { type: "array" } },
9
+ required: ["command"]
10
+ ) do |a|
11
+ cmd = a["command"]
12
+ next { error: "not allowed: #{cmd}" } unless allowed.include?(cmd)
13
+
14
+ agent.toolbox.exec("git", cmd, *(a["args"] || []))
15
+ end
16
+
17
+ agent.toolbox.after("git") do |_, result|
18
+ agent.terminal.say " ! #{result[:error]}" if result[:error]
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:glob) do |agent|
4
+ agent.toolbox.add("glob",
5
+ description: "Find files matching pattern",
6
+ params: { pattern: { type: "string" }, path: { type: "string" } },
7
+ required: ["pattern"]
8
+ ) do |a|
9
+ path = a["path"] || "."
10
+ result = agent.toolbox.exec("fd", "--glob", a["pattern"], path)
11
+ result[:ok] ? result : agent.toolbox.exec("find", path, "-name", a["pattern"])
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:grep) do |agent|
4
+ agent.toolbox.add("grep",
5
+ description: "Search file contents",
6
+ params: { pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" } },
7
+ required: ["pattern"]
8
+ ) do |a|
9
+ path = a["path"] || "."
10
+ glob = a["glob"]
11
+ rg_args = ["rg", "-n", a["pattern"], path]
12
+ rg_args += ["-g", glob] if glob
13
+ result = agent.toolbox.exec(*rg_args)
14
+ next result if result[:ok]
15
+
16
+ grep_args = ["grep", "-rn"]
17
+ grep_args += ["--include", glob] if glob
18
+ grep_args += [a["pattern"], path]
19
+ agent.toolbox.exec(*grep_args)
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:list) do |agent|
4
+ agent.toolbox.add("list",
5
+ description: "List directory contents",
6
+ params: { path: { type: "string" }, recursive: { type: "boolean" } },
7
+ required: [],
8
+ aliases: ["ls"]
9
+ ) do |a|
10
+ path = a["path"] || "."
11
+ flags = a["recursive"] ? "-laR" : "-la"
12
+ agent.toolbox.exec("ls", flags, path)
13
+ end
14
+ end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:mcp) do |toolbox|
3
+ Elelem::Plugins.register(:mcp) do |agent|
4
4
  mcp = Elelem::MCP.new
5
5
  at_exit { mcp.close }
6
- mcp.tools.each do |name, tool|
7
- toolbox.add(name,
8
- description: tool[:description],
9
- params: tool[:params],
10
- required: tool[:required],
11
- &tool[:fn]
12
- )
6
+
7
+ Thread.new do
8
+ mcp.tools.each do |name, tool|
9
+ agent.toolbox.add(
10
+ name,
11
+ description: tool[:description],
12
+ params: tool[:params],
13
+ required: tool[:required],
14
+ &tool[:fn]
15
+ )
16
+ end
17
+ rescue => e
18
+ warn "MCP failed: #{e.message}"
13
19
  end
14
20
  end
@@ -0,0 +1,6 @@
1
+ {
2
+ "read": "allow",
3
+ "write": "ask",
4
+ "edit": "ask",
5
+ "execute": "ask"
6
+ }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:read) do |toolbox|
4
- toolbox.add("read",
3
+ Elelem::Plugins.register(:read) do |agent|
4
+ agent.toolbox.add("read",
5
5
  description: "Read file",
6
6
  params: { path: { type: "string" } },
7
7
  required: ["path"],
@@ -11,11 +11,11 @@ Elelem::Plugins.register(:read) do |toolbox|
11
11
  path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
12
12
  end
13
13
 
14
- toolbox.after("read") do |_, result|
14
+ agent.toolbox.after("read") do |_, result|
15
15
  if result[:error]
16
- $stdout.puts " ! #{result[:error]}"
17
- elsif !system("bat", "--paging=never", result[:path])
18
- $stdout.puts result[:content]
16
+ agent.terminal.say " ! #{result[:error]}"
17
+ else
18
+ agent.terminal.display_file(result[:path], fallback: result[:content])
19
19
  end
20
20
  end
21
21
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:task) do |agent|
4
+ agent.toolbox.add("task",
5
+ description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
6
+ params: { prompt: { type: "string" } },
7
+ required: ["prompt"]
8
+ ) do |a|
9
+ sub = Elelem::Agent.new(agent.client, toolbox: agent.toolbox, terminal: agent.terminal,
10
+ system_prompt: "Research agent. Search, analyze, report. Be concise.")
11
+ sub.turn(a["prompt"])
12
+ { result: sub.conversation.last[:content] }
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:tools) do |agent|
4
+ agent.commands.register("tools", description: "List available tools") do
5
+ agent.toolbox.tools.each_value do |tool|
6
+ agent.terminal.say ""
7
+ agent.terminal.say " #{tool.name}"
8
+ agent.terminal.say " #{tool.description}"
9
+ tool.params.each { |k, v| agent.terminal.say " #{k}: #{v[:type] || v["type"]}" }
10
+ agent.terminal.say " aliases: #{tool.aliases.join(", ")}" if tool.aliases.any?
11
+ end
12
+ end
13
+ end
@@ -27,16 +27,16 @@ module Elelem
27
27
  end
28
28
  end
29
29
 
30
- Plugins.register(:verify) do |toolbox|
31
- toolbox.add("verify",
30
+ Plugins.register(:verify) do |agent|
31
+ agent.toolbox.add("verify",
32
32
  description: "Verify file syntax and run tests",
33
33
  params: { path: { type: "string" } },
34
34
  required: ["path"]
35
35
  ) do |a|
36
36
  path = a["path"]
37
37
  Verifiers.for(path).inject({verified: []}) do |memo, cmd|
38
- $stdout.puts toolbox.header("execute", { "command" => cmd })
39
- v = toolbox.run("execute", { "command" => cmd })
38
+ agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
39
+ v = agent.toolbox.run("execute", { "command" => cmd })
40
40
  break v.merge(path: path, command: cmd) if v[:exit_status] != 0
41
41
 
42
42
  memo[:verified] << cmd
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Elelem::Plugins.register(:write) do |toolbox|
4
- toolbox.add("write",
3
+ Elelem::Plugins.register(:write) do |agent|
4
+ agent.toolbox.add("write",
5
5
  description: "Write file",
6
6
  params: { path: { type: "string" }, content: { type: "string" } },
7
7
  required: ["path", "content"],
@@ -12,12 +12,23 @@ Elelem::Plugins.register(:write) do |toolbox|
12
12
  { bytes: path.write(a["content"]), path: a["path"] }
13
13
  end
14
14
 
15
- toolbox.after("write") do |_, result|
15
+ agent.toolbox.before("write") do |args|
16
+ path = Pathname.new(args["path"]).expand_path
17
+ next unless path.exist? && $stdin.tty?
18
+
19
+ Tempfile.create(["elelem", File.extname(path)]) do |t|
20
+ t.write(args["content"])
21
+ t.flush
22
+ system("diff", "--color=always", "-u", path.to_s, t.path)
23
+ end
24
+ end
25
+
26
+ agent.toolbox.after("write") do |_, result|
16
27
  if result[:error]
17
- $stdout.puts " ! #{result[:error]}"
28
+ agent.terminal.say " ! #{result[:error]}"
18
29
  else
19
- system("bat", "--paging=never", result[:path]) || $stdout.puts(" -> #{result[:path]}")
20
- toolbox.run("verify", { "path" => result[:path] })
30
+ agent.terminal.display_file(result[:path], fallback: " -> #{result[:path]}")
31
+ agent.toolbox.run("verify", { "path" => result[:path] })
21
32
  end
22
33
  end
23
34
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Elelem::Plugins.register(:confirm) do |agent|
4
+ permissions = Elelem::Permissions.new
5
+
6
+ agent.toolbox.before do |args, tool_name:|
7
+ permissions.check(tool_name, args, terminal: agent.terminal)
8
+ end
9
+ end
@@ -8,15 +8,15 @@ module Elelem
8
8
  ".elelem/plugins"
9
9
  ].freeze
10
10
 
11
- def self.setup!(toolbox)
11
+ def self.setup!(agent)
12
12
  load_plugins
13
- registry.each_value { |plugin| plugin.call(toolbox) }
13
+ registry.each_value { |plugin| plugin.call(agent) }
14
14
  end
15
15
 
16
- def self.reload!(toolbox)
17
- @registry = {}
16
+ def self.reload!(agent)
17
+ registry.clear
18
18
  load_plugins
19
- registry.each_value { |plugin| plugin.call(toolbox) }
19
+ registry.each_value { |plugin| plugin.call(agent) }
20
20
  end
21
21
 
22
22
  def self.load_plugins
@@ -33,7 +33,7 @@ module Elelem
33
33
  end
34
34
 
35
35
  def self.register(name, &block)
36
- (@registry ||= {})[name] = block
36
+ registry[name] = block
37
37
  end
38
38
 
39
39
  def self.registry
@@ -2,56 +2,150 @@
2
2
 
3
3
  module Elelem
4
4
  class SystemPrompt
5
- TEMPLATE_PATH = File.expand_path("templates/system_prompt.erb", __dir__)
5
+ TEMPLATE = <<~ERB
6
+ Terminal coding agent. Be concise. Verify your work.
6
7
 
7
- attr_reader :memory
8
+ # Tools
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
14
 
9
- def initialize(memory: nil)
10
- @memory = memory
11
- end
15
+ # Editing
16
+ Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
17
+ Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
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
12
54
 
13
55
  def render
14
- ERB.new(template, trim_mode: "-").result(binding)
56
+ ERB.new(TEMPLATE, trim_mode: "-").result(binding)
15
57
  end
16
58
 
17
59
  private
18
60
 
19
- def template
20
- File.read(TEMPLATE_PATH)
61
+ def pwd = Dir.pwd
62
+ def platform = RUBY_PLATFORM.split("-").last
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__)
21
68
  end
22
69
 
23
- def pwd
24
- Dir.pwd
70
+ def git_info
71
+ return unless File.exist?(".git")
72
+ "branch: #{`git branch --show-current`.strip}"
73
+ rescue Errno::ENOENT
74
+ nil
25
75
  end
26
76
 
27
- def elelem_source
28
- File.expand_path("../..", __dir__)
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)
29
83
  end
30
84
 
31
- def platform
32
- RUBY_PLATFORM.split("-").last
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)
33
90
  end
34
91
 
35
- def date
36
- Date.today
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
+ []
37
100
  end
38
101
 
39
- def git_branch
40
- return unless File.exist?(".git")
102
+ def ctags_fallback(files)
103
+ return [] if files.empty?
41
104
 
42
- "branch: #{`git branch --show-current`.strip}"
43
- rescue
44
- nil
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
+ []
45
117
  end
46
118
 
47
- def repo_map
48
- `ctags -x --sort=no --languages=Ruby,Python,JavaScript,TypeScript,Go,Rust -R . 2>/dev/null`
49
- .lines
50
- .reject { |l| l.include?("vendor/") || l.include?("node_modules/") || l.include?("spec/") }
51
- .first(100)
52
- .join
53
- rescue
54
- ""
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
133
+ end
134
+
135
+ def render_tree(node, indent: 0, budget:, result: String.new)
136
+ node.each do |key, value|
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
55
149
  end
56
150
 
57
151
  def agents_md
@@ -51,6 +51,12 @@ module Elelem
51
51
  n.times { $stdout.puts("") }
52
52
  end
53
53
 
54
+ def display_file(path, fallback: nil)
55
+ return if @quiet
56
+
57
+ system("bat", "--paging=never", path) || say(fallback || path)
58
+ end
59
+
54
60
  def waiting
55
61
  return if @quiet
56
62
 
@@ -66,7 +72,7 @@ module Elelem
66
72
  private
67
73
 
68
74
  def blank?(text)
69
- text.nil? || text.strip.empty?
75
+ text.nil? || text.to_s.strip.empty?
70
76
  end
71
77
 
72
78
  def stop_dots
data/lib/elelem/tool.rb CHANGED
@@ -7,11 +7,12 @@ module Elelem
7
7
  def initialize(name, description:, params: {}, required: [], aliases: [], &fn)
8
8
  @name = name
9
9
  @description = description
10
- @params = params
11
- @required = required
12
- @aliases = aliases
10
+ @params = params.freeze
11
+ @required = required.freeze
12
+ @aliases = aliases.freeze
13
13
  @fn = fn
14
- @schema = JSONSchemer.schema(schema_hash)
14
+ @schema_hash = { type: "object", properties: @params, required: @required }.freeze
15
+ @schema = JSONSchemer.schema(@schema_hash)
15
16
  end
16
17
 
17
18
  def call(args)
@@ -30,19 +31,9 @@ module Elelem
30
31
  function: {
31
32
  name: name,
32
33
  description: description,
33
- parameters: schema_hash
34
+ parameters: @schema_hash
34
35
  }
35
36
  }
36
37
  end
37
-
38
- private
39
-
40
- def schema_hash
41
- {
42
- type: "object",
43
- properties: params,
44
- required: required
45
- }
46
- end
47
38
  end
48
39
  end