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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- 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)
|