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,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'gsd/ai/context'
5
+ require 'gsd/ai/history'
6
+ require 'gsd/ai/streaming'
7
+ require 'gsd/ai/cost_tracker'
8
+ require 'gsd/ai/command_parser'
9
+ require 'gsd/ai/reference_resolver'
10
+ require 'gsd/ai/commands/help'
11
+ require 'gsd/ai/commands/clear'
12
+ require 'gsd/ai/commands/reset'
13
+ require 'gsd/ai/commands/model'
14
+ require 'gsd/ai/commands/cost'
15
+ require 'gsd/ai/commands/context'
16
+ require 'gsd/ai/commands/export'
17
+ require 'gsd/tools/registry'
18
+ require 'gsd/skills/loader'
19
+ require 'gsd/ai/providers/base'
20
+ require 'gsd/ai/providers/base'
21
+ require 'gsd/ai/providers/anthropic'
22
+ require 'gsd/ai/providers/openai'
23
+ require 'gsd/ai/providers/ollama'
24
+ require 'gsd/ai/providers/lmstudio'
25
+ require 'gsd/ai/providers/openrouter'
26
+
27
+ module Gsd
28
+ module AI
29
+ # Chat REPL Interface - Interface principal de conversa com IA
30
+ #
31
+ # Responsável por:
32
+ # - Gerenciar conversas com LLMs
33
+ # - Executar tool calls
34
+ # - Manter histórico de mensagens
35
+ # - Streaming de respostas
36
+ class Chat
37
+ attr_reader :context, :history, :tools, :skills, :provider
38
+
39
+ # Inicializa uma nova sessão de chat
40
+ #
41
+ # @param provider [Symbol] Provider de LLM (:anthropic, :openai, :ollama)
42
+ # @param model [String] Modelo do LLM
43
+ # @param cwd [String] Diretório de trabalho
44
+ # @param debug [Boolean] Habilitar modo debug
45
+ # @param cache_ttl [Integer] TTL do cache de contexto (segundos)
46
+ # @param persist_history [Boolean] Persistir histórico em disco
47
+ # @param budget [Float] Orçamento mensal em USD
48
+ def initialize(provider: :anthropic, model: nil, cwd: Dir.pwd, debug: false,
49
+ cache_ttl: 300, persist_history: true, budget: 100.0)
50
+ @cwd = cwd
51
+ @debug = debug
52
+ @provider = ProviderFactory.create(provider, model: model, debug: debug)
53
+ @context = Context.new(cwd: cwd, cache_ttl: cache_ttl)
54
+ persist_dir = persist_history ? File.join(cwd, '.gsd', 'ai') : nil
55
+ @history = History.new(persist_dir: persist_dir)
56
+ @tools = Tools::Registry.all
57
+ @skills = Skills::Loader.new(cwd: cwd).bundled_skills
58
+ @streaming = Streaming.new
59
+ @cost_tracker = CostTracker.new(budget: budget, persist_dir: persist_dir)
60
+
61
+ # Registra pricing do provider
62
+ register_provider_pricing
63
+ end
64
+
65
+ # Registra pricing do provider no cost tracker
66
+ #
67
+ # @return [void]
68
+ def register_provider_pricing
69
+ pricing = begin
70
+ @provider.class::MODELS
71
+ rescue
72
+ {}
73
+ end
74
+ provider_name = begin
75
+ @provider.class::PROVIDER_NAME
76
+ rescue
77
+ 'unknown'
78
+ end
79
+ @cost_tracker.register_provider(provider_name, pricing) if pricing.any?
80
+ end
81
+
82
+ # Envia uma mensagem para a IA e retorna a resposta
83
+ #
84
+ # @param message [String] Mensagem do usuário
85
+ # @param options [Hash] Opções adicionais
86
+ # @return [String] Resposta da IA
87
+ def send(message, **options)
88
+ log_debug("User: #{message}")
89
+
90
+ # Preprocessar input (comandos e referências)
91
+ preprocessed = preprocess_input(message)
92
+
93
+ # Se for comando, executa e retorna
94
+ return preprocessed[:response] if preprocessed[:type] == :command
95
+
96
+ # Usa mensagem processada (expandida com referências)
97
+ final_message = preprocessed[:message]
98
+
99
+ # 1. Adicionar mensagem do usuário ao histórico
100
+ @history.add_user_message(final_message)
101
+
102
+ # 2. Carregar contexto do projeto
103
+ context_data = @context.load
104
+
105
+ # 3. Selecionar tools relevantes
106
+ available_tools = select_tools_for_message(message)
107
+
108
+ # 4. Construir system prompt
109
+ system_prompt = build_system_prompt(context_data)
110
+
111
+ # 5. Chamar LLM
112
+ response = call_llm(
113
+ system_prompt: system_prompt,
114
+ messages: @history.messages,
115
+ tools: available_tools,
116
+ stream: options.fetch(:stream, true)
117
+ )
118
+
119
+ # 6. Processar tool calls se existirem
120
+ if response.tool_calls
121
+ results = execute_tool_calls(response.tool_calls)
122
+ @history.add_tool_results(results)
123
+
124
+ # 7. Chamar LLM novamente com resultados das tools
125
+ response = call_llm(
126
+ system_prompt: system_prompt,
127
+ messages: @history.messages,
128
+ tools: available_tools,
129
+ stream: options.fetch(:stream, true)
130
+ )
131
+ end
132
+
133
+ # 8. Adicionar resposta ao histórico
134
+ @history.add_assistant_message(response.content)
135
+
136
+ # 9. Track custo
137
+ provider_name = begin
138
+ @provider.class::PROVIDER_NAME
139
+ rescue
140
+ 'unknown'
141
+ end
142
+ @cost_tracker.track(
143
+ response.usage,
144
+ provider: provider_name,
145
+ model: @provider.model
146
+ )
147
+
148
+ log_debug("Assistant: #{response.content}")
149
+ log_debug("Cost: #{@cost_tracker.total_cost}")
150
+ log_debug("Tokens: #{@cost_tracker.total_tokens}")
151
+ log_debug("Budget used: #{@cost_tracker.stats[:budget_used_percent]}%")
152
+
153
+ response.content
154
+ end
155
+
156
+ # Inicia o REPL interativo
157
+ #
158
+ # @return [void]
159
+ def repl
160
+ puts "🤖 GSD AI Chat - #{provider.class} (#{@provider.model})"
161
+ puts "Digite 'quit' ou 'exit' para sair, 'clear' para limpar histórico"
162
+ puts "-" * 60
163
+
164
+ loop do
165
+ print "\n👤 "
166
+ input = $stdin.gets&.chomp
167
+
168
+ break if input.nil? || %w[quit exit].include?(input.downcase)
169
+
170
+ if input.downcase == 'clear'
171
+ @history.clear
172
+ puts "🧹 Histórico limpo!"
173
+ next
174
+ end
175
+
176
+ next if input.strip.empty?
177
+
178
+ begin
179
+ response = send(input, stream: true)
180
+ puts "\n🤖 #{response}"
181
+ rescue => e
182
+ puts "\n❌ Erro: #{e.message}"
183
+ log_debug(e.backtrace.join("\n")) if @debug
184
+ end
185
+ end
186
+
187
+ puts "\n👋 Até logo!"
188
+ end
189
+
190
+ # Limpa o histórico de conversas
191
+ #
192
+ # @return [void]
193
+ def clear_history
194
+ @history.clear
195
+ end
196
+
197
+ # Retorna estatísticas de uso
198
+ #
199
+ # @return [Hash] Estatísticas de tokens e custo
200
+ def stats
201
+ {
202
+ messages_count: @history.messages.count,
203
+ total_tokens: @cost_tracker.total_tokens,
204
+ total_cost: @cost_tracker.total_cost,
205
+ context_size: @context.size,
206
+ cache_info: @context.cache_info,
207
+ history_stats: @history.stats,
208
+ cost_stats: @cost_tracker.stats,
209
+ budget_remaining: @cost_tracker.stats[:budget_remaining],
210
+ budget_used_percent: @cost_tracker.stats[:budget_used_percent]
211
+ }
212
+ end
213
+
214
+ # Retorna informações detalhadas de custo
215
+ #
216
+ # @return [Hash] Informações de custo
217
+ def cost_info
218
+ @cost_tracker.stats
219
+ end
220
+
221
+ # Retorna histórico de custos
222
+ #
223
+ # @param limit [Integer] Limite de registros
224
+ # @return [Array] Histórico
225
+ def cost_history(limit: 50)
226
+ @cost_tracker.history(limit: limit)
227
+ end
228
+
229
+ # Define orçamento
230
+ #
231
+ # @param budget [Float] Orçamento em USD
232
+ # @return [void]
233
+ def budget=(budget)
234
+ @cost_tracker.budget = budget
235
+ end
236
+
237
+ # Retorna orçamento restante
238
+ #
239
+ # @return [Float] Orçamento restante
240
+ def budget_remaining
241
+ @cost_tracker.stats[:budget_remaining]
242
+ end
243
+
244
+ # Reseta estatísticas de custo
245
+ #
246
+ # @return [void]
247
+ def reset_cost_stats
248
+ @cost_tracker.reset
249
+ end
250
+
251
+ # Retorna informações do cache de contexto
252
+ #
253
+ # @return [Hash] Informações do cache
254
+ def cache_info
255
+ @context.cache_info
256
+ end
257
+
258
+ # Retorna informações do histórico
259
+ #
260
+ # @return [Hash] Estatísticas do histórico
261
+ def history_info
262
+ @history.stats
263
+ end
264
+
265
+ # Recarrega o contexto
266
+ #
267
+ # @return [Hash] Novo contexto
268
+ def reload_context
269
+ @context.reload
270
+ end
271
+
272
+ # Limpa o cache de contexto
273
+ #
274
+ # @return [void]
275
+ def clear_cache
276
+ @context.clear_cache
277
+ end
278
+
279
+ private
280
+
281
+ # Preprocessa input do usuário (comandos e referências)
282
+ #
283
+ # @param message [String] Input original
284
+ # @return [Hash] { type: :message|:command, message: String, response: String }
285
+ def preprocess_input(message)
286
+ parsed = CommandParser.parse(message)
287
+
288
+ case parsed.type
289
+ when :command
290
+ { type: :command, response: execute_command(parsed.command, parsed.args) }
291
+ when :reference
292
+ resolved = resolve_references(parsed.references)
293
+ expanded = expand_message_with_references(message, resolved)
294
+ { type: :message, message: expanded }
295
+ else
296
+ { type: :message, message: message }
297
+ end
298
+ end
299
+
300
+ # Executa um comando /
301
+ #
302
+ # @param command [String] Nome do comando
303
+ # @param args [Array<String>] Argumentos
304
+ # @return [String] Resposta do comando
305
+ def execute_command(command, args)
306
+ cmd_class = case command
307
+ when 'help' then Commands::Help
308
+ when 'clear' then Commands::Clear
309
+ when 'reset' then Commands::Reset
310
+ when 'model' then Commands::Model
311
+ when 'cost' then Commands::Cost
312
+ when 'context' then Commands::Context
313
+ when 'export' then Commands::Export
314
+ else nil
315
+ end
316
+
317
+ return "Comando desconhecido: /#{command}\nUse /help para listar comandos." if cmd_class.nil?
318
+
319
+ cmd = cmd_class.new(self, args)
320
+ cmd.execute
321
+ rescue => e
322
+ "❌ Erro ao executar /#{command}: #{e.message}"
323
+ end
324
+
325
+ # Resolve referências @ para conteúdo
326
+ #
327
+ # @param references [Array<String>] Lista de referências
328
+ # @return [Hash] Resultado do resolver
329
+ def resolve_references(references)
330
+ resolver = ReferenceResolver.new(cwd: @cwd, provider: @provider)
331
+ resolver.resolve(references)
332
+ end
333
+
334
+ # Expande mensagem com conteúdo das referências
335
+ #
336
+ # @param original [String] Mensagem original
337
+ # @param resolved [Hash] Resultado do resolver
338
+ # @return [String] Mensagem expandida
339
+ def expand_message_with_references(original, resolved)
340
+ return original if resolved[:text].nil? || resolved[:text].empty?
341
+
342
+ <<~MESSAGE
343
+ #{original}
344
+
345
+ [Referências carregadas:]
346
+ #{resolved[:text]}
347
+ MESSAGE
348
+ end
349
+
350
+ # Seleciona tools relevantes para a mensagem
351
+ #
352
+ # @param message [String] Mensagem do usuário
353
+ # @return [Array<Tool>] Tools relevantes
354
+ def select_tools_for_message(message)
355
+ # Por enquanto, retorna todas as tools
356
+ # No futuro, pode usar embedding ou heurísticas para filtrar
357
+ @tools
358
+ end
359
+
360
+ # Constrói o system prompt com contexto do projeto
361
+ #
362
+ # @param context_data [Hash] Dados de contexto
363
+ # @return [String] System prompt
364
+ def build_system_prompt(context_data)
365
+ <<~PROMPT
366
+ Você é o GSD AI, um assistente especializado em gerenciamento de projetos GSD (Get Shit Done).
367
+
368
+ ## Contexto do Projeto
369
+
370
+ ### Estado Atual
371
+ #{JSON.pretty_generate(context_data[:state])}
372
+
373
+ ### Phase Atual
374
+ #{context_data[:current_phase]&.dig('name') || 'N/A'}
375
+
376
+ ### Roadmap
377
+ #{context_data[:roadmap]&.dig('summary') || 'N/A'}
378
+
379
+ ## Suas Capacidades
380
+
381
+ Você tem acesso às seguintes ferramentas:
382
+ - Bash: Executar comandos shell
383
+ - File operations: Ler, escrever e editar arquivos
384
+ - Grep/Glob: Buscar no código
385
+ - GSD commands: Operações de state, phase, roadmap
386
+ - Tasks: Gerenciar tarefas
387
+ - Web: Pesquisar e fetch de URLs
388
+
389
+ ## Instruções
390
+
391
+ 1. Seja direto e prático
392
+ 2. Use ferramentas quando necessário
393
+ 3. Mantenha o foco nas tarefas do usuário
394
+ 4. Reporte erros claramente
395
+ 5. Sugira próximos passos quando relevante
396
+
397
+ ## Formato de Resposta
398
+
399
+ - Use Markdown para formatação
400
+ - Inclua código em blocks quando relevante
401
+ - Seja conciso mas completo
402
+ PROMPT
403
+ end
404
+
405
+ # Chama o provedor de LLM
406
+ #
407
+ # @param system_prompt [String] System prompt
408
+ # @param messages [Array] Histórico de mensagens
409
+ # @param tools [Array] Tools disponíveis
410
+ # @param stream [Boolean] Habilitar streaming
411
+ # @return [OpenStruct] Resposta com content, tool_calls, usage
412
+ def call_llm(system_prompt:, messages:, tools:, stream:)
413
+ @provider.call(
414
+ system_prompt: system_prompt,
415
+ messages: messages,
416
+ tools: tools,
417
+ stream: stream
418
+ ) do |chunk|
419
+ @streaming.process(chunk) if stream
420
+ end
421
+ end
422
+
423
+ # Executa tool calls retornados pelo LLM
424
+ #
425
+ # @param tool_calls [Array] Lista de tool calls
426
+ # @return [Array] Resultados das execuções
427
+ def execute_tool_calls(tool_calls)
428
+ tool_calls.map do |call|
429
+ tool = @tools.find { |t| t.name == call.name }
430
+
431
+ if tool
432
+ begin
433
+ result = tool.execute(call.arguments, cwd: @cwd)
434
+ {
435
+ tool_call_id: call.id,
436
+ success: true,
437
+ result: result
438
+ }
439
+ rescue => e
440
+ {
441
+ tool_call_id: call.id,
442
+ success: false,
443
+ error: e.message
444
+ }
445
+ end
446
+ else
447
+ {
448
+ tool_call_id: call.id,
449
+ success: false,
450
+ error: "Tool not found: #{call.name}"
451
+ }
452
+ end
453
+ end
454
+ end
455
+
456
+ # Log de debug
457
+ #
458
+ # @param message [String] Mensagem de log
459
+ def log_debug(message)
460
+ return unless @debug
461
+
462
+ puts "[DEBUG] #{message}"
463
+ end
464
+ end
465
+
466
+ # Factory para criar provedores de LLM
467
+ module ProviderFactory
468
+ def self.create(provider, model: nil, debug: false, **options)
469
+ case provider
470
+ when :anthropic
471
+ Providers::Anthropic.new(model: model, debug: debug)
472
+ when :openai
473
+ Providers::OpenAI.new(model: model, debug: debug)
474
+ when :ollama
475
+ Providers::Ollama.new(model: model, debug: debug)
476
+ when :lmstudio
477
+ Providers::LMStudio.new(model: model, debug: debug, **options)
478
+ when :openrouter
479
+ Providers::OpenRouter.new(model: model, debug: debug, **options)
480
+ else
481
+ raise ArgumentError, "Unknown provider: #{provider}. Available: :anthropic, :openai, :ollama, :lmstudio, :openrouter"
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end