rubyn-code 0.2.2 → 0.4.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 +151 -5
- data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
- data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
- data/lib/rubyn_code/agent/conversation.rb +84 -56
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +152 -0
- data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
- data/lib/rubyn_code/agent/llm_caller.rb +157 -0
- data/lib/rubyn_code/agent/loop.rb +182 -683
- data/lib/rubyn_code/agent/loop_detector.rb +50 -11
- data/lib/rubyn_code/agent/prompts.rb +109 -0
- data/lib/rubyn_code/agent/response_modes.rb +111 -0
- data/lib/rubyn_code/agent/response_parser.rb +111 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +211 -0
- data/lib/rubyn_code/agent/tool_processor.rb +178 -0
- data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
- data/lib/rubyn_code/auth/key_encryption.rb +118 -0
- data/lib/rubyn_code/auth/oauth.rb +80 -64
- data/lib/rubyn_code/auth/server.rb +21 -24
- data/lib/rubyn_code/auth/token_store.rb +80 -52
- data/lib/rubyn_code/autonomous/daemon.rb +146 -32
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -24
- data/lib/rubyn_code/autonomous/task_claimer.rb +46 -44
- data/lib/rubyn_code/background/worker.rb +64 -76
- data/lib/rubyn_code/cli/app.rb +159 -114
- data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
- data/lib/rubyn_code/cli/commands/model.rb +105 -18
- data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
- data/lib/rubyn_code/cli/commands/provider.rb +123 -0
- data/lib/rubyn_code/cli/commands/skill.rb +52 -3
- data/lib/rubyn_code/cli/daemon_runner.rb +64 -11
- data/lib/rubyn_code/cli/first_run.rb +159 -0
- data/lib/rubyn_code/cli/renderer.rb +109 -60
- data/lib/rubyn_code/cli/repl.rb +48 -374
- data/lib/rubyn_code/cli/repl_commands.rb +177 -0
- data/lib/rubyn_code/cli/repl_lifecycle.rb +76 -0
- data/lib/rubyn_code/cli/repl_setup.rb +181 -0
- data/lib/rubyn_code/cli/setup.rb +6 -2
- data/lib/rubyn_code/cli/stream_formatter.rb +56 -49
- data/lib/rubyn_code/cli/version_check.rb +28 -11
- data/lib/rubyn_code/config/defaults.rb +11 -0
- data/lib/rubyn_code/config/project_profile.rb +185 -0
- data/lib/rubyn_code/config/schema.json +49 -0
- data/lib/rubyn_code/config/settings.rb +103 -1
- data/lib/rubyn_code/config/validator.rb +63 -0
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/context_budget.rb +182 -0
- data/lib/rubyn_code/context/context_collapse.rb +34 -4
- data/lib/rubyn_code/context/decision_compactor.rb +99 -0
- data/lib/rubyn_code/context/manager.rb +44 -8
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/context/micro_compact.rb +29 -19
- data/lib/rubyn_code/context/schema_filter.rb +64 -0
- data/lib/rubyn_code/db/connection.rb +31 -26
- data/lib/rubyn_code/db/migrator.rb +44 -28
- data/lib/rubyn_code/hooks/built_in.rb +14 -10
- data/lib/rubyn_code/hooks/registry.rb +4 -0
- data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
- data/lib/rubyn_code/ide/client.rb +110 -0
- data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
- data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
- data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
- data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
- data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
- data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
- data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
- data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
- data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
- data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
- data/lib/rubyn_code/ide/handlers.rb +76 -0
- data/lib/rubyn_code/ide/protocol.rb +111 -0
- data/lib/rubyn_code/ide/server.rb +186 -0
- data/lib/rubyn_code/index/codebase_index.rb +311 -0
- data/lib/rubyn_code/learning/extractor.rb +65 -82
- data/lib/rubyn_code/learning/injector.rb +22 -23
- data/lib/rubyn_code/learning/instinct.rb +71 -42
- data/lib/rubyn_code/learning/shortcut.rb +95 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +274 -0
- data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +215 -0
- data/lib/rubyn_code/llm/adapters/base.rb +35 -0
- data/lib/rubyn_code/llm/adapters/json_parsing.rb +21 -0
- data/lib/rubyn_code/llm/adapters/openai.rb +246 -0
- data/lib/rubyn_code/llm/adapters/openai_compatible.rb +50 -0
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +90 -0
- data/lib/rubyn_code/llm/adapters/openai_streaming.rb +141 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +60 -0
- data/lib/rubyn_code/llm/client.rb +75 -247
- data/lib/rubyn_code/llm/model_router.rb +237 -0
- data/lib/rubyn_code/llm/streaming.rb +4 -227
- data/lib/rubyn_code/mcp/client.rb +1 -1
- data/lib/rubyn_code/mcp/config.rb +10 -12
- data/lib/rubyn_code/mcp/sse_transport.rb +15 -13
- data/lib/rubyn_code/mcp/stdio_transport.rb +16 -18
- data/lib/rubyn_code/mcp/tool_bridge.rb +31 -62
- data/lib/rubyn_code/memory/search.rb +1 -0
- data/lib/rubyn_code/memory/session_persistence.rb +59 -58
- data/lib/rubyn_code/memory/store.rb +42 -55
- data/lib/rubyn_code/observability/budget_enforcer.rb +46 -32
- data/lib/rubyn_code/observability/cost_calculator.rb +32 -8
- data/lib/rubyn_code/observability/skill_analytics.rb +116 -0
- data/lib/rubyn_code/observability/token_analytics.rb +130 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +79 -61
- data/lib/rubyn_code/output/diff_renderer.rb +102 -77
- data/lib/rubyn_code/output/formatter.rb +11 -11
- data/lib/rubyn_code/permissions/policy.rb +11 -13
- data/lib/rubyn_code/permissions/prompter.rb +8 -9
- data/lib/rubyn_code/protocols/plan_approval.rb +25 -20
- data/lib/rubyn_code/self_test.rb +315 -0
- data/lib/rubyn_code/skills/catalog.rb +66 -0
- data/lib/rubyn_code/skills/document.rb +33 -29
- data/lib/rubyn_code/skills/loader.rb +43 -0
- data/lib/rubyn_code/skills/ttl_manager.rb +100 -0
- data/lib/rubyn_code/sub_agents/runner.rb +20 -25
- data/lib/rubyn_code/tasks/dag.rb +25 -24
- data/lib/rubyn_code/tasks/models.rb +1 -0
- data/lib/rubyn_code/tools/ask_user.rb +44 -0
- data/lib/rubyn_code/tools/background_run.rb +2 -1
- data/lib/rubyn_code/tools/base.rb +39 -32
- data/lib/rubyn_code/tools/bash.rb +7 -1
- data/lib/rubyn_code/tools/edit_file.rb +130 -17
- data/lib/rubyn_code/tools/executor.rb +130 -25
- data/lib/rubyn_code/tools/file_cache.rb +95 -0
- data/lib/rubyn_code/tools/git_commit.rb +12 -10
- data/lib/rubyn_code/tools/git_log.rb +12 -10
- data/lib/rubyn_code/tools/glob.rb +29 -7
- data/lib/rubyn_code/tools/grep.rb +8 -1
- data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
- data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
- data/lib/rubyn_code/tools/load_skill.rb +13 -6
- data/lib/rubyn_code/tools/memory_search.rb +14 -13
- data/lib/rubyn_code/tools/memory_write.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +190 -0
- data/lib/rubyn_code/tools/read_file.rb +17 -6
- data/lib/rubyn_code/tools/registry.rb +11 -0
- data/lib/rubyn_code/tools/review_pr.rb +127 -80
- data/lib/rubyn_code/tools/run_specs.rb +26 -15
- data/lib/rubyn_code/tools/schema.rb +4 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +113 -82
- data/lib/rubyn_code/tools/spawn_teammate.rb +107 -64
- data/lib/rubyn_code/tools/spec_output_parser.rb +118 -0
- data/lib/rubyn_code/tools/task.rb +17 -17
- data/lib/rubyn_code/tools/web_fetch.rb +62 -47
- data/lib/rubyn_code/tools/web_search.rb +66 -48
- data/lib/rubyn_code/tools/write_file.rb +76 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +62 -1
- data/skills/rubyn_self_test.md +133 -0
- metadata +83 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module IDE
|
|
5
|
+
module Handlers
|
|
6
|
+
# Handles the "session/resume" JSON-RPC request.
|
|
7
|
+
#
|
|
8
|
+
# Loads a previously persisted session from SessionPersistence and
|
|
9
|
+
# pre-populates the PromptHandler's conversation cache so that the
|
|
10
|
+
# next prompt continues from where the session left off.
|
|
11
|
+
class SessionResumeHandler
|
|
12
|
+
def initialize(server)
|
|
13
|
+
@server = server
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(params)
|
|
17
|
+
session_id = params['sessionId']
|
|
18
|
+
return { 'resumed' => false, 'error' => 'Missing sessionId' } unless session_id
|
|
19
|
+
|
|
20
|
+
persistence = @server.session_persistence
|
|
21
|
+
return { 'resumed' => false, 'error' => 'Session persistence not available' } unless persistence
|
|
22
|
+
|
|
23
|
+
data = persistence.load_session(session_id)
|
|
24
|
+
return { 'resumed' => false, 'error' => 'Session not found' } unless data
|
|
25
|
+
|
|
26
|
+
messages = data[:messages] || []
|
|
27
|
+
|
|
28
|
+
# Pre-populate the prompt handler's conversation cache so the next
|
|
29
|
+
# prompt picks up from the restored history.
|
|
30
|
+
prompt = @server.handler_instance(:prompt)
|
|
31
|
+
if prompt
|
|
32
|
+
conversation = Agent::Conversation.new
|
|
33
|
+
messages.each { |msg| conversation.messages << msg }
|
|
34
|
+
prompt.inject_conversation(session_id, conversation)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ 'resumed' => true, 'sessionId' => session_id, 'messages' => messages }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module IDE
|
|
5
|
+
module Handlers
|
|
6
|
+
# Handles the "shutdown" JSON-RPC request.
|
|
7
|
+
#
|
|
8
|
+
# Triggers session persistence, signals the server to stop its
|
|
9
|
+
# read loop, and returns confirmation.
|
|
10
|
+
class ShutdownHandler
|
|
11
|
+
def initialize(server)
|
|
12
|
+
@server = server
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(_params)
|
|
16
|
+
warn '[ShutdownHandler] shutdown requested'
|
|
17
|
+
|
|
18
|
+
save_session!
|
|
19
|
+
@server.stop!
|
|
20
|
+
|
|
21
|
+
{ 'shutdown' => true }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def save_session!
|
|
27
|
+
return unless defined?(RubynCode::Memory::SessionPersistence)
|
|
28
|
+
|
|
29
|
+
persistence = @server.session_persistence
|
|
30
|
+
persistence&.save
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
warn "[ShutdownHandler] session save failed: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'handlers/initialize_handler'
|
|
4
|
+
require_relative 'handlers/prompt_handler'
|
|
5
|
+
require_relative 'handlers/cancel_handler'
|
|
6
|
+
require_relative 'handlers/review_handler'
|
|
7
|
+
require_relative 'handlers/approve_tool_use_handler'
|
|
8
|
+
require_relative 'handlers/accept_edit_handler'
|
|
9
|
+
require_relative 'handlers/shutdown_handler'
|
|
10
|
+
require_relative 'handlers/config_get_handler'
|
|
11
|
+
require_relative 'handlers/config_set_handler'
|
|
12
|
+
require_relative 'handlers/models_list_handler'
|
|
13
|
+
require_relative 'handlers/session_reset_handler'
|
|
14
|
+
require_relative 'handlers/session_list_handler'
|
|
15
|
+
require_relative 'handlers/session_resume_handler'
|
|
16
|
+
require_relative 'handlers/session_fork_handler'
|
|
17
|
+
|
|
18
|
+
module RubynCode
|
|
19
|
+
module IDE
|
|
20
|
+
module Handlers
|
|
21
|
+
# Method name => Handler class mapping.
|
|
22
|
+
REGISTRY = {
|
|
23
|
+
'initialize' => InitializeHandler,
|
|
24
|
+
'prompt' => PromptHandler,
|
|
25
|
+
'cancel' => CancelHandler,
|
|
26
|
+
'review' => ReviewHandler,
|
|
27
|
+
'approveToolUse' => ApproveToolUseHandler,
|
|
28
|
+
'acceptEdit' => AcceptEditHandler,
|
|
29
|
+
'shutdown' => ShutdownHandler,
|
|
30
|
+
'config/get' => ConfigGetHandler,
|
|
31
|
+
'config/set' => ConfigSetHandler,
|
|
32
|
+
'models/list' => ModelsListHandler,
|
|
33
|
+
'session/reset' => SessionResetHandler,
|
|
34
|
+
'session/list' => SessionListHandler,
|
|
35
|
+
'session/resume' => SessionResumeHandler,
|
|
36
|
+
'session/fork' => SessionForkHandler
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Short name => method name mapping (for handler_instance lookups).
|
|
40
|
+
SHORT_NAMES = {
|
|
41
|
+
prompt: 'prompt',
|
|
42
|
+
cancel: 'cancel',
|
|
43
|
+
review: 'review',
|
|
44
|
+
approve_tool_use: 'approveToolUse',
|
|
45
|
+
accept_edit: 'acceptEdit',
|
|
46
|
+
shutdown: 'shutdown',
|
|
47
|
+
initialize: 'initialize',
|
|
48
|
+
config_get: 'config/get',
|
|
49
|
+
config_set: 'config/set',
|
|
50
|
+
models_list: 'models/list',
|
|
51
|
+
session_reset: 'session/reset',
|
|
52
|
+
session_list: 'session/list',
|
|
53
|
+
session_resume: 'session/resume',
|
|
54
|
+
session_fork: 'session/fork'
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Register all handlers on the given server instance.
|
|
58
|
+
#
|
|
59
|
+
# @param server [RubynCode::IDE::Server] the IDE server
|
|
60
|
+
def self.register_all(server)
|
|
61
|
+
instances = {}
|
|
62
|
+
|
|
63
|
+
REGISTRY.each do |method, handler_class|
|
|
64
|
+
handler = handler_class.new(server)
|
|
65
|
+
instances[method] = handler
|
|
66
|
+
|
|
67
|
+
server.on(method) do |params, _id|
|
|
68
|
+
handler.call(params)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
server.handler_instances = instances
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RubynCode
|
|
6
|
+
module IDE
|
|
7
|
+
# JSON-RPC 2.0 protocol layer for the IDE server.
|
|
8
|
+
# Pure data — no side effects, no I/O beyond JSON serialisation.
|
|
9
|
+
module Protocol
|
|
10
|
+
JSONRPC_VERSION = '2.0'
|
|
11
|
+
|
|
12
|
+
# ── Standard JSON-RPC 2.0 error codes ──────────────────────────────
|
|
13
|
+
PARSE_ERROR = -32_700
|
|
14
|
+
INVALID_REQUEST = -32_600
|
|
15
|
+
METHOD_NOT_FOUND = -32_601
|
|
16
|
+
INVALID_PARAMS = -32_602
|
|
17
|
+
INTERNAL_ERROR = -32_603
|
|
18
|
+
|
|
19
|
+
# ── Custom error codes ─────────────────────────────────────────────
|
|
20
|
+
AGENT_BUSY = -1
|
|
21
|
+
SESSION_NOT_FOUND = -2
|
|
22
|
+
BUDGET_EXCEEDED = -3
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Parse a JSON string into a request hash.
|
|
27
|
+
# Returns either a valid request hash or an error response hash.
|
|
28
|
+
def parse(line) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- JSON-RPC validation checks
|
|
29
|
+
begin
|
|
30
|
+
data = JSON.parse(line)
|
|
31
|
+
rescue JSON::ParserError
|
|
32
|
+
return error(nil, PARSE_ERROR, 'Parse error: invalid JSON')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return error(nil, INVALID_REQUEST, 'Invalid request: expected JSON object') unless data.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
unless data['jsonrpc'] == JSONRPC_VERSION
|
|
38
|
+
return error(data['id'], INVALID_REQUEST, 'Invalid request: missing or wrong "jsonrpc" version')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Response objects (containing "result" or "error") are valid
|
|
42
|
+
# JSON-RPC 2.0 messages that don't carry a "method".
|
|
43
|
+
is_response = data.key?('result') || data.key?('error')
|
|
44
|
+
|
|
45
|
+
unless is_response || data['method'].is_a?(String)
|
|
46
|
+
return error(data['id'], INVALID_REQUEST, 'Invalid request: "method" must be a string')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if data.key?('params') && !data['params'].is_a?(Hash) && !data['params'].is_a?(Array)
|
|
50
|
+
return error(data['id'], INVALID_PARAMS, 'Invalid params: "params" must be an object or array')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build a success response hash.
|
|
57
|
+
def response(id, result)
|
|
58
|
+
{
|
|
59
|
+
'jsonrpc' => JSONRPC_VERSION,
|
|
60
|
+
'id' => id,
|
|
61
|
+
'result' => stringify_keys_deep(result)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build an error response hash.
|
|
66
|
+
def error(id, code, message)
|
|
67
|
+
{
|
|
68
|
+
'jsonrpc' => JSONRPC_VERSION,
|
|
69
|
+
'id' => id,
|
|
70
|
+
'error' => {
|
|
71
|
+
'code' => code,
|
|
72
|
+
'message' => message
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Build a notification hash (no id).
|
|
78
|
+
def notification(method, params)
|
|
79
|
+
{
|
|
80
|
+
'jsonrpc' => JSONRPC_VERSION,
|
|
81
|
+
'method' => method,
|
|
82
|
+
'params' => stringify_keys_deep(params)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Serialise a hash to a JSON string terminated by a newline.
|
|
87
|
+
def serialize(hash)
|
|
88
|
+
"#{JSON.generate(hash)}\n"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ── Helpers ────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
# Recursively convert symbol keys to strings so every hash that
|
|
94
|
+
# leaves this module uses string keys for JSON compatibility.
|
|
95
|
+
def stringify_keys_deep(obj)
|
|
96
|
+
case obj
|
|
97
|
+
when Hash
|
|
98
|
+
obj.each_with_object({}) do |(k, v), memo|
|
|
99
|
+
memo[k.to_s] = stringify_keys_deep(v)
|
|
100
|
+
end
|
|
101
|
+
when Array
|
|
102
|
+
obj.map { |v| stringify_keys_deep(v) }
|
|
103
|
+
else
|
|
104
|
+
obj
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private_class_method :stringify_keys_deep
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'protocol'
|
|
5
|
+
require_relative 'client'
|
|
6
|
+
require_relative 'handlers'
|
|
7
|
+
|
|
8
|
+
module RubynCode
|
|
9
|
+
module IDE
|
|
10
|
+
# JSON-RPC 2.0 server for the VS Code extension.
|
|
11
|
+
#
|
|
12
|
+
# Reads newline-delimited JSON from $stdin, dispatches each request
|
|
13
|
+
# to a handler, and writes JSON-RPC responses/notifications to $stdout.
|
|
14
|
+
# All debug output goes to $stderr — never protocol data.
|
|
15
|
+
#
|
|
16
|
+
# Processes one request at a time on the main thread.
|
|
17
|
+
class Server
|
|
18
|
+
# Attributes set by handlers during the session lifecycle.
|
|
19
|
+
attr_accessor :workspace_path, :extension_version, :client_capabilities,
|
|
20
|
+
:session_persistence, :handler_instances, :tool_output_adapter,
|
|
21
|
+
:permission_mode
|
|
22
|
+
attr_reader :ide_client
|
|
23
|
+
|
|
24
|
+
def initialize(permission_mode: :default, yolo: false)
|
|
25
|
+
@permission_mode = yolo ? :bypass : permission_mode.to_sym
|
|
26
|
+
@running = false
|
|
27
|
+
@write_mutex = Mutex.new
|
|
28
|
+
@handlers = {}
|
|
29
|
+
@handler_instances = {}
|
|
30
|
+
@workspace_path = nil
|
|
31
|
+
@extension_version = nil
|
|
32
|
+
@client_capabilities = {}
|
|
33
|
+
@session_persistence = nil
|
|
34
|
+
@tool_output_adapter = nil
|
|
35
|
+
@ide_client = Client.new(self)
|
|
36
|
+
|
|
37
|
+
Handlers.register_all(self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Backward-compatible reader: true when permission_mode is :bypass.
|
|
41
|
+
def yolo
|
|
42
|
+
@permission_mode == :bypass
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run
|
|
46
|
+
@running = true
|
|
47
|
+
setup_signal_traps!
|
|
48
|
+
|
|
49
|
+
warn "[IDE::Server] started (pid=#{Process.pid})"
|
|
50
|
+
$stdout.sync = true
|
|
51
|
+
|
|
52
|
+
read_loop
|
|
53
|
+
ensure
|
|
54
|
+
graceful_shutdown!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ── Public helpers ──────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
# Send a JSON-RPC notification (no id) to stdout.
|
|
60
|
+
def notify(method, params = {})
|
|
61
|
+
write(Protocol.notification(method, params))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Register a handler for a given JSON-RPC method.
|
|
65
|
+
# The block receives (params, id) and must return a result hash.
|
|
66
|
+
def on(method, &block)
|
|
67
|
+
@handlers[method] = block
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Look up a handler instance by its short name (e.g. :prompt, :cancel).
|
|
71
|
+
# Returns nil if the handler is not registered.
|
|
72
|
+
def handler_instance(short_name)
|
|
73
|
+
method_name = Handlers::SHORT_NAMES[short_name.to_sym]
|
|
74
|
+
return nil unless method_name
|
|
75
|
+
|
|
76
|
+
@handler_instances[method_name]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Signal the server to stop its read loop.
|
|
80
|
+
def stop!
|
|
81
|
+
@running = false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# ── Main loop ───────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def read_loop
|
|
89
|
+
while @running
|
|
90
|
+
line = $stdin.gets
|
|
91
|
+
break if line.nil? # EOF — client disconnected
|
|
92
|
+
|
|
93
|
+
line = line.strip
|
|
94
|
+
next if line.empty?
|
|
95
|
+
|
|
96
|
+
handle_line(line)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_line(line)
|
|
101
|
+
msg = Protocol.parse(line)
|
|
102
|
+
|
|
103
|
+
# Protocol.parse returns an error response hash when parsing fails.
|
|
104
|
+
if msg.key?('error')
|
|
105
|
+
write(msg)
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
dispatch(msg)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
warn "[IDE::Server] error handling message: #{e.message}"
|
|
112
|
+
warn e.backtrace&.first(5)&.join("\n")
|
|
113
|
+
|
|
114
|
+
id = msg.is_a?(Hash) ? msg['id'] : nil
|
|
115
|
+
write(Protocol.error(id, Protocol::INTERNAL_ERROR, "Internal error: #{e.message}"))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ── Dispatch ────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def dispatch(msg)
|
|
121
|
+
# Response messages from the extension (for our outbound requests via ide_client).
|
|
122
|
+
# These have id + (result or error) but no method.
|
|
123
|
+
if !msg.key?('method') && msg.key?('id') && (msg.key?('result') || msg.key?('error'))
|
|
124
|
+
@ide_client.resolve(
|
|
125
|
+
msg['id'],
|
|
126
|
+
result: msg['result'],
|
|
127
|
+
error: msg['error']
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
method = msg['method']
|
|
133
|
+
params = msg['params'] || {}
|
|
134
|
+
id = msg['id']
|
|
135
|
+
|
|
136
|
+
handler = @handlers[method]
|
|
137
|
+
|
|
138
|
+
unless handler
|
|
139
|
+
write(Protocol.error(id, Protocol::METHOD_NOT_FOUND, "Method not found: #{method}")) if id
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
result = handler.call(params, id)
|
|
144
|
+
|
|
145
|
+
# Only send a response for requests (those with an id).
|
|
146
|
+
# Notifications (no id) do not get responses.
|
|
147
|
+
write(Protocol.response(id, result)) if id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ── Wire output ─────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def write(hash)
|
|
153
|
+
serialized = Protocol.serialize(hash)
|
|
154
|
+
@write_mutex.synchronize do
|
|
155
|
+
$stdout.write(serialized)
|
|
156
|
+
$stdout.flush
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ── Signal handling ─────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def setup_signal_traps!
|
|
163
|
+
%w[TERM INT].each do |sig|
|
|
164
|
+
trap(sig) do
|
|
165
|
+
warn "[IDE::Server] received SIG#{sig}, shutting down"
|
|
166
|
+
@running = false
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ── Shutdown ────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def graceful_shutdown!
|
|
174
|
+
warn '[IDE::Server] shutting down'
|
|
175
|
+
save_session!
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def save_session!
|
|
179
|
+
# Delegate to Memory::SessionPersistence if available.
|
|
180
|
+
@session_persistence.save if defined?(RubynCode::Memory::SessionPersistence) && @session_persistence
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
warn "[IDE::Server] session save failed: #{e.message}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|