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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- metadata +231 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Gsd
|
|
6
|
+
module AI
|
|
7
|
+
# Streaming Output - Gerencia streaming de respostas da IA
|
|
8
|
+
#
|
|
9
|
+
# Responsável por:
|
|
10
|
+
# - Processar chunks de streaming
|
|
11
|
+
# - Exibir output em tempo real
|
|
12
|
+
# - Bufferizar conteúdo completo
|
|
13
|
+
# - Suportar diferentes formatos de chunk
|
|
14
|
+
# - Streaming real para Anthropic e OpenAI
|
|
15
|
+
class Streaming
|
|
16
|
+
attr_reader :content, :done, :output_io
|
|
17
|
+
|
|
18
|
+
# Inicializa o streaming
|
|
19
|
+
#
|
|
20
|
+
# @param output_io [IO] IO para output (default: STDOUT)
|
|
21
|
+
# @param color [Boolean] Habilitar cores
|
|
22
|
+
def initialize(output_io: $stdout, color: true)
|
|
23
|
+
@output_io = output_io
|
|
24
|
+
@content = +''
|
|
25
|
+
@done = false
|
|
26
|
+
@buffer = +''
|
|
27
|
+
@color = color
|
|
28
|
+
@chunk_count = 0
|
|
29
|
+
@start_time = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Processa um chunk de streaming
|
|
33
|
+
#
|
|
34
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
35
|
+
# @return [void]
|
|
36
|
+
def process(chunk)
|
|
37
|
+
@start_time ||= Time.now
|
|
38
|
+
|
|
39
|
+
case chunk
|
|
40
|
+
when Hash
|
|
41
|
+
process_hash_chunk(chunk)
|
|
42
|
+
when String
|
|
43
|
+
process_string_chunk(chunk)
|
|
44
|
+
else
|
|
45
|
+
# Chunk desconhecido, ignora
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@chunk_count += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Finaliza o streaming
|
|
52
|
+
#
|
|
53
|
+
# @return [String] Conteúdo completo
|
|
54
|
+
def finish
|
|
55
|
+
@done = true
|
|
56
|
+
flush_buffer
|
|
57
|
+
@output_io.puts if @color # Nova linha após o conteúdo
|
|
58
|
+
@content
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reseta o streaming
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def reset
|
|
65
|
+
@content = +''
|
|
66
|
+
@done = false
|
|
67
|
+
@buffer = +''
|
|
68
|
+
@chunk_count = 0
|
|
69
|
+
@start_time = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Retorna o conteúdo acumulado até agora
|
|
73
|
+
#
|
|
74
|
+
# @return [String] Conteúdo acumulado
|
|
75
|
+
def content_so_far
|
|
76
|
+
@content + @buffer
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Retorna estatísticas do streaming
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash] Estatísticas
|
|
82
|
+
def stats
|
|
83
|
+
{
|
|
84
|
+
chunk_count: @chunk_count,
|
|
85
|
+
duration: @start_time ? (Time.now - @start_time) : 0,
|
|
86
|
+
content_length: @content.length,
|
|
87
|
+
avg_chunk_size: @chunk_count > 0 ? @content.length / @chunk_count : 0
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Processa chunk no formato Hash
|
|
94
|
+
#
|
|
95
|
+
# @param chunk [Hash] Chunk do streaming
|
|
96
|
+
# @return [void]
|
|
97
|
+
def process_hash_chunk(chunk)
|
|
98
|
+
# Formato Anthropic: { type: 'content_block_delta', delta: { text: '...' } }
|
|
99
|
+
# Formato OpenAI: { choices: [{ delta: { content: '...' } }] }
|
|
100
|
+
|
|
101
|
+
text = nil
|
|
102
|
+
|
|
103
|
+
if chunk[:type] == 'content_block_delta'
|
|
104
|
+
# Anthropic
|
|
105
|
+
text = chunk.dig(:delta, :text)
|
|
106
|
+
elsif chunk.key?(:choices)
|
|
107
|
+
# OpenAI
|
|
108
|
+
text = chunk.dig(:choices, 0, :delta, :content)
|
|
109
|
+
elsif chunk.key?(:delta)
|
|
110
|
+
# Formato genérico
|
|
111
|
+
text = chunk[:delta].to_s
|
|
112
|
+
elsif chunk.key?(:content)
|
|
113
|
+
# Formato alternativo
|
|
114
|
+
text = chunk[:content]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
add_text(text) if text
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Processa chunk no formato String
|
|
121
|
+
#
|
|
122
|
+
# @param chunk [String] Chunk do streaming
|
|
123
|
+
# @return [void]
|
|
124
|
+
def process_string_chunk(chunk)
|
|
125
|
+
add_text(chunk)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Adiciona texto ao buffer e faz flush se necessário
|
|
129
|
+
#
|
|
130
|
+
# @param text [String] Texto para adicionar
|
|
131
|
+
# @return [void]
|
|
132
|
+
def add_text(text)
|
|
133
|
+
@buffer << text
|
|
134
|
+
|
|
135
|
+
# Flush imediato para terminal
|
|
136
|
+
flush_buffer
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Flush do buffer para output
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
def flush_buffer
|
|
143
|
+
return if @buffer.empty?
|
|
144
|
+
|
|
145
|
+
if @color
|
|
146
|
+
@output_io.print(@buffer)
|
|
147
|
+
else
|
|
148
|
+
@output_io.print(@buffer)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@output_io.flush
|
|
152
|
+
@content << @buffer
|
|
153
|
+
@buffer = +''
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Streaming com animação de typing
|
|
158
|
+
#
|
|
159
|
+
# Subclasse que adiciona efeito de digitação
|
|
160
|
+
class TypingStreaming < Streaming
|
|
161
|
+
attr_accessor :delay, :enabled
|
|
162
|
+
|
|
163
|
+
# Inicializa o streaming com typing
|
|
164
|
+
#
|
|
165
|
+
# @param delay [Float] Delay entre caracteres (segundos)
|
|
166
|
+
# @param output_io [IO] IO para output
|
|
167
|
+
# @param enabled [Boolean] Habilitar animação
|
|
168
|
+
def initialize(delay: 0.01, output_io: $stdout, enabled: true)
|
|
169
|
+
super(output_io: output_io)
|
|
170
|
+
@delay = delay
|
|
171
|
+
@enabled = enabled
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Processa chunk com efeito de typing
|
|
175
|
+
#
|
|
176
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
177
|
+
# @return [void]
|
|
178
|
+
def process(chunk)
|
|
179
|
+
text = extract_text_from_chunk(chunk)
|
|
180
|
+
return unless text
|
|
181
|
+
|
|
182
|
+
if @enabled
|
|
183
|
+
# Efeito de digitação caractere por caractere
|
|
184
|
+
text.each_char do |char|
|
|
185
|
+
@output_io.print(char)
|
|
186
|
+
@output_io.flush
|
|
187
|
+
sleep(@delay)
|
|
188
|
+
end
|
|
189
|
+
else
|
|
190
|
+
# Sem animação, output direto
|
|
191
|
+
@output_io.print(text)
|
|
192
|
+
@output_io.flush
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
@content << text
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Extrai texto do chunk
|
|
201
|
+
#
|
|
202
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
203
|
+
# @return [String] Texto extraído
|
|
204
|
+
def extract_text_from_chunk(chunk)
|
|
205
|
+
case chunk
|
|
206
|
+
when Hash
|
|
207
|
+
if chunk[:type] == 'content_block_delta'
|
|
208
|
+
chunk.dig(:delta, :text)
|
|
209
|
+
elsif chunk.key?(:choices)
|
|
210
|
+
chunk.dig(:choices, 0, :delta, :content)
|
|
211
|
+
else
|
|
212
|
+
chunk[:delta].to_s
|
|
213
|
+
end
|
|
214
|
+
when String
|
|
215
|
+
chunk
|
|
216
|
+
else
|
|
217
|
+
''
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Streaming com barra de progresso
|
|
223
|
+
#
|
|
224
|
+
# Mostra barra de progresso enquanto carrega
|
|
225
|
+
class ProgressStreaming < Streaming
|
|
226
|
+
attr_reader :total_expected, :received
|
|
227
|
+
|
|
228
|
+
# Inicializa o streaming com progresso
|
|
229
|
+
#
|
|
230
|
+
# @param total_expected [Integer] Total esperado de tokens/chunks
|
|
231
|
+
# @param output_io [IO] IO para output
|
|
232
|
+
def initialize(total_expected: nil, output_io: $stdout)
|
|
233
|
+
super(output_io: output_io)
|
|
234
|
+
@total_expected = total_expected
|
|
235
|
+
@received = 0
|
|
236
|
+
@show_progress = true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Processa chunk com barra de progresso
|
|
240
|
+
#
|
|
241
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
242
|
+
# @return [void]
|
|
243
|
+
def process(chunk)
|
|
244
|
+
@received += 1
|
|
245
|
+
|
|
246
|
+
# Extrai e adiciona texto
|
|
247
|
+
text = extract_text_from_chunk(chunk)
|
|
248
|
+
@content << text if text
|
|
249
|
+
|
|
250
|
+
# Atualiza barra de progresso
|
|
251
|
+
update_progress_bar if @show_progress
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Finaliza o streaming
|
|
255
|
+
#
|
|
256
|
+
# @return [String] Conteúdo completo
|
|
257
|
+
def finish
|
|
258
|
+
@done = true
|
|
259
|
+
clear_progress_bar
|
|
260
|
+
@output_io.puts
|
|
261
|
+
@content
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
# Atualiza barra de progresso
|
|
267
|
+
#
|
|
268
|
+
# @return [void]
|
|
269
|
+
def update_progress_bar
|
|
270
|
+
return unless @total_expected
|
|
271
|
+
|
|
272
|
+
progress = @received.to_f / @total_expected
|
|
273
|
+
width = 40
|
|
274
|
+
filled = (progress * width).to_i
|
|
275
|
+
empty = width - filled
|
|
276
|
+
|
|
277
|
+
bar = '█' * filled + '░' * empty
|
|
278
|
+
percent = (progress * 100).to_i
|
|
279
|
+
|
|
280
|
+
@output_io.print("\r[#{bar}] #{percent}% (#{@received}/#{@total_expected})")
|
|
281
|
+
@output_io.flush
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Limpa barra de progresso
|
|
285
|
+
#
|
|
286
|
+
# @return [void]
|
|
287
|
+
def clear_progress_bar
|
|
288
|
+
@output_io.print("\r" + ' ' * 50 + "\r")
|
|
289
|
+
@output_io.flush
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Extrai texto do chunk
|
|
293
|
+
#
|
|
294
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
295
|
+
# @return [String] Texto extraído
|
|
296
|
+
def extract_text_from_chunk(chunk)
|
|
297
|
+
case chunk
|
|
298
|
+
when Hash
|
|
299
|
+
if chunk[:type] == 'content_block_delta'
|
|
300
|
+
chunk.dig(:delta, :text)
|
|
301
|
+
elsif chunk.key?(:choices)
|
|
302
|
+
chunk.dig(:choices, 0, :delta, :content)
|
|
303
|
+
else
|
|
304
|
+
chunk[:delta].to_s
|
|
305
|
+
end
|
|
306
|
+
when String
|
|
307
|
+
chunk
|
|
308
|
+
else
|
|
309
|
+
''
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Streaming com output colorido
|
|
315
|
+
#
|
|
316
|
+
# Adiciona cores para diferentes tipos de conteúdo
|
|
317
|
+
class ColorStreaming < Streaming
|
|
318
|
+
# Cores ANSI
|
|
319
|
+
COLORS = {
|
|
320
|
+
reset: "\e[0m",
|
|
321
|
+
bold: "\e[1m",
|
|
322
|
+
italic: "\e[3m",
|
|
323
|
+
underline: "\e[4m",
|
|
324
|
+
black: "\e[30m",
|
|
325
|
+
red: "\e[31m",
|
|
326
|
+
green: "\e[32m",
|
|
327
|
+
yellow: "\e[33m",
|
|
328
|
+
blue: "\e[34m",
|
|
329
|
+
magenta: "\e[35m",
|
|
330
|
+
cyan: "\e[36m",
|
|
331
|
+
white: "\e[37m",
|
|
332
|
+
gray: "\e[90m"
|
|
333
|
+
}.freeze
|
|
334
|
+
|
|
335
|
+
attr_accessor :color_scheme
|
|
336
|
+
|
|
337
|
+
# Inicializa o streaming colorido
|
|
338
|
+
#
|
|
339
|
+
# @param color_scheme [Hash] Esquema de cores
|
|
340
|
+
# @param output_io [IO] IO para output
|
|
341
|
+
def initialize(color_scheme: :default, output_io: $stdout)
|
|
342
|
+
super(output_io: output_io)
|
|
343
|
+
@color_scheme = color_scheme
|
|
344
|
+
@current_color = nil
|
|
345
|
+
@supports_color = supports_color?
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Processa chunk com cores
|
|
349
|
+
#
|
|
350
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
351
|
+
# @return [void]
|
|
352
|
+
def process(chunk)
|
|
353
|
+
text = extract_text_from_chunk(chunk)
|
|
354
|
+
return unless text
|
|
355
|
+
|
|
356
|
+
# Detecta tipo de conteúdo e aplica cor
|
|
357
|
+
color = detect_content_type(text)
|
|
358
|
+
add_text_with_color(text, color)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
# Adiciona texto com cor
|
|
364
|
+
#
|
|
365
|
+
# @param text [String] Texto
|
|
366
|
+
# @param color [Symbol] Cor
|
|
367
|
+
# @return [void]
|
|
368
|
+
def add_text_with_color(text, color)
|
|
369
|
+
if @supports_color && color
|
|
370
|
+
colored_text = colorize(text, color)
|
|
371
|
+
@output_io.print(colored_text)
|
|
372
|
+
else
|
|
373
|
+
@output_io.print(text)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
@output_io.flush
|
|
377
|
+
@content << text
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Detecta tipo de conteúdo
|
|
381
|
+
#
|
|
382
|
+
# @param text [String] Texto
|
|
383
|
+
# @return [Symbol] Tipo de conteúdo
|
|
384
|
+
def detect_content_type(text)
|
|
385
|
+
return :code if text.start_with?('```')
|
|
386
|
+
return :heading if text.start_with?('#')
|
|
387
|
+
return :emphasis if text.include?('**') || text.include?('*')
|
|
388
|
+
return :link if text.include?('[') && text.include?('](')
|
|
389
|
+
:normal
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Aplica cor ao texto
|
|
393
|
+
#
|
|
394
|
+
# @param text [String] Texto
|
|
395
|
+
# @param color [Symbol] Cor
|
|
396
|
+
# @return [String] Texto colorido
|
|
397
|
+
def colorize(text, color)
|
|
398
|
+
return text unless @supports_color
|
|
399
|
+
|
|
400
|
+
color_code = COLORS[color] || COLORS[:normal]
|
|
401
|
+
"#{color_code}#{text}#{COLORS[:reset]}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Verifica se terminal suporta cores
|
|
405
|
+
#
|
|
406
|
+
# @return [Boolean] true se suporta cores
|
|
407
|
+
def supports_color?
|
|
408
|
+
# Verifica variável de ambiente
|
|
409
|
+
return false if ENV['NO_COLOR']
|
|
410
|
+
return true if ENV['FORCE_COLOR']
|
|
411
|
+
|
|
412
|
+
# Verifica se é um TTY
|
|
413
|
+
@output_io.is_a?(IO) && @output_io.tty?
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Extrai texto do chunk
|
|
417
|
+
#
|
|
418
|
+
# @param chunk [Hash,String] Chunk do streaming
|
|
419
|
+
# @return [String] Texto extraído
|
|
420
|
+
def extract_text_from_chunk(chunk)
|
|
421
|
+
case chunk
|
|
422
|
+
when Hash
|
|
423
|
+
if chunk[:type] == 'content_block_delta'
|
|
424
|
+
chunk.dig(:delta, :text)
|
|
425
|
+
elsif chunk.key?(:choices)
|
|
426
|
+
chunk.dig(:choices, 0, :delta, :content)
|
|
427
|
+
else
|
|
428
|
+
chunk[:delta].to_s
|
|
429
|
+
end
|
|
430
|
+
when String
|
|
431
|
+
chunk
|
|
432
|
+
else
|
|
433
|
+
''
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|