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,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'zlib'
5
+ require 'securerandom'
6
+ require 'fileutils'
7
+
8
+ module Gsd
9
+ module AI
10
+ # History Management - Gerencia histórico de conversas
11
+ #
12
+ # Responsável por:
13
+ # - Manter histórico de mensagens
14
+ # - Adicionar mensagens de usuário e assistente
15
+ # - Adicionar resultados de tool calls
16
+ # - Limitar tamanho do histórico
17
+ # - Persistência em disco
18
+ # - Compressão de histórico antigo
19
+ # - Sumarização de conversas antigas
20
+ class History
21
+ attr_reader :messages, :max_messages, :max_tokens
22
+
23
+ # Inicializa o histórico
24
+ #
25
+ # @param max_messages [Integer] Número máximo de mensagens
26
+ # @param max_tokens [Integer] Número máximo de tokens
27
+ # @param persist_dir [String] Diretório para persistência
28
+ def initialize(max_messages: 100, max_tokens: 100_000, persist_dir: nil)
29
+ @messages = []
30
+ @max_messages = max_messages
31
+ @max_tokens = max_tokens
32
+ @persist_dir = persist_dir
33
+ @summaries = [] # Sumarizações de conversas antigas
34
+
35
+ load_from_disk if @persist_dir
36
+ end
37
+
38
+ # Adiciona mensagem do usuário
39
+ #
40
+ # @param content [String] Conteúdo da mensagem
41
+ # @return [void]
42
+ def add_user_message(content)
43
+ add_message(role: 'user', content: content)
44
+ end
45
+
46
+ # Adiciona mensagem do assistente
47
+ #
48
+ # @param content [String] Conteúdo da mensagem
49
+ # @return [void]
50
+ def add_assistant_message(content)
51
+ add_message(role: 'assistant', content: content)
52
+ end
53
+
54
+ # Adiciona mensagem de sistema
55
+ #
56
+ # @param content [String] Conteúdo da mensagem
57
+ # @return [void]
58
+ def add_system_message(content)
59
+ add_message(role: 'system', content: content)
60
+ end
61
+
62
+ # Adiciona resultados de tool calls
63
+ #
64
+ # @param results [Array] Resultados das tools
65
+ # @return [void]
66
+ def add_tool_results(results)
67
+ content = build_tool_results_content(results)
68
+ add_message(role: 'tool', content: content)
69
+ end
70
+
71
+ # Limpa o histórico
72
+ #
73
+ # @return [void]
74
+ def clear
75
+ @messages = []
76
+ @summaries = []
77
+ save_to_disk if @persist_dir
78
+ end
79
+
80
+ # Retorna o número de mensagens
81
+ #
82
+ # @return [Integer] Número de mensagens
83
+ def count
84
+ @messages.count
85
+ end
86
+
87
+ # Converte para formato da API do LLM
88
+ #
89
+ # @param include_summaries [Boolean] Incluir sumarizações antigas
90
+ # @return [Array] Mensagens formatadas
91
+ def to_api_messages(include_summaries: true)
92
+ result = []
93
+
94
+ # Adiciona sumarizações primeiro (se houver)
95
+ if include_summaries && @summaries.any?
96
+ result << {
97
+ role: 'system',
98
+ content: build_summaries_content
99
+ }
100
+ end
101
+
102
+ # Adiciona mensagens
103
+ result + @messages.map do |msg|
104
+ format_message_for_api(msg)
105
+ end
106
+ end
107
+
108
+ # Trunca histórico se exceder limites
109
+ #
110
+ # @return [void]
111
+ def truncate_if_needed
112
+ # Trunca por número de mensagens
113
+ if @messages.count > @max_messages
114
+ # Move mensagens antigas para summaries antes de truncar
115
+ to_remove = @messages.count - @max_messages
116
+ if to_remove > 0
117
+ summarize_old_messages(to_remove)
118
+ @messages = @messages.last(@max_messages)
119
+ end
120
+ end
121
+
122
+ # Trunca por tokens (estimativa simples)
123
+ while estimate_tokens > @max_tokens && @messages.count > 10
124
+ # Move mensagens antigas para summaries
125
+ to_remove = (@messages.count * 0.2).ceil # Remove 20% por vez
126
+ summarize_old_messages(to_remove)
127
+ @messages = @messages.last(@messages.count - to_remove)
128
+ end
129
+ end
130
+
131
+ # Exporta histórico para JSON
132
+ #
133
+ # @return [String] JSON do histórico
134
+ def to_json
135
+ JSON.pretty_generate({
136
+ messages: @messages,
137
+ summaries: @summaries,
138
+ metadata: {
139
+ max_messages: @max_messages,
140
+ max_tokens: @max_tokens,
141
+ created_at: @created_at,
142
+ updated_at: Time.now.iso8601
143
+ }
144
+ })
145
+ end
146
+
147
+ # Importa histórico de JSON
148
+ #
149
+ # @param json [String] JSON do histórico
150
+ # @return [void]
151
+ def from_json(json)
152
+ data = JSON.parse(json, symbolize_names: true)
153
+ @messages = data[:messages] || []
154
+ @summaries = data[:summaries] || []
155
+ @created_at = data.dig(:metadata, :created_at)
156
+ save_to_disk if @persist_dir
157
+ end
158
+
159
+ # Retorna estatísticas do histórico
160
+ #
161
+ # @return [Hash] Estatísticas
162
+ def stats
163
+ {
164
+ total_messages: @messages.count,
165
+ user_messages: @messages.count { |m| m[:role] == 'user' },
166
+ assistant_messages: @messages.count { |m| m[:role] == 'assistant' },
167
+ tool_messages: @messages.count { |m| m[:role] == 'tool' },
168
+ estimated_tokens: estimate_tokens,
169
+ summaries_count: @summaries.count,
170
+ oldest_message: @messages.first&.dig(:timestamp),
171
+ newest_message: @messages.last&.dig(:timestamp)
172
+ }
173
+ end
174
+
175
+ # Persiste histórico em disco
176
+ #
177
+ # @return [void]
178
+ def save_to_disk
179
+ return unless @persist_dir
180
+
181
+ FileUtils.mkdir_p(@persist_dir)
182
+ file = File.join(@persist_dir, 'history.json')
183
+
184
+ # Salva versão comprimida se for grande
185
+ json = to_json
186
+ if json.bytesize > 100_000 # 100KB
187
+ compressed = compress(json)
188
+ File.write(file + '.gz', compressed)
189
+ File.delete(file) if File.exist?(file)
190
+ else
191
+ File.write(file, json)
192
+ File.delete(file + '.gz') if File.exist?(file + '.gz')
193
+ end
194
+ end
195
+
196
+ # Carrega histórico do disco
197
+ #
198
+ # @return [void]
199
+ def load_from_disk
200
+ return unless @persist_dir
201
+
202
+ file = File.join(@persist_dir, 'history.json')
203
+ compressed_file = file + '.gz'
204
+
205
+ json = nil
206
+ if File.exist?(compressed_file)
207
+ json = decompress(File.read(compressed_file))
208
+ elsif File.exist?(file)
209
+ json = File.read(file)
210
+ end
211
+
212
+ from_json(json) if json
213
+ rescue => e
214
+ warn "[History] Erro ao carregar histórico: #{e.message}"
215
+ end
216
+
217
+ private
218
+
219
+ # Adiciona mensagem ao histórico
220
+ #
221
+ # @param role [String] Role da mensagem (user, assistant, tool)
222
+ # @param content [String] Conteúdo da mensagem
223
+ # @return [void]
224
+ def add_message(role:, content:)
225
+ @messages << {
226
+ role: role,
227
+ content: content,
228
+ timestamp: Time.now.iso8601
229
+ }
230
+
231
+ truncate_if_needed
232
+ save_to_disk if @persist_dir
233
+ end
234
+
235
+ # Constrói conteúdo para resultados de tools
236
+ #
237
+ # @param results [Array] Resultados das tools
238
+ # @return [String] Conteúdo formatado
239
+ def build_tool_results_content(results)
240
+ lines = ["## Tool Results\n"]
241
+
242
+ results.each do |result|
243
+ status = result[:success] ? '✅' : '❌'
244
+ lines << "### #{result[:tool_call_id]} #{status}"
245
+
246
+ if result[:success]
247
+ lines << "```json"
248
+ lines << JSON.pretty_generate(result[:result])
249
+ lines << "```"
250
+ else
251
+ lines << "Erro: #{result[:error]}"
252
+ end
253
+
254
+ lines << ""
255
+ end
256
+
257
+ lines.join("\n")
258
+ end
259
+
260
+ # Formata mensagem para API do LLM
261
+ #
262
+ # @param msg [Hash] Mensagem
263
+ # @return [Hash] Mensagem formatada
264
+ def format_message_for_api(msg)
265
+ {
266
+ role: msg[:role],
267
+ content: msg[:content]
268
+ }
269
+ end
270
+
271
+ # Estima número de tokens (4 chars ≈ 1 token)
272
+ #
273
+ # @return [Integer] Tokens estimados
274
+ def estimate_tokens
275
+ total_chars = @messages.sum { |m| m[:content].to_s.length }
276
+ total_chars / 4
277
+ end
278
+
279
+ # Sumariza mensagens antigas
280
+ #
281
+ # @param count [Integer] Número de mensagens para sumarizar
282
+ # @return [void]
283
+ def summarize_old_messages(count)
284
+ return if count <= 0 || @messages.empty?
285
+
286
+ messages_to_summarize = @messages.first([count, @messages.count].min)
287
+
288
+ summary = {
289
+ id: SecureRandom.uuid,
290
+ created_at: Time.now.iso8601,
291
+ message_count: messages_to_summarize.count,
292
+ content: build_summary_content(messages_to_summarize)
293
+ }
294
+
295
+ @summaries << summary
296
+ end
297
+
298
+ # Constrói conteúdo de sumarização
299
+ #
300
+ # @param messages [Array] Mensagens para sumarizar
301
+ # @return [String] Conteúdo sumarizado
302
+ def build_summary_content(messages)
303
+ user_count = messages.count { |m| m[:role] == 'user' }
304
+ assistant_count = messages.count { |m| m[:role] == 'assistant' }
305
+ tool_count = messages.count { |m| m[:role] == 'tool' }
306
+
307
+ <<~SUMMARY
308
+ ## Resumo de Conversa Antiga
309
+
310
+ Esta é uma sumarização de #{messages.count} mensagens antigas para economizar tokens.
311
+
312
+ ### Estatísticas
313
+ - Mensagens do usuário: #{user_count}
314
+ - Mensagens do assistente: #{assistant_count}
315
+ - Tool calls: #{tool_count}
316
+
317
+ ### Tópicos Discutidos
318
+ #{extract_topics(messages)}
319
+
320
+ ### Ações Realizadas
321
+ #{extract_actions(messages)}
322
+
323
+ ---
324
+ *Sumarizado em #{Time.now.strftime('%Y-%m-%d %H:%M')}*
325
+ SUMMARY
326
+ end
327
+
328
+ # Extrai tópicos das mensagens
329
+ #
330
+ # @param messages [Array] Mensagens
331
+ # @return [String] Tópicos extraídos
332
+ def extract_topics(messages)
333
+ # Implementação simples - em produção usaria NLP/LLM
334
+ "- Histórico de conversa (#{messages.count} mensagens)"
335
+ end
336
+
337
+ # Extrai ações das mensagens
338
+ #
339
+ # @param messages [Array] Mensagens
340
+ # @return [String] Ações extraídas
341
+ def extract_actions(messages)
342
+ tool_messages = messages.select { |m| m[:role] == 'tool' }
343
+ if tool_messages.any?
344
+ "- #{tool_messages.count} tool calls executados"
345
+ else
346
+ "- Nenhuma ação específica registrada"
347
+ end
348
+ end
349
+
350
+ # Constrói conteúdo de sumarizações
351
+ #
352
+ # @return [String] Conteúdo das sumarizações
353
+ def build_summaries_content
354
+ return "" if @summaries.empty?
355
+
356
+ lines = ["# Histórico Sumarizado\n"]
357
+
358
+ @summaries.each_with_index do |summary, i|
359
+ lines << "## Sumário #{i + 1}"
360
+ lines << summary[:content]
361
+ lines << ""
362
+ end
363
+
364
+ lines.join("\n")
365
+ end
366
+
367
+ # Comprime string com Gzip
368
+ #
369
+ # @param data [String] Dados para comprimir
370
+ # @return [String] Dados comprimidos
371
+ def compress(data)
372
+ Zlib::Deflate.deflate(data)
373
+ end
374
+
375
+ # Descomprime string com Gzip
376
+ #
377
+ # @param data [String] Dados comprimidos
378
+ # @return [String] Dados descomprimidos
379
+ def decompress(data)
380
+ Zlib::Inflate.inflate(data)
381
+ end
382
+ end
383
+ end
384
+ end