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
|
@@ -66,20 +66,20 @@ module RubynCode
|
|
|
66
66
|
# @param row [Hash] a database row hash
|
|
67
67
|
# @return [Tasks::Task]
|
|
68
68
|
def build_task(row)
|
|
69
|
-
metadata = parse_json(row[
|
|
69
|
+
metadata = parse_json(row['metadata'])
|
|
70
70
|
|
|
71
71
|
Tasks::Task.new(
|
|
72
|
-
id: row[
|
|
73
|
-
session_id: row[
|
|
74
|
-
title: row[
|
|
75
|
-
description: row[
|
|
76
|
-
status: row[
|
|
77
|
-
priority: row[
|
|
78
|
-
owner: row[
|
|
79
|
-
result: row[
|
|
72
|
+
id: row['id'],
|
|
73
|
+
session_id: row['session_id'],
|
|
74
|
+
title: row['title'],
|
|
75
|
+
description: row['description'],
|
|
76
|
+
status: row['status'],
|
|
77
|
+
priority: row['priority'].to_i,
|
|
78
|
+
owner: row['owner'],
|
|
79
|
+
result: row['result'],
|
|
80
80
|
metadata: metadata,
|
|
81
|
-
created_at: row[
|
|
82
|
-
updated_at: row[
|
|
81
|
+
created_at: row['created_at'],
|
|
82
|
+
updated_at: row['updated_at']
|
|
83
83
|
)
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Layer 8: Background
|
|
2
|
+
|
|
3
|
+
Background job execution for long-running commands.
|
|
4
|
+
|
|
5
|
+
## Classes
|
|
6
|
+
|
|
7
|
+
- **`Worker`** — Manages background processes. Spawns commands in subprocesses,
|
|
8
|
+
tracks their PIDs, and collects output when complete.
|
|
9
|
+
|
|
10
|
+
- **`Job`** — Represents a single background job: command, PID, status, output.
|
|
11
|
+
|
|
12
|
+
- **`Notifier`** — Delivers background job results back to the agent. Injects completed
|
|
13
|
+
job output into the conversation before the next LLM call.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require_relative 'job'
|
|
7
|
+
require_relative 'notifier'
|
|
8
8
|
|
|
9
9
|
module RubynCode
|
|
10
10
|
module Background
|
|
@@ -93,14 +93,59 @@ module RubynCode
|
|
|
93
93
|
private
|
|
94
94
|
|
|
95
95
|
def execute_job(job_id, command, timeout_seconds)
|
|
96
|
-
stdout, stderr,
|
|
96
|
+
stdout, stderr, = nil
|
|
97
97
|
final_status = :completed
|
|
98
98
|
|
|
99
99
|
begin
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
stdin_io, stdout_io, stderr_io, wait_thr = Open3.popen3(command, chdir: @project_root)
|
|
101
|
+
stdin_io.close
|
|
102
|
+
out_buf = +''
|
|
103
|
+
err_buf = +''
|
|
104
|
+
out_reader = Thread.new do
|
|
105
|
+
out_buf << stdout_io.read
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
err_reader = Thread.new do
|
|
110
|
+
err_buf << stderr_io.read
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
unless wait_thr.join(timeout_seconds)
|
|
116
|
+
begin
|
|
117
|
+
Process.kill('TERM', wait_thr.pid)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
sleep 0.1
|
|
122
|
+
begin
|
|
123
|
+
Process.kill('KILL', wait_thr.pid)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
wait_thr.join(5)
|
|
128
|
+
out_reader.join(2)
|
|
129
|
+
err_reader.join(2)
|
|
130
|
+
[stdout_io, stderr_io].each do |io|
|
|
131
|
+
io.close
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
raise Timeout::Error
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
out_reader.join(5)
|
|
139
|
+
err_reader.join(5)
|
|
140
|
+
[stdout_io, stderr_io].each do |io|
|
|
141
|
+
io.close
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
102
144
|
end
|
|
103
145
|
|
|
146
|
+
stdout = out_buf
|
|
147
|
+
stderr = err_buf
|
|
148
|
+
process_status = wait_thr.value
|
|
104
149
|
final_status = process_status.success? ? :completed : :error
|
|
105
150
|
rescue Timeout::Error
|
|
106
151
|
final_status = :timeout
|
|
@@ -127,19 +172,19 @@ module RubynCode
|
|
|
127
172
|
end
|
|
128
173
|
|
|
129
174
|
@notifier.push({
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
type: :job_completed,
|
|
176
|
+
job_id: job_id,
|
|
177
|
+
status: final_status,
|
|
178
|
+
result: result,
|
|
179
|
+
duration: completed_job.duration
|
|
180
|
+
})
|
|
136
181
|
end
|
|
137
182
|
|
|
138
183
|
def build_result(stdout, stderr)
|
|
139
184
|
parts = []
|
|
140
185
|
parts << stdout if stdout && !stdout.empty?
|
|
141
186
|
parts << "STDERR: #{stderr}" if stderr && !stderr.empty?
|
|
142
|
-
parts.empty? ?
|
|
187
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
143
188
|
end
|
|
144
189
|
end
|
|
145
190
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# CLI Layer
|
|
2
|
+
|
|
3
|
+
Terminal interface. Entry point → REPL → rendering.
|
|
4
|
+
|
|
5
|
+
## Classes
|
|
6
|
+
|
|
7
|
+
- **`App`** — Parses ARGV into commands (`:version`, `:auth`, `:help`, `:run`, `:repl`) and dispatches.
|
|
8
|
+
`App.start(ARGV)` is the gem's entry point from `exe/rubyn-code`.
|
|
9
|
+
|
|
10
|
+
- **`REPL`** — Read-eval-print loop. Wires up InputHandler for parsing, Agent::Loop for execution,
|
|
11
|
+
Renderer for output. Delegates `/slash` commands to the Commands::Registry.
|
|
12
|
+
|
|
13
|
+
- **`InputHandler`** — Maps user input to `Command` structs (a `Data.define`). Classifies input
|
|
14
|
+
as `:command`, `:message`, or `:quit`. Registry-driven — no hardcoded command list.
|
|
15
|
+
|
|
16
|
+
- **`Renderer`** — Renders LLM responses to the terminal. Uses Pastel for colors,
|
|
17
|
+
Rouge (Monokai theme) for syntax highlighting. Has a `yolo` writer for permission bypass display.
|
|
18
|
+
|
|
19
|
+
- **`Spinner`** — TTY::Spinner wrapper for thinking/working indicators.
|
|
20
|
+
|
|
21
|
+
- **`StreamFormatter`** — Handles real-time streaming output from the LLM, buffering partial
|
|
22
|
+
markdown and flushing complete lines with syntax highlighting.
|
|
23
|
+
|
|
24
|
+
## Commands Subsystem
|
|
25
|
+
|
|
26
|
+
See [`commands/RUBYN.md`](commands/RUBYN.md) for full docs.
|
|
27
|
+
|
|
28
|
+
19 slash commands, each in its own file under `cli/commands/`. Registry-based dispatch
|
|
29
|
+
with tab-completion. Commands return optional **action hashes** for REPL state changes
|
|
30
|
+
(model switch, plan mode toggle, budget updates, etc.).
|
data/lib/rubyn_code/cli/app.rb
CHANGED
|
@@ -13,15 +13,21 @@ module RubynCode
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def run
|
|
16
|
+
RubynCode::Debug.enable! if @options[:debug]
|
|
17
|
+
|
|
16
18
|
case @options[:command]
|
|
17
19
|
when :version
|
|
18
20
|
puts "rubyn-code #{RubynCode::VERSION}"
|
|
19
21
|
when :auth
|
|
20
22
|
run_auth
|
|
23
|
+
when :setup
|
|
24
|
+
run_setup
|
|
21
25
|
when :help
|
|
22
26
|
display_help
|
|
23
27
|
when :run
|
|
24
28
|
run_single_prompt(@options[:prompt])
|
|
29
|
+
when :daemon
|
|
30
|
+
run_daemon
|
|
25
31
|
when :repl
|
|
26
32
|
run_repl
|
|
27
33
|
end
|
|
@@ -29,27 +35,35 @@ module RubynCode
|
|
|
29
35
|
|
|
30
36
|
private
|
|
31
37
|
|
|
32
|
-
def parse_options(argv)
|
|
38
|
+
def parse_options(argv) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
33
39
|
options = { command: :repl }
|
|
34
40
|
|
|
35
41
|
i = 0
|
|
36
42
|
while i < argv.length
|
|
37
43
|
case argv[i]
|
|
38
|
-
when
|
|
44
|
+
when '--version', '-v'
|
|
39
45
|
options[:command] = :version
|
|
40
|
-
when
|
|
46
|
+
when '--help', '-h'
|
|
41
47
|
options[:command] = :help
|
|
42
|
-
when
|
|
48
|
+
when '--auth'
|
|
43
49
|
options[:command] = :auth
|
|
44
|
-
when
|
|
50
|
+
when '--resume', '-r'
|
|
45
51
|
options[:session_id] = argv[i + 1]
|
|
46
52
|
i += 1
|
|
47
|
-
when
|
|
53
|
+
when '-p', '--prompt'
|
|
48
54
|
options[:command] = :run
|
|
49
55
|
options[:prompt] = argv[i + 1]
|
|
50
56
|
i += 1
|
|
51
|
-
when
|
|
57
|
+
when '--yolo'
|
|
52
58
|
options[:yolo] = true
|
|
59
|
+
when '--debug'
|
|
60
|
+
options[:debug] = true
|
|
61
|
+
when '--setup'
|
|
62
|
+
options[:command] = :setup
|
|
63
|
+
when 'daemon'
|
|
64
|
+
options[:command] = :daemon
|
|
65
|
+
parse_daemon_options!(argv, i + 1, options)
|
|
66
|
+
break
|
|
53
67
|
end
|
|
54
68
|
i += 1
|
|
55
69
|
end
|
|
@@ -57,19 +71,67 @@ module RubynCode
|
|
|
57
71
|
options
|
|
58
72
|
end
|
|
59
73
|
|
|
74
|
+
# Parses daemon-specific flags from the argv starting at the given index.
|
|
75
|
+
#
|
|
76
|
+
# @param argv [Array<String>]
|
|
77
|
+
# @param start [Integer]
|
|
78
|
+
# @param options [Hash]
|
|
79
|
+
# @return [void]
|
|
80
|
+
def parse_daemon_options!(argv, start, options) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
81
|
+
options[:daemon] = {
|
|
82
|
+
max_runs: 100,
|
|
83
|
+
max_cost: 10.0,
|
|
84
|
+
idle_timeout: 60,
|
|
85
|
+
poll_interval: 5,
|
|
86
|
+
agent_name: "golem-#{SecureRandom.hex(4)}",
|
|
87
|
+
role: 'autonomous coding agent'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
i = start
|
|
91
|
+
while i < argv.length
|
|
92
|
+
case argv[i]
|
|
93
|
+
when '--max-runs'
|
|
94
|
+
options[:daemon][:max_runs] = argv[i + 1].to_i
|
|
95
|
+
i += 1
|
|
96
|
+
when '--max-cost'
|
|
97
|
+
options[:daemon][:max_cost] = argv[i + 1].to_f
|
|
98
|
+
i += 1
|
|
99
|
+
when '--idle-timeout'
|
|
100
|
+
options[:daemon][:idle_timeout] = argv[i + 1].to_i
|
|
101
|
+
i += 1
|
|
102
|
+
when '--poll-interval'
|
|
103
|
+
options[:daemon][:poll_interval] = argv[i + 1].to_i
|
|
104
|
+
i += 1
|
|
105
|
+
when '--name'
|
|
106
|
+
options[:daemon][:agent_name] = argv[i + 1]
|
|
107
|
+
i += 1
|
|
108
|
+
when '--role'
|
|
109
|
+
options[:daemon][:role] = argv[i + 1]
|
|
110
|
+
i += 1
|
|
111
|
+
when '--debug'
|
|
112
|
+
options[:debug] = true
|
|
113
|
+
end
|
|
114
|
+
i += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
60
118
|
def run_auth
|
|
61
119
|
renderer = Renderer.new
|
|
62
|
-
renderer.info(
|
|
120
|
+
renderer.info('Starting Claude OAuth authentication...')
|
|
63
121
|
|
|
64
122
|
begin
|
|
65
123
|
Auth::OAuth.new.authenticate!
|
|
66
|
-
renderer.success(
|
|
124
|
+
renderer.success('Authentication successful! Token stored.')
|
|
67
125
|
rescue AuthenticationError => e
|
|
68
126
|
renderer.error("Authentication failed: #{e.message}")
|
|
69
127
|
exit(1)
|
|
70
128
|
end
|
|
71
129
|
end
|
|
72
130
|
|
|
131
|
+
def run_setup
|
|
132
|
+
Setup.run
|
|
133
|
+
end
|
|
134
|
+
|
|
73
135
|
def run_single_prompt(prompt)
|
|
74
136
|
return display_help unless prompt
|
|
75
137
|
|
|
@@ -79,6 +141,10 @@ module RubynCode
|
|
|
79
141
|
puts response
|
|
80
142
|
end
|
|
81
143
|
|
|
144
|
+
def run_daemon
|
|
145
|
+
DaemonRunner.new(@options).run
|
|
146
|
+
end
|
|
147
|
+
|
|
82
148
|
def run_repl
|
|
83
149
|
REPL.new(
|
|
84
150
|
session_id: @options[:session_id],
|
|
@@ -95,10 +161,20 @@ module RubynCode
|
|
|
95
161
|
rubyn-code Start interactive REPL
|
|
96
162
|
rubyn-code -p "prompt" Run a single prompt and exit
|
|
97
163
|
rubyn-code --resume [ID] Resume a previous session
|
|
164
|
+
rubyn-code --setup Pin rubyn-code to bypass rbenv/rvm
|
|
98
165
|
rubyn-code --auth Authenticate with Claude
|
|
99
166
|
rubyn-code --version Show version
|
|
100
167
|
rubyn-code --help Show this help
|
|
101
168
|
|
|
169
|
+
Daemon Mode:
|
|
170
|
+
rubyn-code daemon Start autonomous daemon (GOLEM)
|
|
171
|
+
rubyn-code daemon --name NAME Agent name (default: golem-<random>)
|
|
172
|
+
rubyn-code daemon --role ROLE Agent role description
|
|
173
|
+
rubyn-code daemon --max-runs N Max tasks before shutdown (default: 100)
|
|
174
|
+
rubyn-code daemon --max-cost N Max USD spend before shutdown (default: 10.0)
|
|
175
|
+
rubyn-code daemon --idle-timeout N Seconds idle before shutdown (default: 60)
|
|
176
|
+
rubyn-code daemon --poll-interval N Seconds between polls (default: 5)
|
|
177
|
+
|
|
102
178
|
Interactive Commands:
|
|
103
179
|
/help Show available commands
|
|
104
180
|
/quit Exit
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# CLI::Commands — Slash Command System
|
|
2
|
+
|
|
3
|
+
> Registry-based command dispatch for the REPL. Same Base/Registry pattern as the tool system.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
User types /help
|
|
9
|
+
↓
|
|
10
|
+
InputHandler.classify → :command
|
|
11
|
+
↓
|
|
12
|
+
REPL#handle_command
|
|
13
|
+
↓
|
|
14
|
+
Registry#dispatch('/help', args, context)
|
|
15
|
+
↓
|
|
16
|
+
Help#execute(args, context)
|
|
17
|
+
↓
|
|
18
|
+
(optional) → action hash back to REPL for state changes
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Core Infrastructure
|
|
22
|
+
|
|
23
|
+
| File | Class | Purpose |
|
|
24
|
+
|------|-------|---------|
|
|
25
|
+
| `base.rb` | `Base` | Abstract command — `command_name`, `description`, `aliases`, `execute(args, ctx)` |
|
|
26
|
+
| `registry.rb` | `Registry` | Discovers, registers, dispatches commands. Provides tab-completion list |
|
|
27
|
+
| `context.rb` | `Context` | `Data.define` value object with all deps a command needs |
|
|
28
|
+
|
|
29
|
+
### Context Object
|
|
30
|
+
|
|
31
|
+
`Context` is a frozen `Data.define` carrying everything a command might need:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Context = Data.define(
|
|
35
|
+
:renderer, :conversation, :agent_loop, :context_manager,
|
|
36
|
+
:budget_enforcer, :llm_client, :db, :session_id,
|
|
37
|
+
:project_root, :skill_loader, :session_persistence,
|
|
38
|
+
:background_worker, :permission_tier, :plan_mode
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Commands receive it as the second argument to `execute`. Never mutate it — use
|
|
43
|
+
`with_message_handler` to attach a message callback if the command needs to send
|
|
44
|
+
prompts back to the agent loop.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
| Command | File | Description |
|
|
49
|
+
|---------|------|-------------|
|
|
50
|
+
| `/budget` | `budget.rb` | Show or set session budget (/budget [amount]) |
|
|
51
|
+
| `/clear` | `clear.rb` | Clear the terminal |
|
|
52
|
+
| `/compact` | `compact.rb` | Compress conversation context |
|
|
53
|
+
| `/context` | `context_info.rb` | Show context window usage |
|
|
54
|
+
| `/cost` | `cost.rb` | Show token usage and costs |
|
|
55
|
+
| `/diff` | `diff.rb` | Show git diff (staged, unstaged, or vs branch) |
|
|
56
|
+
| `/doctor` | `doctor.rb` | Environment health check |
|
|
57
|
+
| `/help` | `help.rb` | Show this help message |
|
|
58
|
+
| `/model` | `model.rb` | Show or switch model (/model [name]) |
|
|
59
|
+
| `/plan` | `plan.rb` | Toggle plan mode (think before acting) |
|
|
60
|
+
| `/quit` | `quit.rb` | Exit Rubyn Code (aliases: `/exit`, `/q`) |
|
|
61
|
+
| `/resume` | `resume.rb` | Resume a session or list recent sessions |
|
|
62
|
+
| `/review` | `review.rb` | Review current branch against best practices |
|
|
63
|
+
| `/skill` | `skill.rb` | Load a skill or list available skills |
|
|
64
|
+
| `/spawn` | `spawn.rb` | Spawn a teammate agent (/spawn \<name\> [role]) |
|
|
65
|
+
| `/tasks` | `tasks.rb` | List all tasks |
|
|
66
|
+
| `/tokens` | `tokens.rb` | Show token usage and context window estimate |
|
|
67
|
+
| `/undo` | `undo.rb` | Remove last exchange |
|
|
68
|
+
| `/version` | `version.rb` | Show version info |
|
|
69
|
+
|
|
70
|
+
## Action Hashes
|
|
71
|
+
|
|
72
|
+
Some commands can't change REPL state directly (they don't have access to the loop
|
|
73
|
+
or session). Instead, they return an **action hash** that the REPL processes:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Plan mode toggle
|
|
77
|
+
{ action: :set_plan_mode, enabled: true }
|
|
78
|
+
|
|
79
|
+
# Model switch
|
|
80
|
+
{ action: :set_model, model: 'claude-sonnet-4-20250514' }
|
|
81
|
+
|
|
82
|
+
# Budget update
|
|
83
|
+
{ action: :set_budget, amount: 10.0 }
|
|
84
|
+
|
|
85
|
+
# Spawn teammate
|
|
86
|
+
{ action: :spawn_teammate, name: 'alice', role: 'coder' }
|
|
87
|
+
|
|
88
|
+
# Resume session
|
|
89
|
+
{ action: :set_session_id, session_id: 'abc123' }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The REPL's `handle_command` method pattern-matches on these and applies the state change.
|
|
93
|
+
|
|
94
|
+
## Adding a New Command
|
|
95
|
+
|
|
96
|
+
1. Create `lib/rubyn_code/cli/commands/my_command.rb`
|
|
97
|
+
2. Inherit from `Base`, define `command_name` (with `/` prefix), `description`, `execute`
|
|
98
|
+
3. Add autoload entry in `lib/rubyn_code.rb` under `module Commands`
|
|
99
|
+
4. Register it in `REPL#setup_command_registry!`
|
|
100
|
+
5. Add spec in `spec/rubyn_code/cli/commands/my_command_spec.rb`
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# frozen_string_literal: true
|
|
104
|
+
|
|
105
|
+
module RubynCode
|
|
106
|
+
module CLI
|
|
107
|
+
module Commands
|
|
108
|
+
class MyCommand < Base
|
|
109
|
+
def self.command_name = '/mycommand'
|
|
110
|
+
def self.description = 'Does a thing'
|
|
111
|
+
|
|
112
|
+
def execute(args, ctx)
|
|
113
|
+
ctx.renderer.info('Did the thing!')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Plan Mode
|
|
122
|
+
|
|
123
|
+
`/plan` toggles plan mode via an action hash. When enabled:
|
|
124
|
+
|
|
125
|
+
- `Agent::Loop` sends only **read-only tools** (risk level `:read`) to Claude
|
|
126
|
+
- Claude can read files, grep, glob, check git status/log/diff — full exploration
|
|
127
|
+
- Claude **cannot** write, edit, execute, or modify anything
|
|
128
|
+
- A plan-mode system prompt is injected reinforcing these boundaries
|
|
129
|
+
- Claude responds with analysis, proposed steps, and gathered context
|
|
130
|
+
- Toggle off with `/plan` again to resume normal execution
|
|
131
|
+
|
|
132
|
+
Read-only tools in plan mode: `read_file`, `grep`, `glob`, `git_diff`, `git_log`,
|
|
133
|
+
`git_status`, `review_pr`, `memory_search`, `web_search`, `web_fetch`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Abstract base class for all slash commands.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses must implement:
|
|
9
|
+
# - self.command_name → String (e.g. '/doctor')
|
|
10
|
+
# - self.description → String (one-liner for /help)
|
|
11
|
+
# - execute(args, ctx) → void
|
|
12
|
+
#
|
|
13
|
+
# Optionally override:
|
|
14
|
+
# - self.aliases → Array<String> (e.g. ['/q', '/exit'])
|
|
15
|
+
# - self.hidden? → Boolean (hide from /help listing)
|
|
16
|
+
class Base
|
|
17
|
+
class << self
|
|
18
|
+
def command_name
|
|
19
|
+
raise NotImplementedError, "#{name} must define self.command_name"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
raise NotImplementedError, "#{name} must define self.description"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def aliases
|
|
27
|
+
[].freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def hidden?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# All names this command responds to (primary + aliases).
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String>]
|
|
37
|
+
def all_names
|
|
38
|
+
[command_name, *aliases].freeze
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Execute the command.
|
|
43
|
+
#
|
|
44
|
+
# @param args [Array<String>] arguments passed after the command name
|
|
45
|
+
# @param ctx [Commands::Context] shared context with REPL dependencies
|
|
46
|
+
# @return [void]
|
|
47
|
+
def execute(args, ctx)
|
|
48
|
+
raise NotImplementedError, "#{self.class.name}#execute must be implemented"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Budget < Base
|
|
7
|
+
def self.command_name = '/budget'
|
|
8
|
+
def self.description = 'Show or set session budget (/budget [amount])'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
amount = args.first
|
|
12
|
+
|
|
13
|
+
if amount
|
|
14
|
+
ctx.renderer.info("Session budget set to $#{amount}")
|
|
15
|
+
{ action: :set_budget, amount: amount.to_f }
|
|
16
|
+
else
|
|
17
|
+
remaining = ctx.budget_enforcer.remaining_budget
|
|
18
|
+
ctx.renderer.info("Remaining budget: $#{format('%.4f', remaining)}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Clear < Base
|
|
7
|
+
def self.command_name = '/clear'
|
|
8
|
+
def self.description = 'Clear the terminal'
|
|
9
|
+
|
|
10
|
+
def execute(_args, _ctx)
|
|
11
|
+
system('clear')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Compact < Base
|
|
7
|
+
def self.command_name = '/compact'
|
|
8
|
+
def self.description = 'Compress conversation context'
|
|
9
|
+
|
|
10
|
+
def execute(args, ctx)
|
|
11
|
+
focus = args.first
|
|
12
|
+
|
|
13
|
+
compactor = ::RubynCode::Context::Compactor.new(llm_client: ctx.llm_client)
|
|
14
|
+
new_messages = compactor.manual_compact!(ctx.conversation.messages, focus: focus)
|
|
15
|
+
ctx.conversation.replace!(new_messages)
|
|
16
|
+
ctx.renderer.info("Context compacted. #{ctx.conversation.length} messages remaining.")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Immutable context object passed to every slash command.
|
|
7
|
+
# Provides access to all REPL dependencies without coupling
|
|
8
|
+
# commands to the REPL class itself.
|
|
9
|
+
Context = Data.define(
|
|
10
|
+
:renderer,
|
|
11
|
+
:conversation,
|
|
12
|
+
:agent_loop,
|
|
13
|
+
:context_manager,
|
|
14
|
+
:budget_enforcer,
|
|
15
|
+
:llm_client,
|
|
16
|
+
:db,
|
|
17
|
+
:session_id,
|
|
18
|
+
:project_root,
|
|
19
|
+
:skill_loader,
|
|
20
|
+
:session_persistence,
|
|
21
|
+
:background_worker,
|
|
22
|
+
:permission_tier,
|
|
23
|
+
:plan_mode,
|
|
24
|
+
:message_handler
|
|
25
|
+
) do
|
|
26
|
+
# Convenience: return a new Context with a message handler attached.
|
|
27
|
+
# Used by commands like /review that delegate to the LLM.
|
|
28
|
+
#
|
|
29
|
+
# @param handler [Proc] the REPL's handle_message proc
|
|
30
|
+
def with_message_handler(handler)
|
|
31
|
+
with(message_handler: handler)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param text [String] message to send through the agent loop
|
|
35
|
+
def send_message(text)
|
|
36
|
+
message_handler&.call(text)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def plan_mode? = plan_mode
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|