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,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)
|