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,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class BundleAdd < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'bundle_add'
|
|
11
|
+
DESCRIPTION = 'Adds a gem to the Gemfile and installs it via `bundle add`.'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
gem_name: { type: :string, required: true, description:
|
|
13
|
+
gem_name: { type: :string, required: true, description: 'Name of the gem to add' },
|
|
14
14
|
version: { type: :string, required: false, description: "Version constraint (e.g. '~> 1.0')" },
|
|
15
15
|
group: { type: :string, required: false, description: "Gemfile group (e.g. 'development', 'test')" }
|
|
16
16
|
}.freeze
|
|
@@ -18,14 +18,12 @@ module RubynCode
|
|
|
18
18
|
REQUIRES_CONFIRMATION = false
|
|
19
19
|
|
|
20
20
|
def execute(gem_name:, version: nil, group: nil)
|
|
21
|
-
gemfile_path = File.join(project_root,
|
|
21
|
+
gemfile_path = File.join(project_root, 'Gemfile')
|
|
22
22
|
|
|
23
|
-
unless File.exist?(gemfile_path)
|
|
24
|
-
raise Error, "No Gemfile found in project root. Cannot run bundle add."
|
|
25
|
-
end
|
|
23
|
+
raise Error, 'No Gemfile found in project root. Cannot run bundle add.' unless File.exist?(gemfile_path)
|
|
26
24
|
|
|
27
25
|
command = build_command(gem_name, version, group)
|
|
28
|
-
stdout, stderr, status =
|
|
26
|
+
stdout, stderr, status = safe_capture3(command, chdir: project_root)
|
|
29
27
|
|
|
30
28
|
build_output(stdout, stderr, status)
|
|
31
29
|
end
|
|
@@ -44,7 +42,7 @@ module RubynCode
|
|
|
44
42
|
parts << stdout unless stdout.empty?
|
|
45
43
|
parts << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
46
44
|
parts << "Exit code: #{status.exitstatus}" unless status.success?
|
|
47
|
-
parts.empty? ?
|
|
45
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class BundleInstall < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'bundle_install'
|
|
11
|
+
DESCRIPTION = 'Runs `bundle install` to install gem dependencies.'
|
|
12
12
|
PARAMETERS = {}.freeze
|
|
13
13
|
RISK_LEVEL = :execute
|
|
14
14
|
REQUIRES_CONFIRMATION = false
|
|
15
15
|
|
|
16
16
|
def execute(**_params)
|
|
17
|
-
gemfile_path = File.join(project_root,
|
|
17
|
+
gemfile_path = File.join(project_root, 'Gemfile')
|
|
18
18
|
|
|
19
|
-
unless File.exist?(gemfile_path)
|
|
20
|
-
raise Error, "No Gemfile found in project root. Cannot run bundle install."
|
|
21
|
-
end
|
|
19
|
+
raise Error, 'No Gemfile found in project root. Cannot run bundle install.' unless File.exist?(gemfile_path)
|
|
22
20
|
|
|
23
|
-
stdout, stderr, status =
|
|
21
|
+
stdout, stderr, status = safe_capture3('bundle install', chdir: project_root)
|
|
24
22
|
|
|
25
23
|
build_output(stdout, stderr, status)
|
|
26
24
|
end
|
|
@@ -32,7 +30,7 @@ module RubynCode
|
|
|
32
30
|
parts << stdout unless stdout.empty?
|
|
33
31
|
parts << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
34
32
|
parts << "Exit code: #{status.exitstatus}" unless status.success?
|
|
35
|
-
parts.empty? ?
|
|
33
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class Compact < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'compact'
|
|
10
|
+
DESCRIPTION = 'Triggers manual context compaction to reduce conversation size while preserving key information.'
|
|
11
11
|
PARAMETERS = {
|
|
12
|
-
focus: { type: :string, required: false,
|
|
12
|
+
focus: { type: :string, required: false,
|
|
13
|
+
description: "What to focus the summary on (e.g. 'the auth refactor', 'test failures')" }
|
|
13
14
|
}.freeze
|
|
14
15
|
RISK_LEVEL = :read
|
|
15
16
|
REQUIRES_CONFIRMATION = false
|
|
@@ -23,22 +24,22 @@ module RubynCode
|
|
|
23
24
|
manager = @context_manager
|
|
24
25
|
|
|
25
26
|
unless manager
|
|
26
|
-
return
|
|
27
|
-
|
|
27
|
+
return 'Context compaction is not available in this session. ' \
|
|
28
|
+
'No context manager was provided.'
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
if manager.respond_to?(:compact)
|
|
31
32
|
result = manager.compact(focus: focus)
|
|
32
33
|
format_result(result, focus)
|
|
33
34
|
else
|
|
34
|
-
|
|
35
|
+
'Context manager does not support compaction.'
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
private
|
|
39
40
|
|
|
40
41
|
def format_result(result, focus)
|
|
41
|
-
parts = [
|
|
42
|
+
parts = ['Context compacted successfully.']
|
|
42
43
|
|
|
43
44
|
if result.is_a?(Hash)
|
|
44
45
|
parts << "Messages before: #{result[:before]}" if result[:before]
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class DbMigrate < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'db_migrate'
|
|
11
|
+
DESCRIPTION = 'Runs Rails database migrations (up) or rollback (down).'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
direction: { type: :string, required: false, default:
|
|
14
|
-
|
|
13
|
+
direction: { type: :string, required: false, default: 'up', enum: %w[up down],
|
|
14
|
+
description: "Migration direction: 'up' to migrate, 'down' to rollback (default: 'up')" },
|
|
15
|
+
steps: { type: :integer, required: false,
|
|
16
|
+
description: "Number of steps to rollback (only used with direction 'down')" }
|
|
15
17
|
}.freeze
|
|
16
18
|
RISK_LEVEL = :execute
|
|
17
19
|
REQUIRES_CONFIRMATION = false
|
|
18
20
|
|
|
19
|
-
def execute(direction:
|
|
21
|
+
def execute(direction: 'up', steps: nil)
|
|
20
22
|
command = build_command(direction, steps)
|
|
21
|
-
stdout, stderr, status =
|
|
23
|
+
stdout, stderr, status = safe_capture3(command, chdir: project_root)
|
|
22
24
|
|
|
23
25
|
build_output(stdout, stderr, status)
|
|
24
26
|
end
|
|
@@ -27,11 +29,11 @@ module RubynCode
|
|
|
27
29
|
|
|
28
30
|
def build_command(direction, steps)
|
|
29
31
|
case direction
|
|
30
|
-
when
|
|
31
|
-
|
|
32
|
-
when
|
|
33
|
-
cmd =
|
|
34
|
-
cmd += " STEP=#{steps.to_i}" if steps
|
|
32
|
+
when 'up'
|
|
33
|
+
'bundle exec rails db:migrate'
|
|
34
|
+
when 'down'
|
|
35
|
+
cmd = 'bundle exec rails db:rollback'
|
|
36
|
+
cmd += " STEP=#{steps.to_i}" if steps&.to_i&.positive?
|
|
35
37
|
cmd
|
|
36
38
|
else
|
|
37
39
|
raise Error, "Invalid direction: #{direction}. Must be 'up' or 'down'."
|
|
@@ -43,7 +45,7 @@ module RubynCode
|
|
|
43
45
|
parts << stdout unless stdout.empty?
|
|
44
46
|
parts << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
45
47
|
parts << "Exit code: #{status.exitstatus}" unless status.success?
|
|
46
|
-
parts.empty? ?
|
|
48
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class EditFile < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'edit_file'
|
|
10
|
+
DESCRIPTION = 'Performs exact string replacement in a file. Fails if old_text is not found or is ambiguous.'
|
|
11
11
|
PARAMETERS = {
|
|
12
|
-
path: { type: :string, required: true, description:
|
|
13
|
-
old_text: { type: :string, required: true, description:
|
|
14
|
-
new_text: { type: :string, required: true, description:
|
|
15
|
-
replace_all: { type: :boolean, required: false, default: false,
|
|
12
|
+
path: { type: :string, required: true, description: 'Path to the file to edit' },
|
|
13
|
+
old_text: { type: :string, required: true, description: 'The exact text to find and replace' },
|
|
14
|
+
new_text: { type: :string, required: true, description: 'The replacement text' },
|
|
15
|
+
replace_all: { type: :boolean, required: false, default: false,
|
|
16
|
+
description: 'Replace all occurrences (default: false)' }
|
|
16
17
|
}.freeze
|
|
17
18
|
RISK_LEVEL = :write
|
|
18
19
|
REQUIRES_CONFIRMATION = false
|
|
@@ -23,12 +24,11 @@ module RubynCode
|
|
|
23
24
|
|
|
24
25
|
occurrences = content.scan(old_text).length
|
|
25
26
|
|
|
26
|
-
if occurrences.zero?
|
|
27
|
-
raise Error, "old_text not found in #{path}. No changes made."
|
|
28
|
-
end
|
|
27
|
+
raise Error, "old_text not found in #{path}. No changes made." if occurrences.zero?
|
|
29
28
|
|
|
30
29
|
if !replace_all && occurrences > 1
|
|
31
|
-
raise Error,
|
|
30
|
+
raise Error,
|
|
31
|
+
"old_text found #{occurrences} times in #{path}. Use replace_all: true to replace all, or provide a more specific old_text."
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
new_content = if replace_all
|
|
@@ -20,7 +20,12 @@ module RubynCode
|
|
|
20
20
|
inject_dependencies(tool, tool_name)
|
|
21
21
|
|
|
22
22
|
symbolized = params.transform_keys(&:to_sym)
|
|
23
|
-
|
|
23
|
+
# Filter to only params the tool's execute method accepts — LLM may send extra keys
|
|
24
|
+
allowed = tool.method(:execute).parameters
|
|
25
|
+
.slice(:key, :keyreq)
|
|
26
|
+
.map(&:last)
|
|
27
|
+
filtered = allowed.empty? ? symbolized : symbolized.slice(*allowed)
|
|
28
|
+
result = tool.execute(**filtered)
|
|
24
29
|
tool.truncate(result.to_s)
|
|
25
30
|
rescue ToolNotFoundError => e
|
|
26
31
|
error_result("Tool error: #{e.message}")
|
|
@@ -42,14 +47,14 @@ module RubynCode
|
|
|
42
47
|
|
|
43
48
|
def inject_dependencies(tool, tool_name)
|
|
44
49
|
case tool_name
|
|
45
|
-
when
|
|
50
|
+
when 'spawn_agent'
|
|
46
51
|
tool.llm_client = @llm_client if tool.respond_to?(:llm_client=)
|
|
47
52
|
tool.on_status = @on_agent_status if tool.respond_to?(:on_status=)
|
|
48
|
-
when
|
|
53
|
+
when 'spawn_teammate'
|
|
49
54
|
tool.llm_client = @llm_client if tool.respond_to?(:llm_client=)
|
|
50
55
|
tool.on_status = @on_agent_status if tool.respond_to?(:on_status=)
|
|
51
56
|
tool.db = @db if tool.respond_to?(:db=)
|
|
52
|
-
when
|
|
57
|
+
when 'background_run'
|
|
53
58
|
tool.background_worker = @background_worker if tool.respond_to?(:background_worker=)
|
|
54
59
|
end
|
|
55
60
|
end
|
|
@@ -1,70 +1,65 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class GitCommit < Base
|
|
10
|
-
TOOL_NAME =
|
|
10
|
+
TOOL_NAME = 'git_commit'
|
|
11
11
|
DESCRIPTION = "Stage files and create a git commit. Specify files to stage or use 'all' to stage everything."
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
message: { type: :string, required: true, description:
|
|
14
|
-
files: { type: :string, required: false, default:
|
|
13
|
+
message: { type: :string, required: true, description: 'The commit message' },
|
|
14
|
+
files: { type: :string, required: false, default: 'all',
|
|
15
|
+
description: "Space-separated file paths to stage, or 'all' to stage everything (git add -A)" }
|
|
15
16
|
}.freeze
|
|
16
17
|
RISK_LEVEL = :write
|
|
17
18
|
REQUIRES_CONFIRMATION = true
|
|
18
19
|
|
|
19
|
-
def execute(message:, files:
|
|
20
|
+
def execute(message:, files: 'all')
|
|
20
21
|
validate_git_repo!
|
|
21
22
|
validate_message!(message)
|
|
22
23
|
|
|
23
24
|
stage_files(files)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
commit_output
|
|
25
|
+
create_commit(message)
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
private
|
|
30
29
|
|
|
31
30
|
def validate_git_repo!
|
|
32
|
-
_, _, status =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
_, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
|
|
32
|
+
return if status.success?
|
|
33
|
+
|
|
34
|
+
raise Error, "Not a git repository: #{project_root}"
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def validate_message!(message)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
return unless message.nil? || message.strip.empty?
|
|
39
|
+
|
|
40
|
+
raise Error, 'Commit message cannot be empty'
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def stage_files(files)
|
|
45
|
-
if files.strip.downcase ==
|
|
46
|
-
|
|
44
|
+
if files.strip.downcase == 'all'
|
|
45
|
+
_, stderr, status = safe_capture3('git', 'add', '-A', chdir: project_root)
|
|
47
46
|
else
|
|
48
47
|
file_list = files.split(/\s+/).reject(&:empty?)
|
|
49
|
-
if file_list.empty?
|
|
50
|
-
raise Error, "No files specified to stage"
|
|
51
|
-
end
|
|
48
|
+
raise Error, 'No files specified to stage' if file_list.empty?
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
_, stderr, status = safe_capture3('git', 'add', '--', *file_list, chdir: project_root)
|
|
54
51
|
end
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
return if status.success?
|
|
54
|
+
|
|
55
|
+
raise Error, "Failed to stage files: #{stderr.strip}"
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
def create_commit(message)
|
|
62
|
-
stdout, stderr, status =
|
|
59
|
+
stdout, stderr, status = safe_capture3('git', 'commit', '-m', message, chdir: project_root)
|
|
63
60
|
|
|
64
61
|
unless status.success?
|
|
65
|
-
if stderr.include?(
|
|
66
|
-
return "Nothing to commit — working tree is clean."
|
|
67
|
-
end
|
|
62
|
+
return 'Nothing to commit — working tree is clean.' if stderr.include?('nothing to commit')
|
|
68
63
|
|
|
69
64
|
raise Error, "Commit failed: #{stderr.strip}"
|
|
70
65
|
end
|
|
@@ -75,20 +70,20 @@ module RubynCode
|
|
|
75
70
|
|
|
76
71
|
lines = ["Committed on branch: #{branch}"]
|
|
77
72
|
lines << "Commit: #{commit_hash}" if commit_hash
|
|
78
|
-
lines <<
|
|
73
|
+
lines << ''
|
|
79
74
|
lines << stdout.strip
|
|
80
75
|
|
|
81
76
|
lines.join("\n")
|
|
82
77
|
end
|
|
83
78
|
|
|
84
79
|
def extract_commit_hash
|
|
85
|
-
stdout, _, status =
|
|
80
|
+
stdout, _, status = safe_capture3('git', 'rev-parse', '--short', 'HEAD', chdir: project_root)
|
|
86
81
|
status.success? ? stdout.strip : nil
|
|
87
82
|
end
|
|
88
83
|
|
|
89
84
|
def current_branch
|
|
90
|
-
stdout, _, status =
|
|
91
|
-
status.success? && !stdout.strip.empty? ? stdout.strip :
|
|
85
|
+
stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
|
|
86
|
+
status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
|
|
92
87
|
end
|
|
93
88
|
end
|
|
94
89
|
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class GitDiff < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'git_diff'
|
|
11
|
+
DESCRIPTION = 'Show git diff for staged, unstaged, or between branches/commits.'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
target: { type: :string, required: false, default:
|
|
13
|
+
target: { type: :string, required: false, default: 'unstaged',
|
|
14
|
+
description: 'What to diff: "staged", "unstaged", or a branch/commit ref (default: "unstaged")' }
|
|
14
15
|
}.freeze
|
|
15
16
|
RISK_LEVEL = :read
|
|
16
17
|
REQUIRES_CONFIRMATION = false
|
|
17
18
|
|
|
18
19
|
MAX_DIFF_LENGTH = 80_000
|
|
19
20
|
|
|
20
|
-
def execute(target:
|
|
21
|
+
def execute(target: 'unstaged')
|
|
21
22
|
validate_git_repo!
|
|
22
23
|
|
|
23
24
|
cmd = build_diff_command(target.to_s.strip)
|
|
24
|
-
stdout, stderr, status =
|
|
25
|
+
stdout, stderr, status = safe_capture3(*cmd, chdir: project_root)
|
|
25
26
|
|
|
26
|
-
unless status.success?
|
|
27
|
-
raise Error, "git diff failed: #{stderr.strip}"
|
|
28
|
-
end
|
|
27
|
+
raise Error, "git diff failed: #{stderr.strip}" unless status.success?
|
|
29
28
|
|
|
30
29
|
if stdout.strip.empty?
|
|
31
30
|
"No differences found for target: #{target}"
|
|
@@ -38,20 +37,20 @@ module RubynCode
|
|
|
38
37
|
private
|
|
39
38
|
|
|
40
39
|
def validate_git_repo!
|
|
41
|
-
_, _, status =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
_, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
|
|
41
|
+
return if status.success?
|
|
42
|
+
|
|
43
|
+
raise Error, "Not a git repository: #{project_root}"
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def build_diff_command(target)
|
|
48
47
|
case target.downcase
|
|
49
|
-
when
|
|
48
|
+
when 'staged', 'cached'
|
|
50
49
|
%w[git diff --cached]
|
|
51
|
-
when
|
|
50
|
+
when 'unstaged', ''
|
|
52
51
|
%w[git diff]
|
|
53
52
|
else
|
|
54
|
-
[
|
|
53
|
+
['git', 'diff', target]
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class GitLog < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'git_log'
|
|
11
|
+
DESCRIPTION = 'Show recent git commit history.'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
count: { type: :integer, required: false, default: 20, description:
|
|
14
|
-
branch: { type: :string, required: false, description:
|
|
13
|
+
count: { type: :integer, required: false, default: 20, description: 'Number of commits to show (default: 20)' },
|
|
14
|
+
branch: { type: :string, required: false, description: 'Branch name to show log for (default: current branch)' }
|
|
15
15
|
}.freeze
|
|
16
16
|
RISK_LEVEL = :read
|
|
17
17
|
REQUIRES_CONFIRMATION = false
|
|
@@ -21,17 +21,15 @@ module RubynCode
|
|
|
21
21
|
|
|
22
22
|
count = [[count.to_i, 1].max, 200].min
|
|
23
23
|
|
|
24
|
-
cmd = [
|
|
24
|
+
cmd = ['git', 'log', '--oneline', "-#{count}"]
|
|
25
25
|
cmd << branch unless branch.nil? || branch.strip.empty?
|
|
26
26
|
|
|
27
|
-
stdout, stderr, status =
|
|
27
|
+
stdout, stderr, status = safe_capture3(*cmd, chdir: project_root)
|
|
28
28
|
|
|
29
|
-
unless status.success?
|
|
30
|
-
raise Error, "git log failed: #{stderr.strip}"
|
|
31
|
-
end
|
|
29
|
+
raise Error, "git log failed: #{stderr.strip}" unless status.success?
|
|
32
30
|
|
|
33
31
|
if stdout.strip.empty?
|
|
34
|
-
|
|
32
|
+
'No commits found.'
|
|
35
33
|
else
|
|
36
34
|
current = current_branch
|
|
37
35
|
header = "Commit history#{branch ? " (#{branch})" : " (#{current})"}:\n\n"
|
|
@@ -42,15 +40,15 @@ module RubynCode
|
|
|
42
40
|
private
|
|
43
41
|
|
|
44
42
|
def validate_git_repo!
|
|
45
|
-
_, _, status =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
_, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
|
|
44
|
+
return if status.success?
|
|
45
|
+
|
|
46
|
+
raise Error, "Not a git repository: #{project_root}"
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def current_branch
|
|
52
|
-
stdout, _, status =
|
|
53
|
-
status.success? && !stdout.strip.empty? ? stdout.strip :
|
|
50
|
+
stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
|
|
51
|
+
status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
|
|
54
52
|
end
|
|
55
53
|
end
|
|
56
54
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class GitStatus < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'git_status'
|
|
11
|
+
DESCRIPTION = 'Show the current git status — modified, staged, and untracked files.'
|
|
12
12
|
PARAMETERS = {}.freeze
|
|
13
13
|
RISK_LEVEL = :read
|
|
14
14
|
REQUIRES_CONFIRMATION = false
|
|
@@ -21,11 +21,11 @@ module RubynCode
|
|
|
21
21
|
|
|
22
22
|
lines = ["Branch: #{branch}\n"]
|
|
23
23
|
|
|
24
|
-
if status_output.strip.empty?
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
lines << if status_output.strip.empty?
|
|
25
|
+
'Working tree is clean — nothing to commit.'
|
|
26
|
+
else
|
|
27
|
+
status_output
|
|
28
|
+
end
|
|
29
29
|
|
|
30
30
|
lines.join("\n")
|
|
31
31
|
end
|
|
@@ -33,22 +33,20 @@ module RubynCode
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def validate_git_repo!
|
|
36
|
-
_, _, status =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
_, _, status = safe_capture3('git', 'rev-parse', '--is-inside-work-tree', chdir: project_root)
|
|
37
|
+
return if status.success?
|
|
38
|
+
|
|
39
|
+
raise Error, "Not a git repository: #{project_root}"
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def current_branch
|
|
43
|
-
stdout, _, status =
|
|
44
|
-
status.success? && !stdout.strip.empty? ? stdout.strip :
|
|
43
|
+
stdout, _, status = safe_capture3('git', 'branch', '--show-current', chdir: project_root)
|
|
44
|
+
status.success? && !stdout.strip.empty? ? stdout.strip : 'HEAD (detached)'
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def git_status
|
|
48
|
-
stdout, stderr, status =
|
|
49
|
-
unless status.success?
|
|
50
|
-
raise Error, "git status failed: #{stderr.strip}"
|
|
51
|
-
end
|
|
48
|
+
stdout, stderr, status = safe_capture3('git', 'status', '--short', chdir: project_root)
|
|
49
|
+
raise Error, "git status failed: #{stderr.strip}" unless status.success?
|
|
52
50
|
|
|
53
51
|
stdout
|
|
54
52
|
end
|