fantasy-cli 1.2.6

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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +456 -0
  4. data/bin/gsd +8 -0
  5. data/bin/gsd-core-darwin-amd64 +0 -0
  6. data/bin/gsd-core-darwin-arm64 +0 -0
  7. data/bin/gsd-core-linux-amd64 +0 -0
  8. data/bin/gsd-core-linux-arm64 +0 -0
  9. data/bin/gsd-core-windows-amd64.exe +0 -0
  10. data/bin/gsd-core-windows-arm64.exe +0 -0
  11. data/bin/gsd-core.exe +0 -0
  12. data/lib/gsd/agents/coordinator.rb +195 -0
  13. data/lib/gsd/agents/task_manager.rb +158 -0
  14. data/lib/gsd/agents/worker.rb +162 -0
  15. data/lib/gsd/agents.rb +30 -0
  16. data/lib/gsd/ai/chat.rb +486 -0
  17. data/lib/gsd/ai/cli.rb +248 -0
  18. data/lib/gsd/ai/command_parser.rb +97 -0
  19. data/lib/gsd/ai/commands/base.rb +42 -0
  20. data/lib/gsd/ai/commands/clear.rb +20 -0
  21. data/lib/gsd/ai/commands/context.rb +30 -0
  22. data/lib/gsd/ai/commands/cost.rb +30 -0
  23. data/lib/gsd/ai/commands/export.rb +42 -0
  24. data/lib/gsd/ai/commands/help.rb +61 -0
  25. data/lib/gsd/ai/commands/model.rb +67 -0
  26. data/lib/gsd/ai/commands/reset.rb +22 -0
  27. data/lib/gsd/ai/config.rb +256 -0
  28. data/lib/gsd/ai/context.rb +324 -0
  29. data/lib/gsd/ai/cost_tracker.rb +361 -0
  30. data/lib/gsd/ai/git_context.rb +169 -0
  31. data/lib/gsd/ai/history.rb +384 -0
  32. data/lib/gsd/ai/providers/anthropic.rb +429 -0
  33. data/lib/gsd/ai/providers/base.rb +282 -0
  34. data/lib/gsd/ai/providers/lmstudio.rb +279 -0
  35. data/lib/gsd/ai/providers/ollama.rb +336 -0
  36. data/lib/gsd/ai/providers/openai.rb +396 -0
  37. data/lib/gsd/ai/providers/openrouter.rb +429 -0
  38. data/lib/gsd/ai/reference_resolver.rb +225 -0
  39. data/lib/gsd/ai/repl.rb +349 -0
  40. data/lib/gsd/ai/streaming.rb +438 -0
  41. data/lib/gsd/ai/ui.rb +429 -0
  42. data/lib/gsd/buddy/cli.rb +284 -0
  43. data/lib/gsd/buddy/gacha.rb +148 -0
  44. data/lib/gsd/buddy/renderer.rb +108 -0
  45. data/lib/gsd/buddy/species.rb +190 -0
  46. data/lib/gsd/buddy/stats.rb +156 -0
  47. data/lib/gsd/buddy.rb +28 -0
  48. data/lib/gsd/cli.rb +455 -0
  49. data/lib/gsd/commands.rb +198 -0
  50. data/lib/gsd/config.rb +183 -0
  51. data/lib/gsd/error.rb +188 -0
  52. data/lib/gsd/frontmatter.rb +123 -0
  53. data/lib/gsd/go/bridge.rb +173 -0
  54. data/lib/gsd/history.rb +76 -0
  55. data/lib/gsd/milestone.rb +75 -0
  56. data/lib/gsd/output.rb +184 -0
  57. data/lib/gsd/phase.rb +102 -0
  58. data/lib/gsd/plugins/base.rb +92 -0
  59. data/lib/gsd/plugins/cli.rb +330 -0
  60. data/lib/gsd/plugins/config.rb +164 -0
  61. data/lib/gsd/plugins/hooks.rb +132 -0
  62. data/lib/gsd/plugins/installer.rb +158 -0
  63. data/lib/gsd/plugins/loader.rb +122 -0
  64. data/lib/gsd/plugins/manager.rb +187 -0
  65. data/lib/gsd/plugins/marketplace.rb +142 -0
  66. data/lib/gsd/plugins/sandbox.rb +114 -0
  67. data/lib/gsd/plugins/search.rb +131 -0
  68. data/lib/gsd/plugins/validator.rb +157 -0
  69. data/lib/gsd/plugins.rb +48 -0
  70. data/lib/gsd/profile.rb +127 -0
  71. data/lib/gsd/research.rb +85 -0
  72. data/lib/gsd/roadmap.rb +90 -0
  73. data/lib/gsd/skills/bundled/commit.md +58 -0
  74. data/lib/gsd/skills/bundled/debug.md +28 -0
  75. data/lib/gsd/skills/bundled/explain.md +41 -0
  76. data/lib/gsd/skills/bundled/plan.md +42 -0
  77. data/lib/gsd/skills/bundled/verify.md +26 -0
  78. data/lib/gsd/skills/loader.rb +189 -0
  79. data/lib/gsd/state.rb +102 -0
  80. data/lib/gsd/template.rb +106 -0
  81. data/lib/gsd/tools/ask_user_question.rb +179 -0
  82. data/lib/gsd/tools/base.rb +204 -0
  83. data/lib/gsd/tools/bash.rb +246 -0
  84. data/lib/gsd/tools/file_edit.rb +297 -0
  85. data/lib/gsd/tools/file_read.rb +199 -0
  86. data/lib/gsd/tools/file_write.rb +153 -0
  87. data/lib/gsd/tools/glob.rb +202 -0
  88. data/lib/gsd/tools/grep.rb +227 -0
  89. data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
  90. data/lib/gsd/tools/gsd_phase.rb +140 -0
  91. data/lib/gsd/tools/gsd_roadmap.rb +108 -0
  92. data/lib/gsd/tools/gsd_state.rb +143 -0
  93. data/lib/gsd/tools/gsd_template.rb +157 -0
  94. data/lib/gsd/tools/gsd_verify.rb +159 -0
  95. data/lib/gsd/tools/registry.rb +103 -0
  96. data/lib/gsd/tools/task.rb +235 -0
  97. data/lib/gsd/tools/todo_write.rb +290 -0
  98. data/lib/gsd/tools/web.rb +260 -0
  99. data/lib/gsd/tui/app.rb +366 -0
  100. data/lib/gsd/tui/auto_complete.rb +79 -0
  101. data/lib/gsd/tui/colors.rb +111 -0
  102. data/lib/gsd/tui/command_palette.rb +126 -0
  103. data/lib/gsd/tui/header.rb +38 -0
  104. data/lib/gsd/tui/input_box.rb +199 -0
  105. data/lib/gsd/tui/spinner.rb +40 -0
  106. data/lib/gsd/tui/status_bar.rb +51 -0
  107. data/lib/gsd/tui.rb +17 -0
  108. data/lib/gsd/validator.rb +216 -0
  109. data/lib/gsd/verify.rb +175 -0
  110. data/lib/gsd/version.rb +5 -0
  111. data/lib/gsd/workstream.rb +91 -0
  112. metadata +231 -0
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+
5
+ module Gsd
6
+ module Tools
7
+ # AskUserQuestionTool - Faz perguntas ao usuário e aguarda resposta
8
+ #
9
+ # Uso:
10
+ # tool = AskUserQuestionTool.new(cwd: '/path')
11
+ # result = tool.execute(question: 'Qual arquivo editar?', options: ['a.rb', 'b.rb'])
12
+ class AskUserQuestionTool < Base
13
+ class << self
14
+ tool_name('ask_user_question')
15
+ tool_description('Ask the user a question and wait for their response')
16
+ tool_input_schema({
17
+ type: 'object',
18
+ properties: {
19
+ question: {
20
+ type: 'string',
21
+ description: 'The question to ask the user'
22
+ },
23
+ options: {
24
+ type: 'array',
25
+ description: 'Optional list of options for the user to choose from',
26
+ items: {
27
+ type: 'string'
28
+ }
29
+ },
30
+ allow_multiple: {
31
+ type: 'boolean',
32
+ description: 'Allow multiple selections (default: false)',
33
+ default: false
34
+ },
35
+ default: {
36
+ type: 'string',
37
+ description: 'Default value if user presses Enter'
38
+ },
39
+ timeout: {
40
+ type: 'integer',
41
+ description: 'Timeout in seconds (default: 300, 0 for no timeout)',
42
+ default: 300
43
+ }
44
+ },
45
+ required: ['question']
46
+ })
47
+ end
48
+
49
+ # Faz pergunta ao usuário
50
+ #
51
+ # @param args [Hash] Argumentos
52
+ # @return [Hash] Resposta do usuário
53
+ def execute(args)
54
+ question = args[:question] || args['question']
55
+ options = args[:options] || args['options']
56
+ allow_multiple = args[:allow_multiple] || args['allow_multiple'] || false
57
+ default = args[:default] || args['default']
58
+ timeout = args[:timeout] || args['timeout'] || 300
59
+
60
+ raise ArgumentError, 'Question is required' unless question
61
+
62
+ log_debug("Asking: #{question}")
63
+ log_debug("Options: #{options.inspect}") if options
64
+
65
+ # Exibe pergunta
66
+ puts "\n" + "=" * 60
67
+ puts "❓ #{question}"
68
+ puts "=" * 60
69
+
70
+ # Exibe opções se existirem
71
+ if options && options.any?
72
+ options.each_with_index do |opt, i|
73
+ puts " [#{i + 1}] #{opt}"
74
+ end
75
+
76
+ if allow_multiple
77
+ puts "\nDigite os números separados por vírgula (ex: 1,3,5)"
78
+ else
79
+ puts "\nDigite o número da opção desejada"
80
+ end
81
+
82
+ puts "Ou pressione Enter para usar o default#{default ? " (#{default})" : ''}"
83
+ else
84
+ puts "\nDigite sua resposta:"
85
+ end
86
+
87
+ # Lê resposta com timeout
88
+ answer = read_input_with_timeout(timeout)
89
+
90
+ # Processa resposta
91
+ result = process_answer(answer, options, allow_multiple, default)
92
+
93
+ puts "\n✅ Resposta registrada: #{result[:answer]}"
94
+ puts "=" * 60 + "\n"
95
+
96
+ result
97
+ end
98
+
99
+ # Verifica se é safe
100
+ #
101
+ # @param args [Hash] Argumentos
102
+ # @return [Boolean] true
103
+ def self.safe?(args)
104
+ true
105
+ end
106
+
107
+ # Verifica se é read-only
108
+ #
109
+ # @param args [Hash] Argumentos
110
+ # @return [Boolean] true
111
+ def self.read_only?(args)
112
+ true
113
+ end
114
+
115
+ private
116
+
117
+ # Lê input com timeout
118
+ #
119
+ # @param timeout [Integer] Timeout em segundos
120
+ # @return [String] Input do usuário
121
+ def read_input_with_timeout(timeout)
122
+ require 'io/select'
123
+
124
+ if timeout > 0
125
+ # Usa IO.select para timeout
126
+ if IO.select([$stdin], nil, nil, timeout)
127
+ $stdin.gets&.chomp
128
+ else
129
+ puts "\n⏰ Timeout atingido!"
130
+ ''
131
+ end
132
+ else
133
+ # Sem timeout
134
+ $stdin.gets&.chomp
135
+ end
136
+ end
137
+
138
+ # Processa resposta do usuário
139
+ #
140
+ # @param answer [String] Resposta do usuário
141
+ # @param options [Array] Opções disponíveis
142
+ # @param allow_multiple [Boolean] Permite múltiplas
143
+ # @param default [String] Valor default
144
+ # @return [Hash] Resposta processada
145
+ def process_answer(answer, options, allow_multiple, default)
146
+ # Usa default se resposta vazia
147
+ if answer.nil? || answer.strip.empty?
148
+ return {
149
+ answer: default,
150
+ selected_options: default ? [default] : [],
151
+ indices: []
152
+ }
153
+ end
154
+
155
+ # Se há opções, converte índices
156
+ if options && options.any?
157
+ indices = answer.split(',').map(&:strip).map(&:to_i)
158
+ selected = indices.map { |i| options[i - 1] }.compact
159
+
160
+ return {
161
+ answer: allow_multiple ? selected.join(', ') : selected.first,
162
+ selected_options: selected,
163
+ indices: indices
164
+ }
165
+ end
166
+
167
+ # Resposta livre
168
+ {
169
+ answer: answer,
170
+ selected_options: [answer],
171
+ indices: []
172
+ }
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # Registra a tool
179
+ Gsd::Tools::Registry.register('ask_user_question', Gsd::Tools::AskUserQuestionTool, category: :user)
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Gsd
6
+ module Tools
7
+ # Base Tool Class - Classe base para todas as tools
8
+ #
9
+ # Todas as tools devem herdar desta classe e implementar:
10
+ # - name (nome da tool)
11
+ # - description (descrição)
12
+ # - input_schema (schema JSON dos inputs)
13
+ # - execute (execução da tool)
14
+ class Base
15
+ class << self
16
+ attr_accessor :name, :description, :input_schema
17
+
18
+ # Define o nome da tool
19
+ #
20
+ # @param name [String] Nome da tool
21
+ # @return [void]
22
+ def tool_name(name)
23
+ @name = name
24
+ end
25
+
26
+ # Define a descrição da tool
27
+ #
28
+ # @param desc [String] Descrição
29
+ # @return [void]
30
+ def tool_description(desc)
31
+ @description = desc
32
+ end
33
+
34
+ # Define o schema de input da tool
35
+ #
36
+ # @param schema [Hash] Schema JSON
37
+ # @return [void]
38
+ def tool_input_schema(schema)
39
+ @input_schema = schema
40
+ end
41
+
42
+ # Verifica se a tool é segura para execução automática
43
+ #
44
+ # @param args [Hash] Argumentos
45
+ # @return [Boolean] true se segura
46
+ def safe?(args = {})
47
+ false
48
+ end
49
+
50
+ # Verifica se a tool é read-only
51
+ #
52
+ # @param args [Hash] Argumentos
53
+ # @return [Boolean] true se read-only
54
+ def read_only?(args = {})
55
+ false
56
+ end
57
+
58
+ # Verifica se a tool é destrutiva
59
+ #
60
+ # @param args [Hash] Argumentos
61
+ # @return [Boolean] true se destrutiva
62
+ def destructive?(args = {})
63
+ false
64
+ end
65
+ end
66
+
67
+ # Inicializa a tool
68
+ #
69
+ # @param cwd [String] Diretório de trabalho
70
+ # @param debug [Boolean] Habilitar debug
71
+ def initialize(cwd: Dir.pwd, debug: false)
72
+ @cwd = cwd
73
+ @debug = debug
74
+ end
75
+
76
+ # Executa a tool
77
+ #
78
+ # @param args [Hash] Argumentos da tool
79
+ # @return [Object] Resultado da execução
80
+ def execute(args)
81
+ raise NotImplementedError, 'Subclasses must implement #execute'
82
+ end
83
+
84
+ # Valida os argumentos da tool
85
+ #
86
+ # @param args [Hash] Argumentos
87
+ # @return [Boolean] true se válido
88
+ def validate(args)
89
+ return true unless self.class.input_schema
90
+
91
+ # Validação básica
92
+ required = self.class.input_schema['required'] || []
93
+ properties = self.class.input_schema['properties'] || {}
94
+
95
+ required.each do |field|
96
+ return false unless args.key?(field.to_sym) || args.key?(field.to_s)
97
+ end
98
+
99
+ true
100
+ end
101
+
102
+ # Log de debug
103
+ #
104
+ # @param message [String] Mensagem de log
105
+ # @return [void]
106
+ def log_debug(message)
107
+ return unless @debug
108
+
109
+ puts "[#{self.class.name}] #{message}"
110
+ end
111
+
112
+ # Executa comando shell com segurança
113
+ #
114
+ # @param command [String] Comando
115
+ # @param timeout [Integer] Timeout em segundos
116
+ # @return [Hash] Resultado (stdout, stderr, status)
117
+ def run_shell_command(command, timeout: 30)
118
+ require 'open3'
119
+
120
+ log_debug("Running: #{command}")
121
+
122
+ stdout, stderr, status = Open3.capture3(command, chdir: @cwd)
123
+
124
+ {
125
+ stdout: stdout,
126
+ stderr: stderr,
127
+ status: status.exitstatus,
128
+ success: status.success?
129
+ }
130
+ rescue => e
131
+ log_debug("Error: #{e.message}")
132
+ {
133
+ stdout: '',
134
+ stderr: e.message,
135
+ status: -1,
136
+ success: false
137
+ }
138
+ end
139
+
140
+ # Lê arquivo com limite de tamanho
141
+ #
142
+ # @param path [String] Caminho do arquivo
143
+ # @param max_size [Integer] Tamanho máximo em bytes
144
+ # @return [String] Conteúdo do arquivo
145
+ def read_file(path, max_size: 1_000_000)
146
+ full_path = File.expand_path(path, @cwd)
147
+
148
+ raise "File not found: #{path}" unless File.exist?(full_path)
149
+
150
+ size = File.size(full_path)
151
+ raise "File too large: #{size} bytes (max: #{max_size})" if size > max_size
152
+
153
+ File.read(full_path)
154
+ end
155
+
156
+ # Escreve arquivo com backup
157
+ #
158
+ # @param path [String] Caminho do arquivo
159
+ # @param content [String] Conteúdo
160
+ # @param create_backup [Boolean] Criar backup
161
+ # @return [void]
162
+ def write_file(path, content, create_backup: true)
163
+ full_path = File.expand_path(path, @cwd)
164
+
165
+ # Cria diretórios se necessário
166
+ FileUtils.mkdir_p(File.dirname(full_path))
167
+
168
+ # Backup se arquivo existe
169
+ if File.exist?(full_path) && create_backup
170
+ backup_path = "#{full_path}.bak.#{Time.now.to_i}"
171
+ File.rename(full_path, backup_path)
172
+ log_debug("Backup created: #{backup_path}")
173
+ end
174
+
175
+ File.write(full_path, content)
176
+ log_debug("File written: #{full_path}")
177
+ end
178
+
179
+ # Verifica se path existe
180
+ #
181
+ # @param path [String] Caminho
182
+ # @return [Boolean] true se existe
183
+ def path_exists?(path)
184
+ full_path = File.expand_path(path, @cwd)
185
+ File.exist?(full_path)
186
+ end
187
+
188
+ # Normaliza path (previne directory traversal)
189
+ #
190
+ # @param path [String] Caminho
191
+ # @return [String] Path normalizado
192
+ def normalize_path(path)
193
+ full_path = File.expand_path(path, @cwd)
194
+
195
+ # Verifica se está dentro do cwd
196
+ unless full_path.start_with?(File.expand_path(@cwd))
197
+ raise "Path outside working directory: #{path}"
198
+ end
199
+
200
+ full_path
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+ require 'open3'
5
+
6
+ module Gsd
7
+ module Tools
8
+ # BashTool - Executa comandos shell
9
+ #
10
+ # Uso:
11
+ # tool = BashTool.new(cwd: '/path')
12
+ # result = tool.execute(command: 'ls -la')
13
+ class BashTool < Base
14
+ class << self
15
+ tool_name('bash')
16
+ tool_description('Execute shell commands with output streaming')
17
+ tool_input_schema({
18
+ type: 'object',
19
+ properties: {
20
+ command: {
21
+ type: 'string',
22
+ description: 'The shell command to execute'
23
+ },
24
+ timeout: {
25
+ type: 'integer',
26
+ description: 'Timeout in seconds (default: 30)',
27
+ default: 30
28
+ },
29
+ stream: {
30
+ type: 'boolean',
31
+ description: 'Stream output in real-time (default: false)',
32
+ default: false
33
+ }
34
+ },
35
+ required: ['command']
36
+ })
37
+ end
38
+
39
+ # Comandos bloqueados por segurança
40
+ BLOCKED_COMMANDS = [
41
+ 'rm -rf /',
42
+ 'mkfs',
43
+ 'dd if=/dev/zero',
44
+ ':(){:|:&};:',
45
+ 'chmod -R 000',
46
+ 'chown -R'
47
+ ].freeze
48
+
49
+ # Comandos permitidos sem confirmação
50
+ SAFE_COMMANDS = [
51
+ 'ls', 'dir', 'pwd', 'echo', 'cat', 'head', 'tail',
52
+ 'grep', 'find', 'which', 'whoami', 'date', 'git',
53
+ 'npm', 'yarn', 'bundle', 'gem', 'go', 'cargo',
54
+ 'ruby', 'python', 'node', 'make'
55
+ ].freeze
56
+
57
+ # Executa comando bash
58
+ #
59
+ # @param args [Hash] Argumentos
60
+ # @return [Hash] Resultado da execução
61
+ def execute(args)
62
+ command = args[:command] || args['command']
63
+ timeout = args[:timeout] || args['timeout'] || 30
64
+ stream = args[:stream] || args['stream'] || false
65
+
66
+ raise ArgumentError, 'Command is required' unless command
67
+
68
+ # Validações de segurança
69
+ validate_command(command)
70
+
71
+ log_debug("Executing: #{command}")
72
+ log_debug("Timeout: #{timeout}s, Stream: #{stream}")
73
+
74
+ if stream
75
+ execute_with_stream(command, timeout)
76
+ else
77
+ execute_sync(command, timeout)
78
+ end
79
+ end
80
+
81
+ # Verifica se comando é seguro
82
+ #
83
+ # @param args [Hash] Argumentos
84
+ # @return [Boolean] true se seguro
85
+ def self.safe?(args)
86
+ command = args[:command] || args['command'] || ''
87
+ SAFE_COMMANDS.any? { |safe| command.start_with?(safe) }
88
+ end
89
+
90
+ # Verifica se é read-only
91
+ #
92
+ # @param args [Hash] Argumentos
93
+ # @return [Boolean] true se read-only
94
+ def self.read_only?(args)
95
+ command = args[:command] || args['command'] || ''
96
+ # Comandos que não modificam arquivos
97
+ ['ls', 'dir', 'pwd', 'cat', 'head', 'tail', 'grep', 'find', 'which', 'whoami', 'date'].any? { |c| command.start_with?(c) }
98
+ end
99
+
100
+ # Verifica se é destrutivo
101
+ #
102
+ # @param args [Hash] Argumentos
103
+ # @return [Boolean] true se destrutivo
104
+ def self.destructive?(args)
105
+ command = args[:command] || args['command'] || ''
106
+ # Comandos que podem modificar/deletar
107
+ ['rm', 'mv', 'cp', 'chmod', 'chown', 'mkdir', 'touch'].any? { |c| command.start_with?(c) }
108
+ end
109
+
110
+ private
111
+
112
+ # Valida comando por segurança
113
+ #
114
+ # @param command [String] Comando
115
+ # @raise [SecurityError] Se comando for perigoso
116
+ def validate_command(command)
117
+ # Verifica comandos bloqueados
118
+ BLOCKED_COMMANDS.each do |blocked|
119
+ if command.include?(blocked)
120
+ raise SecurityError, "Command blocked for security: #{blocked}"
121
+ end
122
+ end
123
+
124
+ # Verifica se tenta acessar paths fora do cwd
125
+ if command.include?('..') && !command.start_with?('git')
126
+ log_debug("Warning: Command contains '..'")
127
+ end
128
+
129
+ # Verifica se é sudo
130
+ if command.start_with?('sudo')
131
+ raise SecurityError, 'Sudo commands are not allowed'
132
+ end
133
+ end
134
+
135
+ # Executa comando com sync
136
+ #
137
+ # @param command [String] Comando
138
+ # @param timeout [Integer] Timeout
139
+ # @return [Hash] Resultado
140
+ def execute_sync(command, timeout)
141
+ start_time = Time.now
142
+
143
+ stdout, stderr, status = Open3.capture3(command, chdir: @cwd)
144
+
145
+ elapsed = Time.now - start_time
146
+
147
+ {
148
+ stdout: stdout,
149
+ stderr: stderr,
150
+ status: status.exitstatus,
151
+ success: status.success?,
152
+ elapsed: elapsed.round(2),
153
+ command: command
154
+ }
155
+ rescue => e
156
+ log_debug("Error: #{e.message}")
157
+ {
158
+ stdout: '',
159
+ stderr: e.message,
160
+ status: -1,
161
+ success: false,
162
+ elapsed: 0,
163
+ command: command,
164
+ error: e.class.name
165
+ }
166
+ end
167
+
168
+ # Executa comando com streaming
169
+ #
170
+ # @param command [String] Comando
171
+ # @param timeout [Integer] Timeout
172
+ # @return [Hash] Resultado
173
+ def execute_with_stream(command, timeout)
174
+ start_time = Time.now
175
+ stdout_data = +''
176
+ stderr_data = +''
177
+
178
+ Open3.popen3(command, chdir: @cwd) do |stdin, stdout, stderr, wait_thr|
179
+ # Threads para ler stdout e stderr
180
+ out_thread = Thread.new do
181
+ while line = stdout.gets
182
+ stdout_data << line
183
+ print line # Streaming em tempo real
184
+ end
185
+ end
186
+
187
+ err_thread = Thread.new do
188
+ while line = stderr.gets
189
+ stderr_data << line
190
+ $stderr.print line # Streaming em tempo real
191
+ end
192
+ end
193
+
194
+ # Aguarda threads
195
+ out_thread.join(timeout)
196
+ err_thread.join(timeout)
197
+
198
+ # Aguarda processo
199
+ status = wait_thr.value(timeout)
200
+ elapsed = Time.now - start_time
201
+
202
+ puts # Nova linha após streaming
203
+
204
+ {
205
+ stdout: stdout_data,
206
+ stderr: stderr_data,
207
+ status: status.exitstatus,
208
+ success: status.success?,
209
+ elapsed: elapsed.round(2),
210
+ command: command
211
+ }
212
+ end
213
+ rescue Timeout::Error => e
214
+ log_debug("Timeout after #{timeout}s")
215
+ {
216
+ stdout: stdout_data,
217
+ stderr: stderr_data,
218
+ status: -1,
219
+ success: false,
220
+ elapsed: timeout,
221
+ command: command,
222
+ error: 'timeout',
223
+ message: "Command timed out after #{timeout}s"
224
+ }
225
+ rescue => e
226
+ log_debug("Error: #{e.message}")
227
+ {
228
+ stdout: stdout_data,
229
+ stderr: stderr_data,
230
+ status: -1,
231
+ success: false,
232
+ elapsed: 0,
233
+ command: command,
234
+ error: e.class.name,
235
+ message: e.message
236
+ }
237
+ end
238
+ end
239
+
240
+ # Erro de segurança
241
+ class SecurityError < StandardError; end
242
+ end
243
+ end
244
+
245
+ # Registra a tool
246
+ Gsd::Tools::Registry.register('bash', Gsd::Tools::BashTool, category: :shell)