elelem 0.8.0 → 0.9.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 +71 -3
- data/README.md +31 -46
- data/exe/elelem +78 -3
- data/lib/elelem/agent.rb +135 -228
- data/lib/elelem/mcp.rb +96 -0
- data/lib/elelem/net/claude.rb +200 -0
- data/lib/elelem/net/ollama.rb +78 -0
- data/lib/elelem/net/openai.rb +86 -0
- data/lib/elelem/net.rb +16 -0
- data/lib/elelem/plugins/confirm.rb +12 -0
- data/lib/elelem/plugins/edit.rb +15 -0
- data/lib/elelem/plugins/eval.rb +20 -0
- data/lib/elelem/plugins/execute.rb +18 -0
- data/lib/elelem/plugins/mcp.rb +14 -0
- data/lib/elelem/plugins/read.rb +21 -0
- data/lib/elelem/plugins/verify.rb +47 -0
- data/lib/elelem/plugins/write.rb +23 -0
- data/lib/elelem/plugins.rb +43 -0
- data/lib/elelem/system_prompt.rb +65 -0
- data/lib/elelem/templates/system_prompt.erb +53 -0
- data/lib/elelem/terminal.rb +55 -65
- data/lib/elelem/tool.rb +30 -32
- data/lib/elelem/toolbox.rb +36 -94
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +28 -36
- metadata +39 -61
- data/lib/elelem/application.rb +0 -45
- data/lib/elelem/conversation.rb +0 -78
- data/lib/elelem/git_context.rb +0 -79
- data/lib/elelem/system_prompt.erb +0 -16
data/lib/elelem/agent.rb
CHANGED
|
@@ -2,277 +2,184 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Agent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
COMMANDS = %w[/clear /context /init /reload /shell /exit /help].freeze
|
|
6
|
+
MAX_CONTEXT_MESSAGES = 50
|
|
7
|
+
INIT_PROMPT = <<~PROMPT
|
|
8
|
+
AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
|
|
9
|
+
|
|
10
|
+
# AGENTS.md Spec (https://agents.md/)
|
|
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)
|
|
32
|
+
@client = client
|
|
17
33
|
@toolbox = toolbox
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@
|
|
34
|
+
@terminal = terminal || Terminal.new(commands: COMMANDS)
|
|
35
|
+
@history = history || []
|
|
36
|
+
@system_prompt = system_prompt
|
|
37
|
+
@memory = nil
|
|
38
|
+
register_task_tool
|
|
21
39
|
end
|
|
22
40
|
|
|
23
41
|
def repl
|
|
42
|
+
terminal.say "elelem v#{VERSION}"
|
|
24
43
|
loop do
|
|
25
|
-
input = terminal.ask("
|
|
44
|
+
input = terminal.ask("> ")
|
|
26
45
|
break if input.nil?
|
|
27
|
-
if input.
|
|
28
|
-
|
|
29
|
-
else
|
|
30
|
-
conversation.add(role: :user, content: input)
|
|
31
|
-
result = execute_turn(conversation.history_for(permissions))
|
|
32
|
-
conversation.add(role: result[:role], content: result[:content])
|
|
33
|
-
end
|
|
46
|
+
next if input.empty?
|
|
47
|
+
input.start_with?("/") ? command(input) : turn(input)
|
|
34
48
|
end
|
|
35
49
|
end
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def default_terminal
|
|
40
|
-
Terminal.new(
|
|
41
|
-
commands: COMMANDS,
|
|
42
|
-
env_vars: ENV_VARS,
|
|
43
|
-
modes: MODES,
|
|
44
|
-
providers: PROVIDERS
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def handle_slash_command(input)
|
|
51
|
+
def command(input)
|
|
49
52
|
case input
|
|
50
|
-
when "/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
when "/mode build"
|
|
54
|
-
permissions.replace([:read, :write])
|
|
55
|
-
terminal.say " → Mode: build (read + write)"
|
|
56
|
-
when "/mode plan"
|
|
57
|
-
permissions.replace([:read])
|
|
58
|
-
terminal.say " → Mode: plan (read-only)"
|
|
59
|
-
when "/mode verify"
|
|
60
|
-
permissions.replace([:read, :execute])
|
|
61
|
-
terminal.say " → Mode: verify (read + execute)"
|
|
62
|
-
when "/mode"
|
|
63
|
-
terminal.say " Usage: /mode [auto|build|plan|verify]"
|
|
64
|
-
terminal.say ""
|
|
65
|
-
terminal.say " Provider: #{provider}/#{client.model}"
|
|
66
|
-
terminal.say " Permissions: #{permissions.to_a.inspect}"
|
|
67
|
-
terminal.say " Tools: #{toolbox.tools_for(permissions).map { |t| t.dig(:function, :name) }}"
|
|
68
|
-
when "/exit" then exit
|
|
69
|
-
when "/clear"
|
|
70
|
-
conversation.clear
|
|
71
|
-
terminal.say " → Conversation cleared"
|
|
72
|
-
when "/context"
|
|
73
|
-
terminal.say conversation.dump(permissions)
|
|
53
|
+
when "/exit" then exit(0)
|
|
54
|
+
when "/init" then init_agents_md
|
|
55
|
+
when "/reload" then reload_source!
|
|
74
56
|
when "/shell"
|
|
75
57
|
transcript = start_shell
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
end
|
|
84
|
-
when "/model"
|
|
85
|
-
terminal.select("Model?", models_for(provider)) do |m|
|
|
86
|
-
switch_model(m)
|
|
87
|
-
end
|
|
88
|
-
when "/env"
|
|
89
|
-
terminal.say " Usage: /env VAR cmd..."
|
|
90
|
-
terminal.say ""
|
|
91
|
-
ENV_VARS.each do |var|
|
|
92
|
-
value = ENV[var]
|
|
93
|
-
if value
|
|
94
|
-
masked = value.length > 8 ? "#{value[0..3]}...#{value[-4..]}" : "****"
|
|
95
|
-
terminal.say " #{var}=#{masked}"
|
|
96
|
-
else
|
|
97
|
-
terminal.say " #{var}=(not set)"
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
when %r{^/env\s+(\w+)\s+(.+)$}
|
|
101
|
-
var_name = $1
|
|
102
|
-
command = $2
|
|
103
|
-
result = Elelem.shell.execute("sh", args: ["-c", command])
|
|
104
|
-
if result["exit_status"].zero?
|
|
105
|
-
value = result["stdout"].lines.first&.strip
|
|
106
|
-
if value && !value.empty?
|
|
107
|
-
ENV[var_name] = value
|
|
108
|
-
terminal.say " → Set #{var_name}"
|
|
109
|
-
else
|
|
110
|
-
terminal.say " ⚠ Command produced no output"
|
|
111
|
-
end
|
|
112
|
-
else
|
|
113
|
-
terminal.say " ⚠ Command failed: #{result['stderr']}"
|
|
114
|
-
end
|
|
58
|
+
history << { role: "user", content: transcript } unless transcript.strip.empty?
|
|
59
|
+
when "/clear"
|
|
60
|
+
@history = []
|
|
61
|
+
@memory = nil
|
|
62
|
+
terminal.say " → context cleared"
|
|
63
|
+
when "/context"
|
|
64
|
+
terminal.say JSON.pretty_generate(combined_history)
|
|
115
65
|
else
|
|
116
|
-
terminal.say
|
|
66
|
+
terminal.say COMMANDS.join(" ")
|
|
117
67
|
end
|
|
118
68
|
end
|
|
119
69
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.gsub(/[\b]/, '')
|
|
126
|
-
.gsub(/\r/, '')
|
|
127
|
-
end
|
|
70
|
+
def turn(input)
|
|
71
|
+
compact_if_needed
|
|
72
|
+
history << { role: "user", content: input }
|
|
73
|
+
ctx = []
|
|
74
|
+
content = nil
|
|
128
75
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
76
|
+
loop do
|
|
77
|
+
terminal.waiting
|
|
78
|
+
content, tool_calls = fetch_response(ctx)
|
|
79
|
+
terminal.say(terminal.markdown(content))
|
|
80
|
+
break if tool_calls.empty?
|
|
81
|
+
|
|
82
|
+
ctx << { role: "assistant", content: content, tool_calls: tool_calls }.compact
|
|
83
|
+
tool_calls.each do |tool_call|
|
|
84
|
+
ctx << { role: "tool", tool_call_id: tool_call[:id], content: process(tool_call).to_json }
|
|
85
|
+
end
|
|
133
86
|
end
|
|
134
|
-
end
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/env VAR cmd...
|
|
139
|
-
/mode auto build plan verify
|
|
140
|
-
/provider
|
|
141
|
-
/model
|
|
142
|
-
/shell
|
|
143
|
-
/clear
|
|
144
|
-
/context
|
|
145
|
-
/exit
|
|
146
|
-
/help
|
|
147
|
-
HELP
|
|
88
|
+
history << { role: "assistant", content: content }
|
|
89
|
+
content
|
|
148
90
|
end
|
|
149
91
|
|
|
150
|
-
|
|
151
|
-
model_opts = model ? { model: model } : {}
|
|
152
|
-
|
|
153
|
-
case provider_name
|
|
154
|
-
when "ollama" then Net::Llm::Ollama.new(**model_opts)
|
|
155
|
-
when "anthropic" then Net::Llm::Anthropic.new(**model_opts)
|
|
156
|
-
when "openai" then Net::Llm::OpenAI.new(**model_opts)
|
|
157
|
-
when "vertex-ai" then Net::Llm::VertexAI.new(**model_opts)
|
|
158
|
-
else
|
|
159
|
-
raise Error, "Unknown provider: #{provider_name}"
|
|
160
|
-
end
|
|
161
|
-
end
|
|
92
|
+
private
|
|
162
93
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
94
|
+
def process(tool_call)
|
|
95
|
+
name, args = tool_call[:name], tool_call[:arguments]
|
|
96
|
+
terminal.say toolbox.header(name, args)
|
|
97
|
+
toolbox.run(name.to_s, args)
|
|
98
|
+
end
|
|
99
|
+
|
|
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] }
|
|
177
110
|
end
|
|
178
|
-
rescue KeyError => e
|
|
179
|
-
terminal.say " ⚠ Missing credentials: #{e.message}"
|
|
180
|
-
[]
|
|
181
|
-
rescue => e
|
|
182
|
-
terminal.say " ⚠ Could not fetch models: #{e.message}"
|
|
183
|
-
[]
|
|
184
111
|
end
|
|
185
112
|
|
|
186
|
-
def
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
terminal.say " → Switched to #{new_provider}/#{client.model}"
|
|
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")
|
|
190
116
|
end
|
|
191
117
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
126
|
end
|
|
196
127
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
""
|
|
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
|
|
204
133
|
end
|
|
205
134
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
def fetch_response(ctx)
|
|
145
|
+
content = ""
|
|
146
|
+
tool_calls = client.fetch(combined_history + ctx, toolbox.to_a) do |delta|
|
|
147
|
+
content += delta[:content].to_s
|
|
148
|
+
terminal.print(terminal.think(delta[:thinking])) if delta[:thinking]
|
|
214
149
|
end
|
|
150
|
+
[content, tool_calls]
|
|
151
|
+
rescue => e
|
|
152
|
+
terminal.say "\n ✗ #{e.message}"
|
|
153
|
+
["Error: #{e.message} #{e.backtrace.join("\n")}", []]
|
|
215
154
|
end
|
|
216
155
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
args = openai_client? ? JSON.dump(tc[:arguments]) : tc[:arguments]
|
|
220
|
-
{
|
|
221
|
-
id: tc[:id],
|
|
222
|
-
type: "function",
|
|
223
|
-
function: { name: tc[:name], arguments: args }
|
|
224
|
-
}
|
|
225
|
-
end
|
|
156
|
+
def combined_history
|
|
157
|
+
[{ role: "system", content: system_prompt }] + history
|
|
226
158
|
end
|
|
227
159
|
|
|
228
|
-
def
|
|
229
|
-
|
|
160
|
+
def system_prompt
|
|
161
|
+
@system_prompt || SystemPrompt.new(memory: @memory).render
|
|
230
162
|
end
|
|
231
163
|
|
|
232
|
-
def
|
|
233
|
-
|
|
234
|
-
turn_context = []
|
|
235
|
-
errors = 0
|
|
164
|
+
def compact_if_needed
|
|
165
|
+
return if history.length <= MAX_CONTEXT_MESSAGES
|
|
236
166
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
167
|
+
terminal.say " → compacting context"
|
|
168
|
+
keep = MAX_CONTEXT_MESSAGES / 2
|
|
169
|
+
old = history.first(history.length - keep)
|
|
240
170
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
when :delta
|
|
246
|
-
terminal.write chunk[:thinking] if chunk[:thinking]
|
|
247
|
-
content += chunk[:content] if chunk[:content]
|
|
248
|
-
when :complete
|
|
249
|
-
content = chunk[:content] if chunk[:content]
|
|
250
|
-
tool_calls = chunk[:tool_calls] || []
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
rescue => e
|
|
254
|
-
terminal.say "\n ✗ API Error: #{e.message}"
|
|
255
|
-
return { role: "assistant", content: "[Error: #{e.message}]" }
|
|
256
|
-
end
|
|
171
|
+
to_summarize = @memory ? [{ role: "memory", content: @memory }, *old] : old
|
|
172
|
+
@memory = summarize(to_summarize)
|
|
173
|
+
@history = history.last(keep)
|
|
174
|
+
end
|
|
257
175
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
turn_context << { role: "assistant", content: content, tool_calls: api_tool_calls }.compact
|
|
176
|
+
def summarize(messages)
|
|
177
|
+
text = messages.map { |message| { role: message[:role], content: message[:content] } }.to_json
|
|
261
178
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
terminal.say "\nTool> #{name}(#{args})"
|
|
266
|
-
result = toolbox.run_tool(name, args, permissions: permissions)
|
|
267
|
-
terminal.say truncate_output(format_tool_call_result(result))
|
|
268
|
-
turn_context << { role: "tool", tool_call_id: call[:id], content: JSON.dump(result) }
|
|
269
|
-
errors += 1 if result[:error]
|
|
270
|
-
end
|
|
271
|
-
return { role: "assistant", content: "[Stopped: too many errors]" } if errors >= 3
|
|
272
|
-
next
|
|
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
|
|
273
182
|
end
|
|
274
|
-
|
|
275
|
-
return { role: "assistant", content: content }
|
|
276
183
|
end
|
|
277
184
|
end
|
|
278
185
|
end
|
data/lib/elelem/mcp.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class MCP
|
|
5
|
+
def initialize(config_path = ".mcp.json")
|
|
6
|
+
@config = File.exist?(config_path) ? JSON.parse(IO.read(config_path)) : {}
|
|
7
|
+
@servers = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def tools
|
|
11
|
+
@config.fetch("mcpServers", {}).flat_map do |name, _|
|
|
12
|
+
server(name).tools.map do |tool|
|
|
13
|
+
[
|
|
14
|
+
"#{name}_#{tool["name"]}",
|
|
15
|
+
{
|
|
16
|
+
description: tool["description"],
|
|
17
|
+
params: tool.dig("inputSchema", "properties") || {},
|
|
18
|
+
required: tool.dig("inputSchema", "required") || [],
|
|
19
|
+
fn: ->(a) { server(name).call(tool["name"], a) }
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
end.to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
@servers.each_value(&:close)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def server(name)
|
|
33
|
+
@servers[name] ||= Server.new(**@config.dig("mcpServers", name).transform_keys(&:to_sym))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Server
|
|
37
|
+
def initialize(command:, args: [], env: {})
|
|
38
|
+
resolved_env = env.transform_values { |v| v.gsub(/\$\{(\w+)\}/) { ENV[$1] } }
|
|
39
|
+
@stdin, @stdout, @stderr, @wait = Open3.popen3(resolved_env, command, *args)
|
|
40
|
+
@id = 0
|
|
41
|
+
initialize!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tools
|
|
45
|
+
request("tools/list")["tools"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(name, args)
|
|
49
|
+
result = request("tools/call", { name: name, arguments: args })
|
|
50
|
+
{ content: result["content"]&.map { |c| c["text"] }&.join("\n") }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def close
|
|
54
|
+
@stdin.close rescue nil
|
|
55
|
+
@stdout.close rescue nil
|
|
56
|
+
@stderr.close rescue nil
|
|
57
|
+
@wait.kill rescue nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def initialize!
|
|
63
|
+
request("initialize", {
|
|
64
|
+
protocolVersion: "2024-11-05",
|
|
65
|
+
capabilities: {},
|
|
66
|
+
clientInfo: { name: "elelem", version: VERSION }
|
|
67
|
+
})
|
|
68
|
+
notify("notifications/initialized")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def request(method, params = {})
|
|
72
|
+
send_msg(id: @id += 1, method: method, params: params)
|
|
73
|
+
read_response(@id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def notify(method, params = {})
|
|
77
|
+
send_msg(method: method, params: params)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def send_msg(msg)
|
|
81
|
+
@stdin.puts({ jsonrpc: "2.0", **msg }.to_json)
|
|
82
|
+
@stdin.flush
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_response(id)
|
|
86
|
+
loop do
|
|
87
|
+
line = @stdout.gets
|
|
88
|
+
raise "Server closed" unless line
|
|
89
|
+
msg = JSON.parse(line)
|
|
90
|
+
return msg["result"] if msg["id"] == id
|
|
91
|
+
raise msg["error"]["message"] if msg["error"]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|