rubyn-code 0.1.0 → 0.2.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 +269 -467
- data/db/migrations/009_create_teams.sql +6 -6
- data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
- data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
- data/exe/rubyn-code +1 -1
- data/lib/rubyn_code/agent/RUBYN.md +17 -0
- data/lib/rubyn_code/agent/conversation.rb +68 -19
- data/lib/rubyn_code/agent/loop.rb +312 -54
- data/lib/rubyn_code/agent/loop_detector.rb +6 -6
- data/lib/rubyn_code/auth/RUBYN.md +19 -0
- data/lib/rubyn_code/auth/oauth.rb +40 -35
- data/lib/rubyn_code/auth/server.rb +16 -12
- data/lib/rubyn_code/auth/token_store.rb +22 -22
- data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
- data/lib/rubyn_code/autonomous/daemon.rb +115 -79
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
- data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
- data/lib/rubyn_code/background/RUBYN.md +13 -0
- data/lib/rubyn_code/background/notifier.rb +0 -2
- data/lib/rubyn_code/background/worker.rb +60 -15
- data/lib/rubyn_code/cli/RUBYN.md +30 -0
- data/lib/rubyn_code/cli/app.rb +85 -9
- data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
- data/lib/rubyn_code/cli/commands/base.rb +53 -0
- data/lib/rubyn_code/cli/commands/budget.rb +24 -0
- data/lib/rubyn_code/cli/commands/clear.rb +16 -0
- data/lib/rubyn_code/cli/commands/compact.rb +21 -0
- data/lib/rubyn_code/cli/commands/context.rb +44 -0
- data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
- data/lib/rubyn_code/cli/commands/cost.rb +23 -0
- data/lib/rubyn_code/cli/commands/diff.rb +30 -0
- data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
- data/lib/rubyn_code/cli/commands/help.rb +41 -0
- data/lib/rubyn_code/cli/commands/model.rb +37 -0
- data/lib/rubyn_code/cli/commands/plan.rb +22 -0
- data/lib/rubyn_code/cli/commands/quit.rb +17 -0
- data/lib/rubyn_code/cli/commands/registry.rb +64 -0
- data/lib/rubyn_code/cli/commands/resume.rb +51 -0
- data/lib/rubyn_code/cli/commands/review.rb +26 -0
- data/lib/rubyn_code/cli/commands/skill.rb +32 -0
- data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
- data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
- data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
- data/lib/rubyn_code/cli/commands/undo.rb +17 -0
- data/lib/rubyn_code/cli/commands/version.rb +16 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
- data/lib/rubyn_code/cli/input_handler.rb +20 -23
- data/lib/rubyn_code/cli/renderer.rb +25 -27
- data/lib/rubyn_code/cli/repl.rb +161 -194
- data/lib/rubyn_code/cli/setup.rb +117 -0
- data/lib/rubyn_code/cli/spinner.rb +40 -40
- data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
- data/lib/rubyn_code/cli/version_check.rb +94 -0
- data/lib/rubyn_code/config/RUBYN.md +14 -0
- data/lib/rubyn_code/config/defaults.rb +28 -19
- data/lib/rubyn_code/config/project_config.rb +7 -9
- data/lib/rubyn_code/config/settings.rb +3 -3
- data/lib/rubyn_code/context/RUBYN.md +20 -0
- data/lib/rubyn_code/context/auto_compact.rb +7 -7
- data/lib/rubyn_code/context/compactor.rb +2 -2
- data/lib/rubyn_code/context/context_collapse.rb +45 -0
- data/lib/rubyn_code/context/manager.rb +20 -3
- data/lib/rubyn_code/context/manual_compact.rb +7 -7
- data/lib/rubyn_code/context/micro_compact.rb +12 -12
- data/lib/rubyn_code/db/RUBYN.md +40 -0
- data/lib/rubyn_code/db/connection.rb +13 -13
- data/lib/rubyn_code/db/migrator.rb +67 -27
- data/lib/rubyn_code/db/schema.rb +6 -6
- data/lib/rubyn_code/debug.rb +74 -0
- data/lib/rubyn_code/hooks/RUBYN.md +17 -0
- data/lib/rubyn_code/hooks/built_in.rb +9 -9
- data/lib/rubyn_code/hooks/registry.rb +5 -5
- data/lib/rubyn_code/hooks/runner.rb +1 -1
- data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
- data/lib/rubyn_code/learning/RUBYN.md +16 -0
- data/lib/rubyn_code/learning/extractor.rb +22 -22
- data/lib/rubyn_code/learning/injector.rb +17 -18
- data/lib/rubyn_code/learning/instinct.rb +18 -14
- data/lib/rubyn_code/llm/RUBYN.md +15 -0
- data/lib/rubyn_code/llm/client.rb +121 -55
- data/lib/rubyn_code/llm/message_builder.rb +19 -15
- data/lib/rubyn_code/llm/streaming.rb +80 -50
- data/lib/rubyn_code/mcp/RUBYN.md +21 -0
- data/lib/rubyn_code/mcp/client.rb +25 -24
- data/lib/rubyn_code/mcp/config.rb +7 -7
- data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
- data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
- data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
- data/lib/rubyn_code/memory/RUBYN.md +17 -0
- data/lib/rubyn_code/memory/models.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +17 -17
- data/lib/rubyn_code/memory/session_persistence.rb +49 -34
- data/lib/rubyn_code/memory/store.rb +17 -17
- data/lib/rubyn_code/observability/RUBYN.md +19 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
- data/lib/rubyn_code/observability/token_counter.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
- data/lib/rubyn_code/output/RUBYN.md +11 -0
- data/lib/rubyn_code/output/diff_renderer.rb +6 -6
- data/lib/rubyn_code/output/formatter.rb +4 -4
- data/lib/rubyn_code/permissions/RUBYN.md +17 -0
- data/lib/rubyn_code/permissions/prompter.rb +8 -8
- data/lib/rubyn_code/protocols/RUBYN.md +14 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
- data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
- data/lib/rubyn_code/skills/RUBYN.md +19 -0
- data/lib/rubyn_code/skills/catalog.rb +7 -7
- data/lib/rubyn_code/skills/document.rb +15 -15
- data/lib/rubyn_code/skills/loader.rb +6 -8
- data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
- data/lib/rubyn_code/sub_agents/runner.rb +15 -15
- data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
- data/lib/rubyn_code/tasks/RUBYN.md +13 -0
- data/lib/rubyn_code/tasks/dag.rb +12 -16
- data/lib/rubyn_code/tasks/manager.rb +24 -24
- data/lib/rubyn_code/tasks/models.rb +4 -4
- data/lib/rubyn_code/teams/RUBYN.md +14 -0
- data/lib/rubyn_code/teams/mailbox.rb +38 -18
- data/lib/rubyn_code/teams/manager.rb +19 -19
- data/lib/rubyn_code/teams/teammate.rb +3 -4
- data/lib/rubyn_code/tools/RUBYN.md +38 -0
- data/lib/rubyn_code/tools/background_run.rb +9 -11
- data/lib/rubyn_code/tools/base.rb +54 -3
- data/lib/rubyn_code/tools/bash.rb +16 -34
- data/lib/rubyn_code/tools/bundle_add.rb +10 -12
- data/lib/rubyn_code/tools/bundle_install.rb +9 -11
- data/lib/rubyn_code/tools/compact.rb +10 -9
- data/lib/rubyn_code/tools/db_migrate.rb +17 -15
- data/lib/rubyn_code/tools/edit_file.rb +12 -12
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/git_commit.rb +29 -34
- data/lib/rubyn_code/tools/git_diff.rb +17 -18
- data/lib/rubyn_code/tools/git_log.rb +17 -19
- data/lib/rubyn_code/tools/git_status.rb +18 -20
- data/lib/rubyn_code/tools/glob.rb +7 -9
- data/lib/rubyn_code/tools/grep.rb +11 -9
- data/lib/rubyn_code/tools/load_skill.rb +7 -7
- data/lib/rubyn_code/tools/memory_search.rb +13 -12
- data/lib/rubyn_code/tools/memory_write.rb +14 -12
- data/lib/rubyn_code/tools/rails_generate.rb +16 -16
- data/lib/rubyn_code/tools/read_file.rb +8 -7
- data/lib/rubyn_code/tools/read_inbox.rb +5 -5
- data/lib/rubyn_code/tools/registry.rb +2 -2
- data/lib/rubyn_code/tools/review_pr.rb +55 -55
- data/lib/rubyn_code/tools/run_specs.rb +20 -19
- data/lib/rubyn_code/tools/schema.rb +9 -11
- data/lib/rubyn_code/tools/send_message.rb +10 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
- data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
- data/lib/rubyn_code/tools/task.rb +28 -28
- data/lib/rubyn_code/tools/web_fetch.rb +46 -31
- data/lib/rubyn_code/tools/web_search.rb +64 -66
- data/lib/rubyn_code/tools/write_file.rb +7 -6
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +136 -105
- metadata +94 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'json'
|
|
4
4
|
|
|
5
5
|
module RubynCode
|
|
6
6
|
module MCP
|
|
@@ -9,7 +9,8 @@ module RubynCode
|
|
|
9
9
|
class Client
|
|
10
10
|
INITIALIZE_TIMEOUT = 10
|
|
11
11
|
|
|
12
|
-
ClientError
|
|
12
|
+
class ClientError < RubynCode::Error
|
|
13
|
+
end
|
|
13
14
|
|
|
14
15
|
attr_reader :name, :transport
|
|
15
16
|
|
|
@@ -41,7 +42,7 @@ module RubynCode
|
|
|
41
42
|
#
|
|
42
43
|
# @return [Array<Hash>] tool definitions in JSON Schema format
|
|
43
44
|
def tools
|
|
44
|
-
@
|
|
45
|
+
@tools ||= discover_tools
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Invokes a tool on the MCP server.
|
|
@@ -53,10 +54,10 @@ module RubynCode
|
|
|
53
54
|
def call_tool(tool_name, arguments = {})
|
|
54
55
|
ensure_connected!
|
|
55
56
|
|
|
56
|
-
@transport.send_request(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
@transport.send_request('tools/call', {
|
|
58
|
+
name: tool_name,
|
|
59
|
+
arguments: arguments
|
|
60
|
+
})
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
# Gracefully disconnects from the MCP server.
|
|
@@ -107,28 +108,28 @@ module RubynCode
|
|
|
107
108
|
private
|
|
108
109
|
|
|
109
110
|
def perform_initialize
|
|
110
|
-
result = @transport.send_request(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@server_info = result&.dig(
|
|
122
|
-
@server_capabilities = result&.dig(
|
|
123
|
-
|
|
124
|
-
@transport.send_notification(
|
|
111
|
+
result = @transport.send_request('initialize', {
|
|
112
|
+
protocolVersion: '2024-11-05',
|
|
113
|
+
capabilities: {
|
|
114
|
+
tools: {}
|
|
115
|
+
},
|
|
116
|
+
clientInfo: {
|
|
117
|
+
name: 'rubyn-code',
|
|
118
|
+
version: RubynCode::VERSION
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
@server_info = result&.dig('serverInfo')
|
|
123
|
+
@server_capabilities = result&.dig('capabilities')
|
|
124
|
+
|
|
125
|
+
@transport.send_notification('notifications/initialized') if @transport.respond_to?(:send_notification)
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def discover_tools
|
|
128
129
|
ensure_connected!
|
|
129
130
|
|
|
130
|
-
result = @transport.send_request(
|
|
131
|
-
result&.fetch(
|
|
131
|
+
result = @transport.send_request('tools/list')
|
|
132
|
+
result&.fetch('tools', []) || []
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
def ensure_connected!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'json'
|
|
4
4
|
|
|
5
5
|
module RubynCode
|
|
6
6
|
module MCP
|
|
@@ -17,7 +17,7 @@ module RubynCode
|
|
|
17
17
|
# }
|
|
18
18
|
# }
|
|
19
19
|
module Config
|
|
20
|
-
CONFIG_FILENAME =
|
|
20
|
+
CONFIG_FILENAME = '.rubyn-code/mcp.json'
|
|
21
21
|
|
|
22
22
|
ENV_VAR_PATTERN = /\$\{([^}]+)\}/
|
|
23
23
|
|
|
@@ -32,14 +32,14 @@ module RubynCode
|
|
|
32
32
|
|
|
33
33
|
raw = File.read(config_path)
|
|
34
34
|
data = JSON.parse(raw)
|
|
35
|
-
servers = data[
|
|
35
|
+
servers = data['mcpServers'] || {}
|
|
36
36
|
|
|
37
37
|
servers.map do |name, server_def|
|
|
38
38
|
{
|
|
39
39
|
name: name,
|
|
40
|
-
command: server_def[
|
|
41
|
-
args: Array(server_def[
|
|
42
|
-
env: expand_env(server_def[
|
|
40
|
+
command: server_def['command'],
|
|
41
|
+
args: Array(server_def['args']),
|
|
42
|
+
env: expand_env(server_def['env'] || {})
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
rescue JSON::ParserError => e
|
|
@@ -73,7 +73,7 @@ module RubynCode
|
|
|
73
73
|
env_name = ::Regexp.last_match(1)
|
|
74
74
|
ENV.fetch(env_name) do
|
|
75
75
|
warn "[MCP::Config] Environment variable #{env_name} is not set"
|
|
76
|
-
|
|
76
|
+
''
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module MCP
|
|
@@ -15,8 +15,11 @@ module RubynCode
|
|
|
15
15
|
class SSETransport
|
|
16
16
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
17
17
|
|
|
18
|
-
TransportError
|
|
19
|
-
|
|
18
|
+
class TransportError < RubynCode::Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class TimeoutError < TransportError
|
|
22
|
+
end
|
|
20
23
|
|
|
21
24
|
# @param url [String] the SSE endpoint URL of the MCP server
|
|
22
25
|
# @param timeout [Integer] default timeout in seconds per request
|
|
@@ -36,7 +39,7 @@ module RubynCode
|
|
|
36
39
|
# @return [void]
|
|
37
40
|
# @raise [TransportError] if the connection cannot be established
|
|
38
41
|
def start!
|
|
39
|
-
raise TransportError,
|
|
42
|
+
raise TransportError, 'Transport already started' if @connected
|
|
40
43
|
|
|
41
44
|
@pending_responses = {}
|
|
42
45
|
@sse_thread = Thread.new { run_sse_listener }
|
|
@@ -61,14 +64,14 @@ module RubynCode
|
|
|
61
64
|
# @raise [TransportError] on protocol or server errors
|
|
62
65
|
# @raise [TimeoutError] if the response is not received in time
|
|
63
66
|
def send_request(method, params = {})
|
|
64
|
-
raise TransportError,
|
|
67
|
+
raise TransportError, 'Transport is not connected' unless @connected
|
|
65
68
|
|
|
66
69
|
id = next_request_id
|
|
67
70
|
queue = Queue.new
|
|
68
71
|
@mutex.synchronize { @pending_responses[id] = queue }
|
|
69
72
|
|
|
70
73
|
request = {
|
|
71
|
-
jsonrpc:
|
|
74
|
+
jsonrpc: '2.0',
|
|
72
75
|
id: id,
|
|
73
76
|
method: method,
|
|
74
77
|
params: params
|
|
@@ -111,7 +114,7 @@ module RubynCode
|
|
|
111
114
|
@connection ||= Faraday.new(url: base_url) do |f|
|
|
112
115
|
f.options.timeout = @timeout
|
|
113
116
|
f.options.open_timeout = @timeout
|
|
114
|
-
f.headers[
|
|
117
|
+
f.headers['Content-Type'] = 'application/json'
|
|
115
118
|
f.adapter Faraday.default_adapter
|
|
116
119
|
end
|
|
117
120
|
end
|
|
@@ -121,9 +124,7 @@ module RubynCode
|
|
|
121
124
|
req.body = JSON.generate(request)
|
|
122
125
|
end
|
|
123
126
|
|
|
124
|
-
unless response.success?
|
|
125
|
-
raise TransportError, "MCP server returned HTTP #{response.status}: #{response.body}"
|
|
126
|
-
end
|
|
127
|
+
raise TransportError, "MCP server returned HTTP #{response.status}: #{response.body}" unless response.success?
|
|
127
128
|
rescue Faraday::Error => e
|
|
128
129
|
raise TransportError, "Failed to send request to MCP server: #{e.message}"
|
|
129
130
|
end
|
|
@@ -138,8 +139,8 @@ module RubynCode
|
|
|
138
139
|
@mutex.synchronize { @pending_responses.delete(id) }
|
|
139
140
|
end
|
|
140
141
|
|
|
141
|
-
if result.is_a?(Hash) && result.key?(
|
|
142
|
-
err = result[
|
|
142
|
+
if result.is_a?(Hash) && result.key?('error')
|
|
143
|
+
err = result['error']
|
|
143
144
|
raise TransportError, "MCP error (#{err['code']}): #{err['message']}"
|
|
144
145
|
end
|
|
145
146
|
|
|
@@ -150,11 +151,11 @@ module RubynCode
|
|
|
150
151
|
sse_connection = Faraday.new(url: base_url) do |f|
|
|
151
152
|
f.options.timeout = nil # Keep-alive
|
|
152
153
|
f.options.open_timeout = @timeout
|
|
153
|
-
f.headers[
|
|
154
|
+
f.headers['Accept'] = 'text/event-stream'
|
|
154
155
|
f.adapter Faraday.default_adapter
|
|
155
156
|
end
|
|
156
157
|
|
|
157
|
-
buffer = +
|
|
158
|
+
buffer = +''
|
|
158
159
|
|
|
159
160
|
sse_connection.get(@url) do |req|
|
|
160
161
|
req.options.on_data = proc do |chunk, _bytes, _env|
|
|
@@ -181,10 +182,10 @@ module RubynCode
|
|
|
181
182
|
|
|
182
183
|
raw.each_line do |line|
|
|
183
184
|
line = line.chomp
|
|
184
|
-
if line.start_with?(
|
|
185
|
-
event_type = line.sub(
|
|
186
|
-
elsif line.start_with?(
|
|
187
|
-
data_lines << line.sub(
|
|
185
|
+
if line.start_with?('event:')
|
|
186
|
+
event_type = line.sub('event:', '').strip
|
|
187
|
+
elsif line.start_with?('data:')
|
|
188
|
+
data_lines << line.sub('data:', '').strip
|
|
188
189
|
end
|
|
189
190
|
end
|
|
190
191
|
|
|
@@ -195,9 +196,9 @@ module RubynCode
|
|
|
195
196
|
|
|
196
197
|
def handle_sse_event(event)
|
|
197
198
|
case event[:type]
|
|
198
|
-
when
|
|
199
|
+
when 'endpoint'
|
|
199
200
|
@post_endpoint = event[:data]
|
|
200
|
-
when
|
|
201
|
+
when 'message'
|
|
201
202
|
dispatch_message(event[:data])
|
|
202
203
|
else
|
|
203
204
|
dispatch_message(event[:data])
|
|
@@ -206,16 +207,16 @@ module RubynCode
|
|
|
206
207
|
|
|
207
208
|
def dispatch_message(data)
|
|
208
209
|
message = JSON.parse(data)
|
|
209
|
-
return unless message.is_a?(Hash) && message.key?(
|
|
210
|
+
return unless message.is_a?(Hash) && message.key?('id')
|
|
210
211
|
|
|
211
|
-
id = message[
|
|
212
|
+
id = message['id']
|
|
212
213
|
queue = @mutex.synchronize { @pending_responses[id] }
|
|
213
214
|
return unless queue
|
|
214
215
|
|
|
215
|
-
if message.key?(
|
|
216
|
+
if message.key?('error')
|
|
216
217
|
queue.push(message)
|
|
217
218
|
else
|
|
218
|
-
queue.push(message[
|
|
219
|
+
queue.push(message['result'])
|
|
219
220
|
end
|
|
220
221
|
rescue JSON::ParserError
|
|
221
222
|
# Ignore malformed messages
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'timeout'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module MCP
|
|
@@ -14,8 +14,11 @@ module RubynCode
|
|
|
14
14
|
class StdioTransport
|
|
15
15
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
16
16
|
|
|
17
|
-
TransportError
|
|
18
|
-
|
|
17
|
+
class TransportError < RubynCode::Error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class TimeoutError < TransportError
|
|
21
|
+
end
|
|
19
22
|
|
|
20
23
|
# @param command [String] executable to spawn
|
|
21
24
|
# @param args [Array<String>] arguments for the command
|
|
@@ -39,7 +42,7 @@ module RubynCode
|
|
|
39
42
|
# @return [void]
|
|
40
43
|
# @raise [TransportError] if the process fails to start
|
|
41
44
|
def start!
|
|
42
|
-
raise TransportError,
|
|
45
|
+
raise TransportError, 'Transport already started' if alive?
|
|
43
46
|
|
|
44
47
|
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@env, @command, *@args)
|
|
45
48
|
rescue Errno::ENOENT => e
|
|
@@ -54,11 +57,11 @@ module RubynCode
|
|
|
54
57
|
# @raise [TransportError] on protocol or server errors
|
|
55
58
|
# @raise [TimeoutError] if the response is not received within the timeout
|
|
56
59
|
def send_request(method, params = {})
|
|
57
|
-
raise TransportError,
|
|
60
|
+
raise TransportError, 'Transport is not running' unless alive?
|
|
58
61
|
|
|
59
62
|
id = next_request_id
|
|
60
63
|
request = {
|
|
61
|
-
jsonrpc:
|
|
64
|
+
jsonrpc: '2.0',
|
|
62
65
|
id: id,
|
|
63
66
|
method: method,
|
|
64
67
|
params: params
|
|
@@ -74,10 +77,10 @@ module RubynCode
|
|
|
74
77
|
# @param params [Hash] parameters for the notification
|
|
75
78
|
# @return [void]
|
|
76
79
|
def send_notification(method, params = {})
|
|
77
|
-
raise TransportError,
|
|
80
|
+
raise TransportError, 'Transport is not running' unless alive?
|
|
78
81
|
|
|
79
82
|
notification = {
|
|
80
|
-
jsonrpc:
|
|
83
|
+
jsonrpc: '2.0',
|
|
81
84
|
method: method,
|
|
82
85
|
params: params
|
|
83
86
|
}
|
|
@@ -92,7 +95,7 @@ module RubynCode
|
|
|
92
95
|
return unless alive?
|
|
93
96
|
|
|
94
97
|
begin
|
|
95
|
-
send_notification(
|
|
98
|
+
send_notification('notifications/cancelled')
|
|
96
99
|
@stdin&.close
|
|
97
100
|
rescue IOError, Errno::EPIPE
|
|
98
101
|
# Process may already be gone
|
|
@@ -138,7 +141,7 @@ module RubynCode
|
|
|
138
141
|
Timeout.timeout(@timeout, TimeoutError, "MCP server did not respond within #{@timeout}s") do
|
|
139
142
|
loop do
|
|
140
143
|
line = @stdout.gets
|
|
141
|
-
raise TransportError,
|
|
144
|
+
raise TransportError, 'MCP server closed stdout unexpectedly' if line.nil?
|
|
142
145
|
|
|
143
146
|
line = line.strip
|
|
144
147
|
next if line.empty?
|
|
@@ -147,17 +150,17 @@ module RubynCode
|
|
|
147
150
|
next unless message
|
|
148
151
|
|
|
149
152
|
# Skip notifications (no id field)
|
|
150
|
-
next unless message.key?(
|
|
153
|
+
next unless message.key?('id')
|
|
151
154
|
|
|
152
155
|
# Skip responses for other requests
|
|
153
|
-
next unless message[
|
|
156
|
+
next unless message['id'] == expected_id
|
|
154
157
|
|
|
155
|
-
if message.key?(
|
|
156
|
-
err = message[
|
|
158
|
+
if message.key?('error')
|
|
159
|
+
err = message['error']
|
|
157
160
|
raise TransportError, "MCP error (#{err['code']}): #{err['message']}"
|
|
158
161
|
end
|
|
159
162
|
|
|
160
|
-
return message[
|
|
163
|
+
return message['result']
|
|
161
164
|
end
|
|
162
165
|
end
|
|
163
166
|
end
|
|
@@ -172,9 +175,9 @@ module RubynCode
|
|
|
172
175
|
return unless @wait_thread
|
|
173
176
|
|
|
174
177
|
pid = @wait_thread.pid
|
|
175
|
-
Process.kill(
|
|
178
|
+
Process.kill('TERM', pid)
|
|
176
179
|
sleep(0.5)
|
|
177
|
-
Process.kill(
|
|
180
|
+
Process.kill('KILL', pid) if @wait_thread.alive?
|
|
178
181
|
rescue Errno::ESRCH, Errno::EPERM
|
|
179
182
|
# Process already gone or we lack permissions
|
|
180
183
|
end
|
|
@@ -34,10 +34,10 @@ module RubynCode
|
|
|
34
34
|
# @param tool_def [Hash] tool definition with "name", "description", "inputSchema"
|
|
35
35
|
# @return [Class] the newly created and registered tool class
|
|
36
36
|
def build_tool_class(mcp_client, tool_def)
|
|
37
|
-
remote_name = tool_def[
|
|
37
|
+
remote_name = tool_def['name']
|
|
38
38
|
tool_name = "mcp_#{sanitize_name(remote_name)}"
|
|
39
|
-
description = tool_def[
|
|
40
|
-
input_schema = tool_def[
|
|
39
|
+
description = tool_def['description'] || "MCP tool: #{remote_name}"
|
|
40
|
+
input_schema = tool_def['inputSchema'] || {}
|
|
41
41
|
parameters = build_parameters_from_schema(input_schema)
|
|
42
42
|
|
|
43
43
|
klass = Class.new(Tools::Base) do
|
|
@@ -60,8 +60,8 @@ module RubynCode
|
|
|
60
60
|
define_method(:format_result) do |result|
|
|
61
61
|
case result
|
|
62
62
|
when Hash
|
|
63
|
-
if result.key?(
|
|
64
|
-
extract_content(result[
|
|
63
|
+
if result.key?('content')
|
|
64
|
+
extract_content(result['content'])
|
|
65
65
|
else
|
|
66
66
|
JSON.generate(result)
|
|
67
67
|
end
|
|
@@ -74,13 +74,13 @@ module RubynCode
|
|
|
74
74
|
|
|
75
75
|
define_method(:extract_content) do |content|
|
|
76
76
|
Array(content).map do |block|
|
|
77
|
-
case block[
|
|
78
|
-
when
|
|
79
|
-
block[
|
|
80
|
-
when
|
|
77
|
+
case block['type']
|
|
78
|
+
when 'text'
|
|
79
|
+
block['text']
|
|
80
|
+
when 'image'
|
|
81
81
|
"[image: #{block['mimeType']}]"
|
|
82
|
-
when
|
|
83
|
-
block.dig(
|
|
82
|
+
when 'resource'
|
|
83
|
+
block.dig('resource', 'text') || "[resource: #{block.dig('resource', 'uri')}]"
|
|
84
84
|
else
|
|
85
85
|
block.to_s
|
|
86
86
|
end
|
|
@@ -90,13 +90,13 @@ module RubynCode
|
|
|
90
90
|
|
|
91
91
|
# Build parameter definitions from JSON Schema
|
|
92
92
|
klass.define_singleton_method(:build_parameters) do |schema|
|
|
93
|
-
properties = schema[
|
|
94
|
-
required = schema[
|
|
93
|
+
properties = schema['properties'] || {}
|
|
94
|
+
required = schema['required'] || []
|
|
95
95
|
|
|
96
96
|
properties.each_with_object({}) do |(name, prop), params|
|
|
97
97
|
params[name.to_sym] = {
|
|
98
|
-
type: map_json_type(prop[
|
|
99
|
-
description: prop[
|
|
98
|
+
type: map_json_type(prop['type']),
|
|
99
|
+
description: prop['description'] || '',
|
|
100
100
|
required: required.include?(name)
|
|
101
101
|
}
|
|
102
102
|
end
|
|
@@ -104,12 +104,12 @@ module RubynCode
|
|
|
104
104
|
|
|
105
105
|
klass.define_singleton_method(:map_json_type) do |json_type|
|
|
106
106
|
case json_type
|
|
107
|
-
when
|
|
108
|
-
when
|
|
109
|
-
when
|
|
110
|
-
when
|
|
111
|
-
when
|
|
112
|
-
when
|
|
107
|
+
when 'string' then :string
|
|
108
|
+
when 'integer' then :integer
|
|
109
|
+
when 'number' then :number
|
|
110
|
+
when 'boolean' then :boolean
|
|
111
|
+
when 'array' then :array
|
|
112
|
+
when 'object' then :object
|
|
113
113
|
else :string
|
|
114
114
|
end
|
|
115
115
|
end
|
|
@@ -123,13 +123,13 @@ module RubynCode
|
|
|
123
123
|
# @param schema [Hash] JSON Schema with "properties" and "required"
|
|
124
124
|
# @return [Hash]
|
|
125
125
|
def build_parameters_from_schema(schema)
|
|
126
|
-
properties = schema[
|
|
127
|
-
required = schema[
|
|
126
|
+
properties = schema['properties'] || {}
|
|
127
|
+
required = schema['required'] || []
|
|
128
128
|
|
|
129
129
|
properties.each_with_object({}) do |(name, prop), params|
|
|
130
130
|
params[name.to_sym] = {
|
|
131
|
-
type: map_json_type(prop[
|
|
132
|
-
description: prop[
|
|
131
|
+
type: map_json_type(prop['type']),
|
|
132
|
+
description: prop['description'] || '',
|
|
133
133
|
required: required.include?(name)
|
|
134
134
|
}
|
|
135
135
|
end
|
|
@@ -141,12 +141,12 @@ module RubynCode
|
|
|
141
141
|
# @return [Symbol]
|
|
142
142
|
def map_json_type(json_type)
|
|
143
143
|
case json_type
|
|
144
|
-
when
|
|
145
|
-
when
|
|
146
|
-
when
|
|
147
|
-
when
|
|
148
|
-
when
|
|
149
|
-
when
|
|
144
|
+
when 'string' then :string
|
|
145
|
+
when 'integer' then :integer
|
|
146
|
+
when 'number' then :number
|
|
147
|
+
when 'boolean' then :boolean
|
|
148
|
+
when 'array' then :array
|
|
149
|
+
when 'object' then :object
|
|
150
150
|
else :string
|
|
151
151
|
end
|
|
152
152
|
end
|
|
@@ -156,7 +156,7 @@ module RubynCode
|
|
|
156
156
|
# @param name [String] the original tool name
|
|
157
157
|
# @return [String] sanitized name
|
|
158
158
|
def sanitize_name(name)
|
|
159
|
-
name.to_s.gsub(/[^a-zA-Z0-9_]/,
|
|
159
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_+/, '_').downcase
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Layer 12: Memory
|
|
2
|
+
|
|
3
|
+
Persistent cross-session memory backed by SQLite.
|
|
4
|
+
|
|
5
|
+
## Classes
|
|
6
|
+
|
|
7
|
+
- **`Store`** — Writes memories to the `memories` table. Each memory has content, category
|
|
8
|
+
(`code_pattern`, `user_preference`, `project_convention`, `error_resolution`, `decision`),
|
|
9
|
+
and a retention tier (`short`, `medium`, `long`).
|
|
10
|
+
|
|
11
|
+
- **`Search`** — Full-text search across memories. Filters by category and tier.
|
|
12
|
+
Used by the agent to recall context from previous sessions.
|
|
13
|
+
|
|
14
|
+
- **`SessionPersistence`** — Saves and restores session state (conversation, tasks, costs)
|
|
15
|
+
across REPL sessions. Keyed by session ID in the `sessions` table.
|
|
16
|
+
|
|
17
|
+
- **`Models`** — Data objects mapping to/from SQLite memory rows.
|
|
@@ -33,13 +33,13 @@ module RubynCode
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# @return [Boolean]
|
|
36
|
-
def short? = tier ==
|
|
36
|
+
def short? = tier == 'short'
|
|
37
37
|
|
|
38
38
|
# @return [Boolean]
|
|
39
|
-
def medium? = tier ==
|
|
39
|
+
def medium? = tier == 'medium'
|
|
40
40
|
|
|
41
41
|
# @return [Boolean]
|
|
42
|
-
def long? = tier ==
|
|
42
|
+
def long? = tier == 'long'
|
|
43
43
|
|
|
44
44
|
# @return [Hash]
|
|
45
45
|
def to_h
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'json'
|
|
4
4
|
|
|
5
5
|
module RubynCode
|
|
6
6
|
module Memory
|
|
@@ -24,16 +24,16 @@ module RubynCode
|
|
|
24
24
|
# @param limit [Integer] maximum results (default 10)
|
|
25
25
|
# @return [Array<MemoryRecord>]
|
|
26
26
|
def search(query, tier: nil, category: nil, limit: 10)
|
|
27
|
-
conditions = [
|
|
27
|
+
conditions = ['m.project_path = ?']
|
|
28
28
|
params = [@project_path]
|
|
29
29
|
|
|
30
30
|
if tier
|
|
31
|
-
conditions <<
|
|
31
|
+
conditions << 'm.tier = ?'
|
|
32
32
|
params << tier
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
if category
|
|
36
|
-
conditions <<
|
|
36
|
+
conditions << 'm.category = ?'
|
|
37
37
|
params << category
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -127,20 +127,20 @@ module RubynCode
|
|
|
127
127
|
# @param row [Hash]
|
|
128
128
|
# @return [MemoryRecord]
|
|
129
129
|
def build_record(row)
|
|
130
|
-
metadata = parse_json(row[
|
|
130
|
+
metadata = parse_json(row['metadata'])
|
|
131
131
|
|
|
132
132
|
MemoryRecord.new(
|
|
133
|
-
id: row[
|
|
134
|
-
project_path: row[
|
|
135
|
-
tier: row[
|
|
136
|
-
category: row[
|
|
137
|
-
content: row[
|
|
138
|
-
relevance_score: row[
|
|
139
|
-
access_count: row[
|
|
140
|
-
last_accessed_at: row[
|
|
141
|
-
expires_at: row[
|
|
133
|
+
id: row['id'],
|
|
134
|
+
project_path: row['project_path'],
|
|
135
|
+
tier: row['tier'],
|
|
136
|
+
category: row['category'],
|
|
137
|
+
content: row['content'],
|
|
138
|
+
relevance_score: row['relevance_score'].to_f,
|
|
139
|
+
access_count: row['access_count'].to_i,
|
|
140
|
+
last_accessed_at: row['last_accessed_at'],
|
|
141
|
+
expires_at: row['expires_at'],
|
|
142
142
|
metadata: metadata,
|
|
143
|
-
created_at: row[
|
|
143
|
+
created_at: row['created_at']
|
|
144
144
|
)
|
|
145
145
|
end
|
|
146
146
|
|
|
@@ -152,9 +152,9 @@ module RubynCode
|
|
|
152
152
|
def touch_accessed(records)
|
|
153
153
|
return if records.empty?
|
|
154
154
|
|
|
155
|
-
now = Time.now.utc.strftime(
|
|
155
|
+
now = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')
|
|
156
156
|
ids = records.map(&:id)
|
|
157
|
-
placeholders = ([
|
|
157
|
+
placeholders = (['?'] * ids.size).join(', ')
|
|
158
158
|
|
|
159
159
|
@db.execute(
|
|
160
160
|
"UPDATE memories SET access_count = access_count + 1, last_accessed_at = ? WHERE id IN (#{placeholders})",
|