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
|
@@ -1,96 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/edit.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:edit) do |agent|
|
|
4
|
-
agent.toolbox.add("edit",
|
|
5
|
-
description: "Replace first occurrence of text in file",
|
|
6
|
-
params: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } },
|
|
7
|
-
required: ["path", "old", "new"]
|
|
8
|
-
) do |a|
|
|
9
|
-
path = Pathname.new(a["path"]).expand_path
|
|
10
|
-
content = path.read
|
|
11
|
-
agent.toolbox
|
|
12
|
-
.run("write", { "path" => a["path"], "content" => content.sub(a["old"], a["new"]) })
|
|
13
|
-
.merge(replaced: a["old"], with: a["new"])
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/elelem/plugins/eval.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:eval) do |agent|
|
|
4
|
-
description = <<~'DESC'
|
|
5
|
-
Evaluate Ruby code. Available API:
|
|
6
|
-
|
|
7
|
-
name = "search"
|
|
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
|
-
end
|
|
11
|
-
DESC
|
|
12
|
-
|
|
13
|
-
agent.toolbox.add("eval",
|
|
14
|
-
description: description,
|
|
15
|
-
params: { ruby: { type: "string" } },
|
|
16
|
-
required: ["ruby"]
|
|
17
|
-
) do |args|
|
|
18
|
-
{ result: binding.eval(args["ruby"]) }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:execute) do |agent|
|
|
4
|
-
agent.toolbox.add("execute",
|
|
5
|
-
description: "Run shell command (supports pipes and redirections)",
|
|
6
|
-
params: { command: { type: "string" } },
|
|
7
|
-
required: ["command"],
|
|
8
|
-
aliases: ["bash", "sh", "exec", "execute<|channel|>"]
|
|
9
|
-
) do |a|
|
|
10
|
-
Elelem.sh("bash", args: ["-c", a["command"]]) { |x| agent.terminal.print(x) }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
agent.toolbox.after("execute") do |args, result|
|
|
14
|
-
next if result[:exit_status] == 0
|
|
15
|
-
|
|
16
|
-
agent.terminal.say agent.toolbox.header("execute", args, state: "x")
|
|
17
|
-
end
|
|
18
|
-
end
|
data/lib/elelem/plugins/git.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/glob.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/grep.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/list.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/mcp.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:mcp) do |agent|
|
|
4
|
-
mcp = Elelem::MCP.new
|
|
5
|
-
at_exit { mcp.close }
|
|
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}"
|
|
19
|
-
end
|
|
20
|
-
end
|
data/lib/elelem/plugins/read.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:read) do |agent|
|
|
4
|
-
agent.toolbox.add("read",
|
|
5
|
-
description: "Read file",
|
|
6
|
-
params: { path: { type: "string" } },
|
|
7
|
-
required: ["path"],
|
|
8
|
-
aliases: ["open"]
|
|
9
|
-
) do |a|
|
|
10
|
-
path = Pathname.new(a["path"]).expand_path
|
|
11
|
-
path.exist? ? { content: path.read, path: a["path"] } : { error: "not found" }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
agent.toolbox.after("read") do |_, result|
|
|
15
|
-
if result[:error]
|
|
16
|
-
agent.terminal.say " ! #{result[:error]}"
|
|
17
|
-
else
|
|
18
|
-
agent.terminal.display_file(result[:path], fallback: result[:content])
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
data/lib/elelem/plugins/task.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
data/lib/elelem/plugins/tools.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
module Verifiers
|
|
5
|
-
SYNTAX = {
|
|
6
|
-
".rb" => "ruby -c %{path}",
|
|
7
|
-
".erb" => "erb -x %{path} | ruby -c",
|
|
8
|
-
".py" => "python -m py_compile %{path}",
|
|
9
|
-
".go" => "go vet %{path}",
|
|
10
|
-
".rs" => "cargo check --quiet",
|
|
11
|
-
".ts" => "npx tsc --noEmit %{path}",
|
|
12
|
-
".js" => "node --check %{path}",
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
def self.for(path)
|
|
16
|
-
return [] unless path
|
|
17
|
-
|
|
18
|
-
cmds = []
|
|
19
|
-
ext = File.extname(path)
|
|
20
|
-
cmds << (SYNTAX[ext] % { path: path }) if SYNTAX[ext]
|
|
21
|
-
cmds << test_runner
|
|
22
|
-
cmds.compact
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.test_runner
|
|
26
|
-
%w[bin/test script/test].find { |s| File.executable?(s) }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
Plugins.register(:verify) do |agent|
|
|
31
|
-
agent.toolbox.add("verify",
|
|
32
|
-
description: "Verify file syntax and run tests",
|
|
33
|
-
params: { path: { type: "string" } },
|
|
34
|
-
required: ["path"]
|
|
35
|
-
) do |a|
|
|
36
|
-
path = a["path"]
|
|
37
|
-
Verifiers.for(path).inject({verified: []}) do |memo, cmd|
|
|
38
|
-
agent.terminal.say agent.toolbox.header("execute", { "command" => cmd })
|
|
39
|
-
v = agent.toolbox.run("execute", { "command" => cmd })
|
|
40
|
-
break v.merge(path: path, command: cmd) if v[:exit_status] != 0
|
|
41
|
-
|
|
42
|
-
memo[:verified] << cmd
|
|
43
|
-
memo
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
data/lib/elelem/plugins/write.rb
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Elelem::Plugins.register(:write) do |agent|
|
|
4
|
-
agent.toolbox.add("write",
|
|
5
|
-
description: "Write file",
|
|
6
|
-
params: { path: { type: "string" }, content: { type: "string" } },
|
|
7
|
-
required: ["path", "content"],
|
|
8
|
-
aliases: ["write<|channel|>"]
|
|
9
|
-
) do |a|
|
|
10
|
-
path = Pathname.new(a["path"]).expand_path
|
|
11
|
-
FileUtils.mkdir_p(path.dirname)
|
|
12
|
-
{ bytes: path.write(a["content"]), path: a["path"] }
|
|
13
|
-
end
|
|
14
|
-
|
|
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|
|
|
27
|
-
if result[:error]
|
|
28
|
-
agent.terminal.say " ! #{result[:error]}"
|
|
29
|
-
else
|
|
30
|
-
agent.terminal.display_file(result[:path], fallback: " -> #{result[:path]}")
|
|
31
|
-
agent.toolbox.run("verify", { "path" => result[:path] })
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
data/lib/elelem/terminal.rb
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Elelem
|
|
4
|
-
class Terminal
|
|
5
|
-
def initialize(commands: [], quiet: false)
|
|
6
|
-
@commands = commands
|
|
7
|
-
@quiet = quiet
|
|
8
|
-
@dots_thread = nil
|
|
9
|
-
setup_completion unless @quiet
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def ask(prompt)
|
|
13
|
-
Reline.readline(prompt, true)&.strip
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def think(text)
|
|
17
|
-
return if blank?(text)
|
|
18
|
-
|
|
19
|
-
"\e[2;3m#{text}\e[0m"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def markdown(text)
|
|
23
|
-
return if @quiet || blank?(text)
|
|
24
|
-
|
|
25
|
-
newline(n: 2)
|
|
26
|
-
width = $stdout.winsize[1] rescue 80
|
|
27
|
-
IO.popen(["glow", "-s", "dark", "-w", width.to_s, "-"], "r+") do |io|
|
|
28
|
-
io.write(text)
|
|
29
|
-
io.close_write
|
|
30
|
-
io.read
|
|
31
|
-
end
|
|
32
|
-
rescue Errno::ENOENT
|
|
33
|
-
text
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def print(text)
|
|
37
|
-
return if @quiet || blank?(text)
|
|
38
|
-
|
|
39
|
-
stop_dots
|
|
40
|
-
$stdout.print text
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def say(text)
|
|
44
|
-
return if @quiet || blank?(text)
|
|
45
|
-
|
|
46
|
-
stop_dots
|
|
47
|
-
$stdout.puts text
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def newline(n: 1)
|
|
51
|
-
n.times { $stdout.puts("") }
|
|
52
|
-
end
|
|
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
|
-
|
|
60
|
-
def waiting
|
|
61
|
-
return if @quiet
|
|
62
|
-
|
|
63
|
-
@dots_thread = Thread.new do
|
|
64
|
-
loop do
|
|
65
|
-
$stdout.print "."
|
|
66
|
-
$stdout.flush
|
|
67
|
-
sleep 0.1
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def blank?(text)
|
|
75
|
-
text.nil? || text.to_s.strip.empty?
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def stop_dots
|
|
79
|
-
return unless @dots_thread
|
|
80
|
-
|
|
81
|
-
@dots_thread.kill
|
|
82
|
-
@dots_thread = nil
|
|
83
|
-
newline
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def setup_completion
|
|
87
|
-
Reline.autocompletion = true
|
|
88
|
-
Reline.completion_proc = ->(target, preposing) { complete(target, preposing) }
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def complete(target, preposing)
|
|
92
|
-
line = "#{preposing}#{target}"
|
|
93
|
-
return @commands.select { |c| c.start_with?(line) } if line.start_with?("/") && !preposing.include?(" ")
|
|
94
|
-
|
|
95
|
-
complete_files(target)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def complete_files(target)
|
|
99
|
-
result = Elelem.sh("bash", args: ["-c", "compgen -f #{target}"])
|
|
100
|
-
result[:content].lines.map(&:strip).first(20)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|