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,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