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,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Gsd
7
+ module AI
8
+ # Config CLI - Comando para configurar API keys
9
+ #
10
+ # Uso:
11
+ # gsd ai config set openrouter <api-key>
12
+ # gsd ai config get openrouter
13
+ # gsd ai config list
14
+ class ConfigCLI
15
+ attr_reader :config_dir, :config_file
16
+
17
+ # Inicializa o config CLI
18
+ #
19
+ # @param cwd [String] Diretório de trabalho
20
+ def initialize(cwd: Dir.pwd)
21
+ @cwd = cwd
22
+ @config_dir = File.join(cwd, '.gsd')
23
+ @config_file = File.join(@config_dir, 'config.json')
24
+
25
+ ensure_config_dir
26
+ end
27
+
28
+ # Define uma API key
29
+ #
30
+ # @param provider [String] Nome do provider
31
+ # @param api_key [String] API key
32
+ # @return [void]
33
+ def set_key(provider, api_key)
34
+ config = load_config
35
+ config['api_keys'] ||= {}
36
+ config['api_keys'][provider] = api_key
37
+ save_config(config)
38
+
39
+ puts "✅ API key de #{provider} configurada!"
40
+
41
+ # Sugere exportar para variável de ambiente
42
+ env_var = "#{provider.upcase}_API_KEY"
43
+ puts "\nDica: Exporte para variável de ambiente:"
44
+ puts " export #{env_var}=#{api_key[0..10]}..."
45
+ end
46
+
47
+ # Obtém uma API key
48
+ #
49
+ # @param provider [String] Nome do provider
50
+ # @return [String, nil] API key
51
+ def get_key(provider)
52
+ # Tenta variável de ambiente primeiro
53
+ env_var = "#{provider.upcase}_API_KEY"
54
+ return ENV[env_var] if ENV[env_var]
55
+
56
+ # Tenta config file
57
+ config = load_config
58
+ config['api_keys']&.[](provider)
59
+ end
60
+
61
+ # Lista todas as API keys configuradas
62
+ #
63
+ # @return [void]
64
+ def list_keys
65
+ config = load_config
66
+ api_keys = config['api_keys'] || {}
67
+
68
+ # Adiciona variáveis de ambiente
69
+ env_keys = {
70
+ 'anthropic' => ENV['ANTHROPIC_API_KEY'],
71
+ 'openai' => ENV['OPENAI_API_KEY'],
72
+ 'openrouter' => ENV['OPENROUTER_API_KEY'],
73
+ 'ollama' => 'local (não requer key)',
74
+ 'lmstudio' => 'local (não requer key)'
75
+ }
76
+
77
+ puts "\n🔑 API Keys Configuradas\n"
78
+ puts "=" * 50
79
+
80
+ env_keys.each do |provider, key|
81
+ status = key ? '✅' : '❌'
82
+ masked = key ? "#{key[0..10]}..." : 'Não configurada'
83
+ puts "#{status} #{provider.ljust(12)} : #{masked}"
84
+ end
85
+
86
+ api_keys.each do |provider, key|
87
+ next if env_keys[provider] # Já listado
88
+
89
+ status = '✅ (config file)'
90
+ masked = "#{key[0..10]}..."
91
+ puts "#{status} #{provider.ljust(12)} : #{masked}"
92
+ end
93
+
94
+ puts "=" * 50
95
+ puts "\nDica: Use variáveis de ambiente para maior segurança:"
96
+ puts " export OPENROUTER_API_KEY=sk-or-..."
97
+ puts " export ANTHROPIC_API_KEY=sk-ant-..."
98
+ puts " export OPENAI_API_KEY=sk-..."
99
+ puts
100
+ end
101
+
102
+ # Remove uma API key
103
+ #
104
+ # @param provider [String] Nome do provider
105
+ # @return [void]
106
+ def remove_key(provider)
107
+ config = load_config
108
+ if config['api_keys']&.delete(provider)
109
+ save_config(config)
110
+ puts "✅ API key de #{provider} removida!"
111
+ else
112
+ puts "⚠️ API key de #{provider} não encontrada!"
113
+ end
114
+ end
115
+
116
+ # Verifica se provider está configurado
117
+ #
118
+ # @param provider [String] Nome do provider
119
+ # @return [Boolean] true se configurado
120
+ def configured?(provider)
121
+ !get_key(provider).nil?
122
+ end
123
+
124
+ # Executa o comando config
125
+ #
126
+ # @param args [Array] Argumentos
127
+ # @return [void]
128
+ def run(args = [])
129
+ subcommand = args.shift
130
+
131
+ case subcommand
132
+ when 'set'
133
+ provider = args.shift
134
+ api_key = args.shift
135
+
136
+ unless provider && api_key
137
+ puts "❌ Uso: gsd ai config set <provider> <api-key>"
138
+ exit 1
139
+ end
140
+
141
+ set_key(provider, api_key)
142
+
143
+ when 'get'
144
+ provider = args.shift
145
+
146
+ unless provider
147
+ puts "❌ Uso: gsd ai config get <provider>"
148
+ exit 1
149
+ end
150
+
151
+ key = get_key(provider)
152
+ if key
153
+ puts "#{provider}: #{key[0..10]}..."
154
+ else
155
+ puts "❌ API key de #{provider} não configurada!"
156
+ puts "\nConfigure com:"
157
+ puts " gsd ai config set #{provider} <api-key>"
158
+ exit 1
159
+ end
160
+
161
+ when 'list', nil
162
+ list_keys
163
+
164
+ when 'remove', 'rm'
165
+ provider = args.shift
166
+
167
+ unless provider
168
+ puts "❌ Uso: gsd ai config remove <provider>"
169
+ exit 1
170
+ end
171
+
172
+ remove_key(provider)
173
+
174
+ when 'help', '--help', '-h'
175
+ print_help
176
+
177
+ else
178
+ puts "❌ Subcomando desconhecido: #{subcommand}"
179
+ print_help
180
+ exit 1
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ # Garante que diretório de config existe
187
+ #
188
+ # @return [void]
189
+ def ensure_config_dir
190
+ FileUtils.mkdir_p(@config_dir) unless File.directory?(@config_dir)
191
+ end
192
+
193
+ # Carrega config do arquivo
194
+ #
195
+ # @return [Hash] Config
196
+ def load_config
197
+ return {} unless File.exist?(@config_file)
198
+
199
+ JSON.parse(File.read(@config_file))
200
+ rescue => e
201
+ warn "[Config] Erro ao carregar: #{e.message}"
202
+ {}
203
+ end
204
+
205
+ # Salva config no arquivo
206
+ #
207
+ # @param config [Hash] Config
208
+ # @return [void]
209
+ def save_config(config)
210
+ File.write(@config_file, JSON.pretty_generate(config))
211
+
212
+ # Define permissões seguras (Unix)
213
+ File.chmod(0600, @config_file) if File.respond_to?(:chmod)
214
+ end
215
+
216
+ # Imprime ajuda
217
+ #
218
+ # @return [void]
219
+ def print_help
220
+ puts <<~HELP
221
+
222
+ GSD AI Config - Gerencie suas API keys
223
+
224
+ USO:
225
+ gsd ai config <subcommand> [args]
226
+
227
+ SUBCOMANDOS:
228
+ set <provider> <key> Configurar API key
229
+ get <provider> Obter API key configurada
230
+ list Listar todas API keys
231
+ remove <provider> Remover API key
232
+ help Mostrar esta ajuda
233
+
234
+ EXEMPLOS:
235
+ gsd ai config set openrouter sk-or-v1-...
236
+ gsd ai config get openrouter
237
+ gsd ai config list
238
+ gsd ai config remove openrouter
239
+
240
+ PROVIDERS:
241
+ anthropic - Anthropic Claude
242
+ openai - OpenAI GPT
243
+ openrouter - OpenRouter (Qwen, Llama, etc.)
244
+ ollama - Ollama (local)
245
+ lmstudio - LM Studio (local)
246
+
247
+ VARIÁVEIS DE AMBIENTE:
248
+ OPENROUTER_API_KEY - OpenRouter API key
249
+ ANTHROPIC_API_KEY - Anthropic API key
250
+ OPENAI_API_KEY - OpenAI API key
251
+
252
+ HELP
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest/md5'
5
+ require 'fileutils'
6
+ require 'time'
7
+ require 'gsd/state'
8
+ require 'gsd/phase'
9
+ require 'gsd/roadmap'
10
+ require 'gsd/ai/git_context'
11
+
12
+ module Gsd
13
+ module AI
14
+ # Context Management - Gerencia contexto do projeto para a IA
15
+ #
16
+ # Responsável por:
17
+ # - Carregar estado atual do projeto
18
+ # - Carregar phase atual
19
+ # - Carregar roadmap
20
+ # - Carregar todos e commits recentes
21
+ # - Cache com TTL e invalidação por hash
22
+ # - Persistência de contexto em disco
23
+ class Context
24
+ attr_reader :cwd, :cache_ttl
25
+
26
+ # Inicializa o contexto
27
+ #
28
+ # @param cwd [String] Diretório de trabalho
29
+ # @param cache_ttl [Integer] TTL do cache em segundos (default: 300)
30
+ # @param cache_dir [String] Diretório para cache persistente
31
+ def initialize(cwd: Dir.pwd, cache_ttl: 300, cache_dir: nil)
32
+ @cwd = cwd
33
+ @cache_ttl = cache_ttl
34
+ @cache_dir = cache_dir || File.join(@cwd, '.gsd', 'cache')
35
+ @cache = {}
36
+ @cache_timestamps = {}
37
+ @cache_hashes = {}
38
+
39
+ ensure_cache_dir
40
+ end
41
+
42
+ # Carrega todo o contexto do projeto
43
+ #
44
+ # @param force [Boolean] Forçar recarregamento (ignora cache)
45
+ # @return [Hash] Dados de contexto
46
+ def load(force: false)
47
+ if force || expired?
48
+ reload
49
+ else
50
+ # Verifica se o conteúdo mudou (hash-based cache)
51
+ current_hash = compute_context_hash
52
+ if @cache_hashes[:context] != current_hash
53
+ reload
54
+ end
55
+ end
56
+
57
+ @cache
58
+ end
59
+
60
+ # Recarrega o contexto (invalida cache)
61
+ #
62
+ # @return [Hash] Dados de contexto
63
+ def reload
64
+ log_debug("Reloading context...")
65
+
66
+ @cache = {}
67
+ @cache_timestamps[:loaded_at] = Time.now
68
+
69
+ @cache[:state] = load_state
70
+ @cache[:current_phase] = load_current_phase(@cache[:state])
71
+ @cache[:roadmap] = load_roadmap
72
+ @cache[:todos] = load_todos
73
+ @cache[:recent_commits] = load_recent_commits
74
+ @cache[:git] = load_git_context
75
+ @cache[:loaded_at] = @cache_timestamps[:loaded_at]&.iso8601
76
+
77
+ # Computa e salva hash do contexto
78
+ @cache_hashes[:context] = compute_context_hash
79
+
80
+ # Persiste cache em disco
81
+ save_cache_to_disk
82
+
83
+ log_debug("Context reloaded. Hash: #{@cache_hashes[:context][0..8]}")
84
+
85
+ @cache
86
+ end
87
+
88
+ # Retorna o tamanho do contexto em KB
89
+ #
90
+ # @return [Integer] Tamanho em KB
91
+ def size
92
+ JSON.generate(@cache).bytesize / 1024
93
+ end
94
+
95
+ # Verifica se o contexto é válido
96
+ #
97
+ # @return [Boolean] true se contexto é válido
98
+ def valid?
99
+ load
100
+ !@cache[:state].nil? && !@cache[:state].empty?
101
+ end
102
+
103
+ # Retorna idade do cache em segundos
104
+ #
105
+ # @return [Integer] Idade em segundos
106
+ def cache_age
107
+ return 0 unless @cache_timestamps[:loaded_at]
108
+
109
+ (Time.now - @cache_timestamps[:loaded_at]).to_i
110
+ end
111
+
112
+ # Retorna informações do cache
113
+ #
114
+ # @return [Hash] Informações do cache
115
+ def cache_info
116
+ {
117
+ loaded_at: @cache_timestamps[:loaded_at]&.iso8601,
118
+ age_seconds: cache_age,
119
+ ttl_seconds: @cache_ttl,
120
+ expired: expired?,
121
+ hash: @cache_hashes[:context],
122
+ size_kb: size
123
+ }
124
+ end
125
+
126
+ # Limpa o cache
127
+ #
128
+ # @return [void]
129
+ def clear_cache
130
+ @cache = {}
131
+ @cache_timestamps = {}
132
+ @cache_hashes = {}
133
+ delete_cache_from_disk
134
+ end
135
+
136
+ # Exporta contexto para JSON
137
+ #
138
+ # @return [String] JSON do contexto
139
+ def to_json
140
+ JSON.pretty_generate(@cache)
141
+ end
142
+
143
+ # Importa contexto de JSON
144
+ #
145
+ # @param json [String] JSON do contexto
146
+ # @return [void]
147
+ def from_json(json)
148
+ @cache = JSON.parse(json, symbolize_names: true)
149
+ @cache_timestamps[:loaded_at] = Time.parse(@cache[:loaded_at]) if @cache[:loaded_at]
150
+ @cache_hashes[:context] = compute_context_hash
151
+ end
152
+
153
+ private
154
+
155
+ # Verifica se cache expirou
156
+ #
157
+ # @return [Boolean] true se expirado
158
+ def expired?
159
+ return true if @cache_ttl.nil? || @cache_ttl <= 0
160
+ return true unless @cache_timestamps[:loaded_at]
161
+
162
+ cache_age >= @cache_ttl
163
+ end
164
+
165
+ # Computa hash do contexto para detecção de mudanças
166
+ #
167
+ # @return [String] Hash MD5 do contexto
168
+ def compute_context_hash
169
+ content = ""
170
+ content += File.read(File.join(@cwd, '.planning', 'STATE.md')) rescue ""
171
+ content += Dir.glob(File.join(@cwd, '.planning', 'phases', '**', '*.md')).sort.map { |f| File.read(f) }.join("\n") rescue ""
172
+ content += File.read(File.join(@cwd, '.planning', 'ROADMAP.md')) rescue ""
173
+
174
+ Digest::MD5.hexdigest(content)
175
+ end
176
+
177
+ # Garante que diretório de cache existe
178
+ #
179
+ # @return [void]
180
+ def ensure_cache_dir
181
+ FileUtils.mkdir_p(@cache_dir) unless File.directory?(@cache_dir)
182
+ end
183
+
184
+ # Salva cache em disco
185
+ #
186
+ # @return [void]
187
+ def save_cache_to_disk
188
+ cache_file = File.join(@cache_dir, 'context.json')
189
+ File.write(cache_file, to_json)
190
+ log_debug("Cache saved to #{cache_file}")
191
+ rescue => e
192
+ log_debug("Failed to save cache: #{e.message}")
193
+ end
194
+
195
+ # Carrega cache do disco
196
+ #
197
+ # @return [Hash,nil] Cache carregado ou nil
198
+ def load_cache_from_disk
199
+ cache_file = File.join(@cache_dir, 'context.json')
200
+ return nil unless File.exist?(cache_file)
201
+
202
+ json = File.read(cache_file)
203
+ JSON.parse(json, symbolize_names: true)
204
+ rescue => e
205
+ log_debug("Failed to load cache: #{e.message}")
206
+ nil
207
+ end
208
+
209
+ # Deleta cache do disco
210
+ #
211
+ # @return [void]
212
+ def delete_cache_from_disk
213
+ cache_file = File.join(@cache_dir, 'context.json')
214
+ File.delete(cache_file) if File.exist?(cache_file)
215
+ rescue => e
216
+ log_debug("Failed to delete cache: #{e.message}")
217
+ end
218
+
219
+ # Carrega estado do projeto
220
+ #
221
+ # @return [Hash] Dados do estado
222
+ def load_state
223
+ Gsd::State.json(cwd: @cwd)
224
+ rescue => e
225
+ warn "[Context] Erro ao carregar state: #{e.message}"
226
+ {}
227
+ end
228
+
229
+ # Carrega phase atual
230
+ #
231
+ # @param state [Hash] Estado do projeto (opcional)
232
+ # @return [Hash] Dados da phase
233
+ def load_current_phase(state = nil)
234
+ state ||= @cache[:state] || load_state
235
+ phase_num = state&.dig('current_phase')
236
+ return nil unless phase_num
237
+
238
+ Gsd::Phase.find(phase_num, cwd: @cwd)
239
+ rescue => e
240
+ warn "[Context] Erro ao carregar phase: #{e.message}"
241
+ nil
242
+ end
243
+
244
+ # Carrega roadmap
245
+ #
246
+ # @return [Hash] Dados do roadmap
247
+ def load_roadmap
248
+ Gsd::Roadmap.analyze(cwd: @cwd)
249
+ rescue => e
250
+ warn "[Context] Erro ao carregar roadmap: #{e.message}"
251
+ {}
252
+ end
253
+
254
+ # Carrega todos
255
+ #
256
+ # @return [Array] Lista de todos
257
+ def load_todos
258
+ todos_path = File.join(@cwd, '.planning', 'todos')
259
+ return [] unless File.directory?(todos_path)
260
+
261
+ todos = []
262
+ Dir.glob(File.join(todos_path, '*.md')).each do |file|
263
+ content = File.read(file)
264
+ todos << {
265
+ file: File.basename(file),
266
+ content: content[0..500] # Primeiros 500 chars
267
+ }
268
+ end
269
+ todos
270
+ rescue => e
271
+ warn "[Context] Erro ao carregar todos: #{e.message}"
272
+ []
273
+ end
274
+
275
+ # Carrega commits recentes
276
+ #
277
+ # @return [Array] Lista de commits
278
+ def load_recent_commits
279
+ return [] unless git_available?
280
+
281
+ output = `git log -n 10 --oneline 2>/dev/null`
282
+ return [] if $?.exitstatus != 0
283
+
284
+ output.split("\n").map do |line|
285
+ parts = line.split(/\s+/, 2)
286
+ {
287
+ hash: parts[0],
288
+ message: parts[1]
289
+ }
290
+ end
291
+ rescue => e
292
+ warn "[Context] Erro ao carregar commits: #{e.message}"
293
+ []
294
+ end
295
+
296
+ # Carrega contexto do Git
297
+ #
298
+ # @return [Hash,nil] Dados do Git ou nil
299
+ def load_git_context
300
+ git = GitContext.new(cwd: @cwd)
301
+ git.gather
302
+ rescue => e
303
+ warn "[Context] Erro ao carregar git context: #{e.message}"
304
+ nil
305
+ end
306
+
307
+ # Verifica se git está disponível
308
+ #
309
+ # @return [Boolean] true se git está disponível
310
+ def git_available?
311
+ @git_available ||= File.directory?(File.join(@cwd, '.git'))
312
+ end
313
+
314
+ # Log de debug
315
+ #
316
+ # @param message [String] Mensagem de log
317
+ def log_debug(message)
318
+ return unless ENV['GSD_DEBUG']
319
+
320
+ puts "[Context] #{message}"
321
+ end
322
+ end
323
+ end
324
+ end