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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'gsd/tools/base'
5
+
6
+ module Gsd
7
+ module Tools
8
+ # Tools Registry - Registro central de todas as tools
9
+ #
10
+ # Responsável por:
11
+ # - Registrar tools disponíveis
12
+ # - Buscar tools por nome
13
+ # - Listar todas as tools
14
+ # - Filtrar tools por categoria
15
+ class Registry
16
+ @tools = {}
17
+ @categories = {}
18
+
19
+ class << self
20
+ # Registra uma nova tool
21
+ #
22
+ # @param name [String] Nome da tool
23
+ # @param tool_class [Class] Classe da tool
24
+ # @param category [Symbol] Categoria da tool
25
+ # @return [void]
26
+ def register(name, tool_class, category: :general)
27
+ @tools[name] = tool_class
28
+ @categories[category] ||= []
29
+ @categories[category] << name
30
+ end
31
+
32
+ # Busca uma tool por nome
33
+ #
34
+ # @param name [String] Nome da tool
35
+ # @return [Class,nil] Classe da tool ou nil
36
+ def find(name)
37
+ @tools[name]
38
+ end
39
+
40
+ # Retorna todas as tools
41
+ #
42
+ # @return [Array<Class>] Lista de tools
43
+ def all
44
+ @tools.values
45
+ end
46
+
47
+ # Retorna nomes de todas as tools
48
+ #
49
+ # @return [Array<String>] Lista de nomes
50
+ def all_names
51
+ @tools.keys
52
+ end
53
+
54
+ # Lista tools por categoria
55
+ #
56
+ # @param category [Symbol] Categoria
57
+ # @return [Array<Class>] Tools da categoria
58
+ def by_category(category)
59
+ (@categories[category] || []).map { |name| @tools[name] }.compact
60
+ end
61
+
62
+ # Retorna todas as categorias
63
+ #
64
+ # @return [Array<Symbol>] Lista de categorias
65
+ def categories
66
+ @categories.keys
67
+ end
68
+
69
+ # Verifica se uma tool existe
70
+ #
71
+ # @param name [String] Nome da tool
72
+ # @return [Boolean] true se existe
73
+ def exist?(name)
74
+ @tools.key?(name)
75
+ end
76
+
77
+ # Limpa o registry (para testes)
78
+ #
79
+ # @return [void]
80
+ def clear
81
+ @tools = {}
82
+ @categories = {}
83
+ end
84
+
85
+ # Exporta tools para formato da API
86
+ #
87
+ # @return [Array<Hash>] Tools formatadas
88
+ def to_api_format
89
+ @tools.map do |name, tool_class|
90
+ {
91
+ type: 'function',
92
+ function: {
93
+ name: tool_class.name,
94
+ description: tool_class.description,
95
+ parameters: tool_class.input_schema || { type: 'object', properties: {} }
96
+ }
97
+ }
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+ require 'gsd/go/bridge'
5
+
6
+ module Gsd
7
+ module Tools
8
+ # TaskCreateTool - Cria nova tarefa
9
+ class TaskCreateTool < Base
10
+ class << self
11
+ tool_name('task_create')
12
+ tool_description('Create a new task')
13
+ tool_input_schema({
14
+ type: 'object',
15
+ properties: {
16
+ description: {
17
+ type: 'string',
18
+ description: 'Task description'
19
+ },
20
+ assignee: {
21
+ type: 'string',
22
+ description: 'Task assignee'
23
+ },
24
+ priority: {
25
+ type: 'string',
26
+ description: 'Priority: low, medium, high',
27
+ enum: ['low', 'medium', 'high']
28
+ }
29
+ },
30
+ required: ['description']
31
+ })
32
+ end
33
+
34
+ def execute(args)
35
+ description = args[:description] || args['description']
36
+ assignee = args[:assignee] || args['assignee']
37
+ priority = args[:priority] || args['priority'] || 'medium'
38
+
39
+ raise ArgumentError, 'Description is required' unless description
40
+
41
+ log_debug("Creating task: #{description}")
42
+
43
+ result = Gsd::Go::Bridge.call('task', {
44
+ 'create' => true,
45
+ 'description' => description,
46
+ 'assignee' => assignee,
47
+ 'priority' => priority
48
+ }, cwd: @cwd)
49
+
50
+ parse_result(result, 'create')
51
+ rescue => e
52
+ error_result(e, 'create')
53
+ end
54
+
55
+ def self.safe?(args); false; end
56
+ def self.read_only?(args); false; end
57
+ def self.destructive?(args); false; end
58
+ end
59
+
60
+ # TaskListTool - Lista tarefas
61
+ class TaskListTool < Base
62
+ class << self
63
+ tool_name('task_list')
64
+ tool_description('List all tasks')
65
+ tool_input_schema({
66
+ type: 'object',
67
+ properties: {
68
+ status: {
69
+ type: 'string',
70
+ description: 'Filter by status: pending, running, completed',
71
+ enum: ['pending', 'running', 'completed']
72
+ },
73
+ assignee: {
74
+ type: 'string',
75
+ description: 'Filter by assignee'
76
+ }
77
+ }
78
+ })
79
+ end
80
+
81
+ def execute(args)
82
+ status = args[:status] || args['status']
83
+ assignee = args[:assignee] || args['assignee']
84
+
85
+ log_debug("Listing tasks")
86
+
87
+ params = { 'list' => true }
88
+ params['status'] = status if status
89
+ params['assignee'] = assignee if assignee
90
+
91
+ result = Gsd::Go::Bridge.call('task', params, cwd: @cwd)
92
+
93
+ parse_result(result, 'list')
94
+ rescue => e
95
+ error_result(e, 'list')
96
+ end
97
+
98
+ def self.safe?(args); true; end
99
+ def self.read_only?(args); true; end
100
+ end
101
+
102
+ # TaskUpdateTool - Atualiza tarefa
103
+ class TaskUpdateTool < Base
104
+ class << self
105
+ tool_name('task_update')
106
+ tool_description('Update an existing task')
107
+ tool_input_schema({
108
+ type: 'object',
109
+ properties: {
110
+ task_id: {
111
+ type: 'string',
112
+ description: 'Task ID'
113
+ },
114
+ status: {
115
+ type: 'string',
116
+ description: 'New status',
117
+ enum: ['pending', 'running', 'completed']
118
+ },
119
+ description: {
120
+ type: 'string',
121
+ description: 'New description'
122
+ },
123
+ assignee: {
124
+ type: 'string',
125
+ description: 'New assignee'
126
+ }
127
+ },
128
+ required: ['task_id']
129
+ })
130
+ end
131
+
132
+ def execute(args)
133
+ task_id = args[:task_id] || args['task_id']
134
+ status = args[:status] || args['status']
135
+ description = args[:description] || args['description']
136
+ assignee = args[:assignee] || args['assignee']
137
+
138
+ raise ArgumentError, 'Task ID is required' unless task_id
139
+
140
+ log_debug("Updating task: #{task_id}")
141
+
142
+ params = { 'update' => true, 'task_id' => task_id }
143
+ params['status'] = status if status
144
+ params['description'] = description if description
145
+ params['assignee'] = assignee if assignee
146
+
147
+ result = Gsd::Go::Bridge.call('task', params, cwd: @cwd)
148
+
149
+ parse_result(result, 'update')
150
+ rescue => e
151
+ error_result(e, 'update')
152
+ end
153
+
154
+ def self.safe?(args); false; end
155
+ def self.read_only?(args); false; end
156
+ def self.destructive?(args); false; end
157
+ end
158
+
159
+ # TaskStopTool - Para tarefa
160
+ class TaskStopTool < Base
161
+ class << self
162
+ tool_name('task_stop')
163
+ tool_description('Stop a running task')
164
+ tool_input_schema({
165
+ type: 'object',
166
+ properties: {
167
+ task_id: {
168
+ type: 'string',
169
+ description: 'Task ID to stop'
170
+ }
171
+ },
172
+ required: ['task_id']
173
+ })
174
+ end
175
+
176
+ def execute(args)
177
+ task_id = args[:task_id] || args['task_id']
178
+
179
+ raise ArgumentError, 'Task ID is required' unless task_id
180
+
181
+ log_debug("Stopping task: #{task_id}")
182
+
183
+ result = Gsd::Go::Bridge.call('task', {
184
+ 'stop' => true,
185
+ 'task_id' => task_id
186
+ }, cwd: @cwd)
187
+
188
+ parse_result(result, 'stop')
189
+ rescue => e
190
+ error_result(e, 'stop')
191
+ end
192
+
193
+ def self.safe?(args); false; end
194
+ def self.read_only?(args); false; end
195
+ def self.destructive?(args); true; end
196
+ end
197
+
198
+ # Helper methods
199
+ module TaskHelpers
200
+ def parse_result(result, operation)
201
+ if result['success']
202
+ {
203
+ success: true,
204
+ operation: operation,
205
+ data: result['data'],
206
+ cwd: @cwd
207
+ }
208
+ else
209
+ {
210
+ success: false,
211
+ error: result['error'] || 'unknown_error',
212
+ message: result['message'],
213
+ operation: operation
214
+ }
215
+ end
216
+ end
217
+
218
+ def error_result(error, operation)
219
+ log_debug("Error: #{error.message}")
220
+ {
221
+ success: false,
222
+ error: 'bridge_error',
223
+ message: error.message,
224
+ operation: operation
225
+ }
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ # Registra as tools
232
+ Gsd::Tools::Registry.register('task_create', Gsd::Tools::TaskCreateTool, category: :tasks)
233
+ Gsd::Tools::Registry.register('task_list', Gsd::Tools::TaskListTool, category: :tasks)
234
+ Gsd::Tools::Registry.register('task_update', Gsd::Tools::TaskUpdateTool, category: :tasks)
235
+ Gsd::Tools::Registry.register('task_stop', Gsd::Tools::TaskStopTool, category: :tasks)
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module Gsd
8
+ module Tools
9
+ # TodoWriteTool - Gerencia lista de tarefas (TODOs)
10
+ #
11
+ # Uso:
12
+ # tool = TodoWriteTool.new(cwd: '/path')
13
+ # result = tool.execute(operation: 'add', todo: 'Implement feature')
14
+ class TodoWriteTool < Base
15
+ class << self
16
+ tool_name('todo_write')
17
+ tool_description('Manage TODO list: add, remove, update, list tasks')
18
+ tool_input_schema({
19
+ type: 'object',
20
+ properties: {
21
+ operation: {
22
+ type: 'string',
23
+ description: 'Operation: add, remove, update, list, complete',
24
+ enum: ['add', 'remove', 'update', 'list', 'complete']
25
+ },
26
+ todo: {
27
+ type: 'string',
28
+ description: 'TODO text (for add/update operations)'
29
+ },
30
+ id: {
31
+ type: 'integer',
32
+ description: 'TODO ID (for remove/update/complete operations)'
33
+ },
34
+ priority: {
35
+ type: 'string',
36
+ description: 'Priority level: low, medium, high',
37
+ enum: ['low', 'medium', 'high'],
38
+ default: 'medium'
39
+ },
40
+ tags: {
41
+ type: 'array',
42
+ description: 'Tags for the TODO',
43
+ items: {
44
+ type: 'string'
45
+ }
46
+ },
47
+ file: {
48
+ type: 'string',
49
+ description: 'TODO file path (default: .planning/todos/todos.json)',
50
+ default: '.planning/todos/todos.json'
51
+ }
52
+ },
53
+ required: ['operation']
54
+ })
55
+ end
56
+
57
+ # Gerencia lista de TODOs
58
+ #
59
+ # @param args [Hash] Argumentos
60
+ # @return [Hash] Resultado da operação
61
+ def execute(args)
62
+ operation = args[:operation] || args['operation']
63
+ todo_text = args[:todo] || args['todo']
64
+ todo_id = args[:id] || args['id']
65
+ priority = args[:priority] || args['priority'] || 'medium'
66
+ tags = args[:tags] || args['tags'] || []
67
+ file_path = args[:file] || args['file'] || '.planning/todos/todos.json'
68
+
69
+ raise ArgumentError, 'Operation is required' unless operation
70
+
71
+ log_debug("Operation: #{operation}")
72
+
73
+ # Normaliza path
74
+ full_path = File.expand_path(file_path, @cwd)
75
+
76
+ # Carrega TODOs existentes
77
+ todos = load_todos(full_path)
78
+
79
+ # Executa operação
80
+ result = case operation
81
+ when 'add'
82
+ add_todo(todos, todo_text, priority, tags)
83
+ when 'remove'
84
+ remove_todo(todos, todo_id)
85
+ when 'update'
86
+ update_todo(todos, todo_id, todo_text, priority, tags)
87
+ when 'complete'
88
+ complete_todo(todos, todo_id)
89
+ when 'list'
90
+ list_todos(todos)
91
+ else
92
+ { success: false, error: 'invalid_operation', message: "Unknown operation: #{operation}" }
93
+ end
94
+
95
+ # Salva se modificou
96
+ if result[:success] && ['add', 'remove', 'update', 'complete'].include?(operation)
97
+ save_todos(full_path, todos)
98
+ result[:todos] = todos
99
+ result[:file] = full_path
100
+ elsif result[:success] && operation == 'list'
101
+ result[:todos] = todos
102
+ end
103
+
104
+ result
105
+ end
106
+
107
+ # Verifica se é safe
108
+ #
109
+ # @param args [Hash] Argumentos
110
+ # @return [Boolean] true
111
+ def self.safe?(args)
112
+ true
113
+ end
114
+
115
+ # Verifica se é read-only
116
+ #
117
+ # @param args [Hash] Argumentos
118
+ # @return [Boolean] true apenas para list
119
+ def self.read_only?(args)
120
+ operation = args[:operation] || args['operation']
121
+ operation == 'list'
122
+ end
123
+
124
+ private
125
+
126
+ # Carrega TODOs do arquivo
127
+ #
128
+ # @param path [String] Caminho do arquivo
129
+ # @return [Array] Lista de TODOs
130
+ def load_todos(path)
131
+ return [] unless File.exist?(path)
132
+
133
+ data = JSON.parse(File.read(path))
134
+ data['todos'] || []
135
+ rescue => e
136
+ log_debug("Error loading todos: #{e.message}")
137
+ []
138
+ end
139
+
140
+ # Salva TODOs no arquivo
141
+ #
142
+ # @param path [String] Caminho do arquivo
143
+ # @param todos [Array] Lista de TODOs
144
+ # @return [void]
145
+ def save_todos(path, todos)
146
+ # Cria diretório se necessário
147
+ FileUtils.mkdir_p(File.dirname(path))
148
+
149
+ data = {
150
+ todos: todos,
151
+ updated_at: Time.now.iso8601
152
+ }
153
+
154
+ File.write(path, JSON.pretty_generate(data))
155
+ log_debug("Todos saved to: #{path}")
156
+ end
157
+
158
+ # Adiciona TODO
159
+ #
160
+ # @param todos [Array] Lista de TODOs
161
+ # @param text [String] Texto do TODO
162
+ # @param priority [String] Prioridade
163
+ # @param tags [Array] Tags
164
+ # @return [Hash] Resultado
165
+ def add_todo(todos, text, priority, tags)
166
+ return { success: false, error: 'missing_todo', message: 'TODO text is required' } unless text
167
+
168
+ new_todo = {
169
+ id: next_id(todos),
170
+ text: text,
171
+ status: 'pending',
172
+ priority: priority,
173
+ tags: tags,
174
+ created_at: Time.now.iso8601,
175
+ updated_at: Time.now.iso8601
176
+ }
177
+
178
+ todos << new_todo
179
+
180
+ {
181
+ success: true,
182
+ message: 'TODO added successfully',
183
+ todo: new_todo,
184
+ total: todos.count
185
+ }
186
+ end
187
+
188
+ # Remove TODO
189
+ #
190
+ # @param todos [Array] Lista de TODOs
191
+ # @param id [Integer] ID do TODO
192
+ # @return [Hash] Resultado
193
+ def remove_todo(todos, id)
194
+ return { success: false, error: 'missing_id', message: 'TODO ID is required' } unless id
195
+
196
+ todo = todos.find { |t| t['id'] == id }
197
+ return { success: false, error: 'not_found', message: "TODO not found: #{id}" } unless todo
198
+
199
+ todos.delete(todo)
200
+
201
+ {
202
+ success: true,
203
+ message: 'TODO removed successfully',
204
+ removed: todo,
205
+ total: todos.count
206
+ }
207
+ end
208
+
209
+ # Atualiza TODO
210
+ #
211
+ # @param todos [Array] Lista de TODOs
212
+ # @param id [Integer] ID do TODO
213
+ # @param text [String] Novo texto
214
+ # @param priority [String] Nova prioridade
215
+ # @param tags [Array] Novas tags
216
+ # @return [Hash] Resultado
217
+ def update_todo(todos, id, text, priority, tags)
218
+ return { success: false, error: 'missing_id', message: 'TODO ID is required' } unless id
219
+
220
+ todo = todos.find { |t| t['id'] == id }
221
+ return { success: false, error: 'not_found', message: "TODO not found: #{id}" } unless todo
222
+
223
+ todo['text'] = text if text
224
+ todo['priority'] = priority if priority
225
+ todo['tags'] = tags if tags
226
+ todo['updated_at'] = Time.now.iso8601
227
+
228
+ {
229
+ success: true,
230
+ message: 'TODO updated successfully',
231
+ todo: todo,
232
+ total: todos.count
233
+ }
234
+ end
235
+
236
+ # Completa TODO
237
+ #
238
+ # @param todos [Array] Lista de TODOs
239
+ # @param id [Integer] ID do TODO
240
+ # @return [Hash] Resultado
241
+ def complete_todo(todos, id)
242
+ return { success: false, error: 'missing_id', message: 'TODO ID is required' } unless id
243
+
244
+ todo = todos.find { |t| t['id'] == id }
245
+ return { success: false, error: 'not_found', message: "TODO not found: #{id}" } unless todo
246
+
247
+ todo['status'] = 'completed'
248
+ todo['completed_at'] = Time.now.iso8601
249
+ todo['updated_at'] = Time.now.iso8601
250
+
251
+ {
252
+ success: true,
253
+ message: 'TODO completed successfully',
254
+ todo: todo,
255
+ total: todos.count
256
+ }
257
+ end
258
+
259
+ # Lista TODOs
260
+ #
261
+ # @param todos [Array] Lista de TODOs
262
+ # @return [Hash] Resultado
263
+ def list_todos(todos)
264
+ pending = todos.select { |t| t['status'] == 'pending' }
265
+ completed = todos.select { |t| t['status'] == 'completed' }
266
+
267
+ {
268
+ success: true,
269
+ total: todos.count,
270
+ pending_count: pending.count,
271
+ completed_count: completed.count,
272
+ pending: pending,
273
+ completed: completed
274
+ }
275
+ end
276
+
277
+ # Próximo ID disponível
278
+ #
279
+ # @param todos [Array] Lista de TODOs
280
+ # @return [Integer] Próximo ID
281
+ def next_id(todos)
282
+ max_id = todos.map { |t| t['id'] }.max || 0
283
+ max_id + 1
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ # Registra a tool
290
+ Gsd::Tools::Registry.register('todo_write', Gsd::Tools::TodoWriteTool, category: :tasks)