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,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+
5
+ module Gsd
6
+ module Tools
7
+ # GlobTool - Busca arquivos por patterns glob
8
+ #
9
+ # Uso:
10
+ # tool = GlobTool.new(cwd: '/path')
11
+ # result = tool.execute(pattern: '**/*.rb', path: 'src/')
12
+ class GlobTool < Base
13
+ class << self
14
+ tool_name('glob')
15
+ tool_description('Find files matching glob patterns (e.g., **/*.rb)')
16
+ tool_input_schema({
17
+ type: 'object',
18
+ properties: {
19
+ pattern: {
20
+ type: 'string',
21
+ description: 'Glob pattern to match (e.g., "**/*.rb", "src/**/*.js")'
22
+ },
23
+ path: {
24
+ type: 'string',
25
+ description: 'Base directory to search in (default: cwd)'
26
+ },
27
+ file_type: {
28
+ type: 'string',
29
+ description: 'Filter by file type: file, directory, or both',
30
+ enum: ['file', 'directory', 'both'],
31
+ default: 'file'
32
+ },
33
+ max_results: {
34
+ type: 'integer',
35
+ description: 'Maximum number of results (default: 1000)',
36
+ default: 1000
37
+ },
38
+ sort: {
39
+ type: 'string',
40
+ description: 'Sort order: name, mtime, size',
41
+ enum: ['name', 'mtime', 'size'],
42
+ default: 'name'
43
+ },
44
+ include_hidden: {
45
+ type: 'boolean',
46
+ description: 'Include hidden files (default: false)',
47
+ default: false
48
+ }
49
+ },
50
+ required: ['pattern']
51
+ })
52
+ end
53
+
54
+ # Executa busca glob
55
+ #
56
+ # @param args [Hash] Argumentos
57
+ # @return [Hash] Arquivos encontrados
58
+ def execute(args)
59
+ pattern = args[:pattern] || args['pattern']
60
+ path = args[:path] || args['path'] || '.'
61
+ file_type = args[:file_type] || args['file_type'] || 'file'
62
+ max_results = args[:max_results] || args['max_results'] || 1000
63
+ sort_order = args[:sort] || args['sort'] || 'name'
64
+ include_hidden = args[:include_hidden] || args['include_hidden'] || false
65
+
66
+ raise ArgumentError, 'Pattern is required' unless pattern
67
+
68
+ log_debug("Searching for: #{pattern}")
69
+ log_debug("Path: #{path}, Type: #{file_type}")
70
+
71
+ # Normaliza path
72
+ search_path = File.expand_path(path, @cwd)
73
+
74
+ # Verifica se path existe
75
+ unless File.directory?(search_path)
76
+ return {
77
+ success: false,
78
+ error: 'directory_not_found',
79
+ message: "Directory not found: #{path}"
80
+ }
81
+ end
82
+
83
+ # Constrói pattern completo
84
+ full_pattern = File.join(search_path, pattern)
85
+
86
+ # Executa glob
87
+ files = Dir.glob(full_pattern, File::FNM_PATHNAME | (include_hidden ? 0 : File::FNM_DOTMATCH))
88
+
89
+ # Filtra por tipo
90
+ files = filter_by_type(files, file_type)
91
+
92
+ # Exclui hidden se necessário
93
+ unless include_hidden
94
+ files = exclude_hidden(files)
95
+ end
96
+
97
+ # Ordena
98
+ files = sort_files(files, sort_order)
99
+
100
+ # Limita resultados
101
+ limited = files.count > max_results
102
+ files = files.first(max_results)
103
+
104
+ # Coleta metadados
105
+ file_info = files.map { |f| collect_file_info(f, search_path) }
106
+
107
+ {
108
+ success: true,
109
+ pattern: pattern,
110
+ path: path,
111
+ search_path: search_path,
112
+ files: file_info,
113
+ total_found: files.count,
114
+ limited: limited,
115
+ max_results: max_results
116
+ }
117
+ end
118
+
119
+ # Verifica se é safe (read-only)
120
+ #
121
+ # @param args [Hash] Argumentos
122
+ # @return [Boolean] true
123
+ def self.safe?(args)
124
+ true
125
+ end
126
+
127
+ # Verifica se é read-only
128
+ #
129
+ # @param args [Hash] Argumentos
130
+ # @return [Boolean] true
131
+ def self.read_only?(args)
132
+ true
133
+ end
134
+
135
+ private
136
+
137
+ # Filtra arquivos por tipo
138
+ #
139
+ # @param files [Array] Lista de arquivos
140
+ # @param file_type [String] Tipo desejado
141
+ # @return [Array] Arquivos filtrados
142
+ def filter_by_type(files, file_type)
143
+ case file_type
144
+ when 'file'
145
+ files.select { |f| File.file?(f) }
146
+ when 'directory'
147
+ files.select { |f| File.directory?(f) }
148
+ else
149
+ files
150
+ end
151
+ end
152
+
153
+ # Exclui arquivos hidden
154
+ #
155
+ # @param files [Array] Lista de arquivos
156
+ # @return [Array] Arquivos sem hidden
157
+ def exclude_hidden(files)
158
+ files.reject { |f| File.basename(f).start_with?('.') }
159
+ end
160
+
161
+ # Ordena arquivos
162
+ #
163
+ # @param files [Array] Lista de arquivos
164
+ # @param sort_order [String] Ordem de sorted
165
+ # @return [Array] Arquivos ordenados
166
+ def sort_files(files, sort_order)
167
+ case sort_order
168
+ when 'name'
169
+ files.sort
170
+ when 'mtime'
171
+ files.sort_by { |f| File.mtime(f) }
172
+ when 'size'
173
+ files.sort_by { |f| File.size(f) rescue 0 }
174
+ else
175
+ files
176
+ end
177
+ end
178
+
179
+ # Coleta informações do arquivo
180
+ #
181
+ # @param file [String] Caminho do arquivo
182
+ # @param base_path [String] Path base
183
+ # @return [Hash] Informações do arquivo
184
+ def collect_file_info(file, base_path)
185
+ relative_path = file.sub("#{base_path}/", '')
186
+
187
+ {
188
+ path: file,
189
+ relative_path: relative_path,
190
+ filename: File.basename(file),
191
+ directory: File.dirname(relative_path),
192
+ type: File.file?(file) ? 'file' : 'directory',
193
+ size: File.file?(file) ? File.size(file) : nil,
194
+ mtime: File.mtime(file).iso8601
195
+ }
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ # Registra a tool
202
+ Gsd::Tools::Registry.register('glob', Gsd::Tools::GlobTool, category: :search)
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+
5
+ module Gsd
6
+ module Tools
7
+ # GrepTool - Busca por padrões em arquivos usando grep
8
+ #
9
+ # Uso:
10
+ # tool = GrepTool.new(cwd: '/path')
11
+ # result = tool.execute(pattern: 'def initialize', path: 'src/')
12
+ class GrepTool < Base
13
+ class << self
14
+ tool_name('grep')
15
+ tool_description('Search for patterns in files using grep with regex support')
16
+ tool_input_schema({
17
+ type: 'object',
18
+ properties: {
19
+ pattern: {
20
+ type: 'string',
21
+ description: 'Pattern to search for (regex supported)'
22
+ },
23
+ path: {
24
+ type: 'string',
25
+ description: 'Directory or file to search in (default: cwd)'
26
+ },
27
+ case_sensitive: {
28
+ type: 'boolean',
29
+ description: 'Case sensitive search (default: false)',
30
+ default: false
31
+ },
32
+ include: {
33
+ type: 'string',
34
+ description: 'File pattern to include (e.g., "*.rb")'
35
+ },
36
+ exclude: {
37
+ type: 'string',
38
+ description: 'File pattern to exclude (e.g., "*.test.rb")'
39
+ },
40
+ max_results: {
41
+ type: 'integer',
42
+ description: 'Maximum number of results to return (default: 100)',
43
+ default: 100
44
+ },
45
+ context_lines: {
46
+ type: 'integer',
47
+ description: 'Number of context lines to show (default: 0)',
48
+ default: 0
49
+ }
50
+ },
51
+ required: ['pattern']
52
+ })
53
+ end
54
+
55
+ # Executa busca grep
56
+ #
57
+ # @param args [Hash] Argumentos
58
+ # @return [Hash] Resultados da busca
59
+ def execute(args)
60
+ pattern = args[:pattern] || args['pattern']
61
+ path = args[:path] || args['path'] || '.'
62
+ case_sensitive = args[:case_sensitive] || args['case_sensitive'] || false
63
+ include_pattern = args[:include] || args['include']
64
+ exclude_pattern = args[:exclude] || args['exclude']
65
+ max_results = args[:max_results] || args['max_results'] || 100
66
+ context_lines = args[:context_lines] || args['context_lines'] || 0
67
+
68
+ raise ArgumentError, 'Pattern is required' unless pattern
69
+
70
+ log_debug("Searching for: #{pattern}")
71
+ log_debug("Path: #{path}, Case sensitive: #{case_sensitive}")
72
+
73
+ # Normaliza path
74
+ search_path = File.expand_path(path, @cwd)
75
+
76
+ # Verifica se path existe
77
+ unless File.exist?(search_path)
78
+ return {
79
+ success: false,
80
+ error: 'path_not_found',
81
+ message: "Path not found: #{path}"
82
+ }
83
+ end
84
+
85
+ # Constrói comando grep
86
+ grep_cmd = build_grep_command(pattern, case_sensitive, include_pattern, exclude_pattern, context_lines)
87
+
88
+ # Executa grep
89
+ result = run_shell_command("#{grep_cmd} #{escape_path(search_path)}")
90
+
91
+ if result[:success]
92
+ matches = parse_grep_output(result[:stdout], pattern)
93
+
94
+ # Limita resultados
95
+ matches = matches.first(max_results) if matches.count > max_results
96
+
97
+ {
98
+ success: true,
99
+ pattern: pattern,
100
+ path: path,
101
+ search_path: search_path,
102
+ matches: matches,
103
+ total_matches: matches.count,
104
+ limited: result[:stdout].lines.count > max_results,
105
+ command: result[:command]
106
+ }
107
+ else
108
+ # grep retorna 1 quando não encontra nada (não é erro)
109
+ if result[:status] == 1
110
+ {
111
+ success: true,
112
+ pattern: pattern,
113
+ path: path,
114
+ search_path: search_path,
115
+ matches: [],
116
+ total_matches: 0,
117
+ message: 'No matches found'
118
+ }
119
+ else
120
+ {
121
+ success: false,
122
+ error: 'grep_error',
123
+ message: result[:stderr]
124
+ }
125
+ end
126
+ end
127
+ end
128
+
129
+ # Verifica se é safe (read-only)
130
+ #
131
+ # @param args [Hash] Argumentos
132
+ # @return [Boolean] true
133
+ def self.safe?(args)
134
+ true
135
+ end
136
+
137
+ # Verifica se é read-only
138
+ #
139
+ # @param args [Hash] Argumentos
140
+ # @return [Boolean] true
141
+ def self.read_only?(args)
142
+ true
143
+ end
144
+
145
+ private
146
+
147
+ # Constrói comando grep
148
+ #
149
+ # @param pattern [String] Padrão de busca
150
+ # @param case_sensitive [Boolean] Case sensitive
151
+ # @param include [String] Pattern para incluir
152
+ # @param exclude [String] Pattern para excluir
153
+ # @param context [Integer] Linhas de contexto
154
+ # @return [String] Comando grep
155
+ def build_grep_command(pattern, case_sensitive, include, exclude, context)
156
+ cmd = ['grep']
157
+
158
+ # Case insensitive
159
+ cmd << '-i' unless case_sensitive
160
+
161
+ # Extended regex
162
+ cmd << '-E'
163
+
164
+ # Context lines
165
+ cmd << "-C #{context}" if context > 0
166
+
167
+ # Include pattern
168
+ if include
169
+ cmd << '--include' << include
170
+ end
171
+
172
+ # Exclude pattern
173
+ if exclude
174
+ cmd << '--exclude' << exclude
175
+ end
176
+
177
+ # Line numbers and color (disabled for parsing)
178
+ cmd << '-n'
179
+
180
+ # Pattern
181
+ cmd << '-e' << pattern
182
+
183
+ cmd.join(' ')
184
+ end
185
+
186
+ # Parseia output do grep
187
+ #
188
+ # @param output [String] Output do grep
189
+ # @param pattern [String] Padrão buscado
190
+ # @return [Array] Matches formatados
191
+ def parse_grep_output(output, pattern)
192
+ matches = []
193
+
194
+ output.each_line do |line|
195
+ # Formato: path/to/file.rb:line_number:content
196
+ parts = line.split(':', 3)
197
+ next if parts.count < 3
198
+
199
+ file_path = parts[0]
200
+ line_number = parts[1].to_i
201
+ content = parts[2].strip
202
+
203
+ matches << {
204
+ file: file_path,
205
+ line_number: line_number,
206
+ content: content,
207
+ pattern: pattern
208
+ }
209
+ end
210
+
211
+ matches
212
+ end
213
+
214
+ # Escapa path para shell
215
+ #
216
+ # @param path [String] Path
217
+ # @return [String] Path escapado
218
+ def escape_path(path)
219
+ # Simples: usa aspas se tiver espaços
220
+ path.include?(' ') ? "'#{path}'" : path
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ # Registra a tool
227
+ Gsd::Tools::Registry.register('grep', Gsd::Tools::GrepTool, category: :search)
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tools/base'
4
+ require 'gsd/go/bridge'
5
+
6
+ module Gsd
7
+ module Tools
8
+ # GsdFrontmatterTool - Operações de Frontmatter via Go bridge
9
+ #
10
+ # Uso:
11
+ # tool = GsdFrontmatterTool.new(cwd: '/path')
12
+ # result = tool.execute(operation: 'get', file: 'STATE.md')
13
+ class GsdFrontmatterTool < Base
14
+ class << self
15
+ tool_name('gsd_frontmatter')
16
+ tool_description('GSD Frontmatter operations: get, set, merge, validate, extract')
17
+ tool_input_schema({
18
+ type: 'object',
19
+ properties: {
20
+ operation: {
21
+ type: 'string',
22
+ description: 'Operation: get, set, merge, validate, extract',
23
+ enum: ['get', 'set', 'merge', 'validate', 'extract']
24
+ },
25
+ file: {
26
+ type: 'string',
27
+ description: 'File path with frontmatter'
28
+ },
29
+ field: {
30
+ type: 'string',
31
+ description: 'Field name (for get/set operations)'
32
+ },
33
+ value: {
34
+ type: 'string',
35
+ description: 'Field value (for set operation, JSON format)'
36
+ },
37
+ data: {
38
+ type: 'object',
39
+ description: 'Data to merge (for merge operation)'
40
+ },
41
+ schema: {
42
+ type: 'string',
43
+ description: 'Schema type: plan, summary, verification (for validate)'
44
+ },
45
+ fields: {
46
+ type: 'array',
47
+ description: 'Fields to extract (for extract operation)',
48
+ items: {
49
+ type: 'string'
50
+ }
51
+ }
52
+ },
53
+ required: ['operation', 'file']
54
+ })
55
+ end
56
+
57
+ # Executa operação de Frontmatter
58
+ #
59
+ # @param args [Hash] Argumentos
60
+ # @return [Hash] Resultado da operação
61
+ def execute(args)
62
+ operation = args[:operation] || args['operation']
63
+ file = args[:file] || args['file']
64
+ field = args[:field] || args['field']
65
+ value = args[:value] || args['value']
66
+ data = args[:data] || args['data']
67
+ schema = args[:schema] || args['schema']
68
+ fields = args[:fields] || args['fields']
69
+
70
+ raise ArgumentError, 'Operation is required' unless operation
71
+ raise ArgumentError, 'File is required' unless file
72
+
73
+ log_debug("Frontmatter operation: #{operation}, file: #{file}")
74
+
75
+ # Chama Go bridge
76
+ result = case operation
77
+ when 'get'
78
+ params = { 'get' => true, 'file' => file }
79
+ params['field'] = field if field
80
+ Gsd::Go::Bridge.call('frontmatter', params, cwd: @cwd)
81
+ when 'set'
82
+ raise ArgumentError, 'Field and value required' unless field && value
83
+ Gsd::Go::Bridge.call('frontmatter', {
84
+ 'set' => true,
85
+ 'file' => file,
86
+ 'field' => field,
87
+ 'value' => value
88
+ }, cwd: @cwd)
89
+ when 'merge'
90
+ raise ArgumentError, 'Data required' unless data
91
+ Gsd::Go::Bridge.call('frontmatter', {
92
+ 'merge' => true,
93
+ 'file' => file,
94
+ 'data' => data
95
+ }, cwd: @cwd)
96
+ when 'validate'
97
+ params = { 'validate' => true, 'file' => file }
98
+ params['schema'] = schema if schema
99
+ Gsd::Go::Bridge.call('frontmatter', params, cwd: @cwd)
100
+ when 'extract'
101
+ params = { 'extract' => true, 'file' => file }
102
+ params['fields'] = fields if fields
103
+ Gsd::Go::Bridge.call('frontmatter', params, cwd: @cwd)
104
+ else
105
+ { 'success' => false, 'error' => "Invalid operation: #{operation}" }
106
+ end
107
+
108
+ # Parseia resultado
109
+ if result['success']
110
+ {
111
+ success: true,
112
+ operation: operation,
113
+ file: file,
114
+ data: result['data'],
115
+ cwd: @cwd
116
+ }
117
+ else
118
+ {
119
+ success: false,
120
+ error: result['error'] || 'unknown_error',
121
+ message: result['message'],
122
+ operation: operation
123
+ }
124
+ end
125
+ rescue => e
126
+ log_debug("Error: #{e.message}")
127
+ {
128
+ success: false,
129
+ error: 'bridge_error',
130
+ message: e.message,
131
+ operation: operation
132
+ }
133
+ end
134
+
135
+ # Verifica se é safe
136
+ #
137
+ # @param args [Hash] Argumentos
138
+ # @return [Boolean] true apenas para get/validate/extract
139
+ def self.safe?(args)
140
+ operation = args[:operation] || args['operation']
141
+ ['get', 'validate', 'extract'].include?(operation)
142
+ end
143
+
144
+ # Verifica se é read-only
145
+ #
146
+ # @param args [Hash] Argumentos
147
+ # @return [Boolean] true apenas para get/validate/extract
148
+ def self.read_only?(args)
149
+ safe?(args)
150
+ end
151
+
152
+ # Verifica se é destrutivo
153
+ #
154
+ # @param args [Hash] Argumentos
155
+ # @return [Boolean] true para set/merge
156
+ def self.destructive?(args)
157
+ operation = args[:operation] || args['operation']
158
+ ['set', 'merge'].include?(operation)
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Registra a tool
165
+ Gsd::Tools::Registry.register('gsd_frontmatter', Gsd::Tools::GsdFrontmatterTool, category: :gsd)