aia 0.3.20 → 0.4.1
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 +4 -4
- data/.semver +2 -2
- data/CHANGELOG.md +5 -0
- data/README.md +112 -87
- data/lib/aia/cli.rb +10 -5
- data/lib/aia/directives.rb +66 -0
- data/lib/aia/main.rb +94 -13
- data/lib/aia/prompt.rb +267 -0
- data/lib/aia/tools/backend_common.rb +76 -0
- data/lib/aia/tools/mods.rb +47 -118
- data/lib/aia/tools/sgpt.rb +19 -3
- data/lib/aia/tools.rb +2 -0
- data/lib/aia.rb +1 -1
- data/man/aia.1 +51 -42
- data/man/aia.1.md +59 -38
- metadata +19 -4
- data/lib/aia/prompt_processing.rb +0 -416
- data/lib/aia/tools/temp.md +0 -97
@@ -1,416 +0,0 @@
|
|
1
|
-
# lib/aia/prompt_processing.rb
|
2
|
-
|
3
|
-
module AIA::PromptProcessing
|
4
|
-
KW_HISTORY_MAX = 5
|
5
|
-
|
6
|
-
# Fetch the first argument which should be the prompt id
|
7
|
-
def get_prompt
|
8
|
-
prompt_id = AIA.config.arguments.shift
|
9
|
-
|
10
|
-
# TODO: or maybe go to a generic search and select process
|
11
|
-
|
12
|
-
# TODO: if external options were provided but no prompt id
|
13
|
-
# then by pass prompt process and send the extra
|
14
|
-
# options to the backend. For example:
|
15
|
-
# aia -- --settings
|
16
|
-
# should send --settings to mods and nothing else
|
17
|
-
|
18
|
-
abort("Please provide a prompt id") unless prompt_id
|
19
|
-
|
20
|
-
search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
|
21
|
-
edit_prompt if AIA.config.edit?
|
22
|
-
end
|
23
|
-
|
24
|
-
|
25
|
-
# Check if a prompt with the given id already exists
|
26
|
-
def existing_prompt?(prompt_id)
|
27
|
-
@prompt = PromptManager::Prompt.get(id: prompt_id)
|
28
|
-
|
29
|
-
# FIXME: Kludge until prompt_manager is changed
|
30
|
-
# prompt_manager v0.3.0 now supports this feature.
|
31
|
-
# keeping the kludge in for legacy JSON files
|
32
|
-
# files which have not yet been reformatted.
|
33
|
-
@prompt.keywords.each do |kw|
|
34
|
-
if @prompt.parameters[kw].nil? || @prompt.parameters[kw].empty?
|
35
|
-
@prompt.parameters[kw] = []
|
36
|
-
else
|
37
|
-
@prompt.parameters[kw] = Array(@prompt.parameters[kw])
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
true
|
42
|
-
rescue ArgumentError
|
43
|
-
false
|
44
|
-
end
|
45
|
-
|
46
|
-
|
47
|
-
# Process the prompt's associated keywords and parameters
|
48
|
-
def process_prompt
|
49
|
-
unless @prompt.keywords.empty?
|
50
|
-
replace_keywords
|
51
|
-
@prompt.build
|
52
|
-
@prompt.save
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
def replace_keywords
|
59
|
-
puts
|
60
|
-
puts "ID: #{@prompt.id}"
|
61
|
-
|
62
|
-
show_prompt_without_comments
|
63
|
-
|
64
|
-
puts "\nPress up/down arrow to scroll through history."
|
65
|
-
puts "Type new input or edit the current input."
|
66
|
-
puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
|
67
|
-
puts
|
68
|
-
@prompt.keywords.each do |kw|
|
69
|
-
value = keyword_value(kw, @prompt.parameters[kw])
|
70
|
-
|
71
|
-
unless value.nil? || value.strip.empty?
|
72
|
-
value_inx = @prompt.parameters[kw].index(value)
|
73
|
-
|
74
|
-
if value_inx
|
75
|
-
@prompt.parameters[kw].delete_at(value_inx)
|
76
|
-
end
|
77
|
-
|
78
|
-
# The most recent value for this kw will always be
|
79
|
-
# in the last position
|
80
|
-
@prompt.parameters[kw] << value
|
81
|
-
@prompt.parameters[kw].shift if @prompt.parameters[kw].size > KW_HISTORY_MAX
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
|
87
|
-
# query the user for a value to the keyword allow the
|
88
|
-
# reuse of the previous value shown as the default
|
89
|
-
def keyword_value(kw, history_array)
|
90
|
-
|
91
|
-
Readline::HISTORY.clear
|
92
|
-
Array(history_array).each { |entry| Readline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
|
93
|
-
|
94
|
-
puts "Parameter #{kw} ..."
|
95
|
-
|
96
|
-
begin
|
97
|
-
a_string = Readline.readline("\n-=> ", true)
|
98
|
-
rescue Interrupt
|
99
|
-
a_string = nil
|
100
|
-
end
|
101
|
-
|
102
|
-
if a_string.nil?
|
103
|
-
puts "okay. Come back soon."
|
104
|
-
exit
|
105
|
-
end
|
106
|
-
|
107
|
-
puts
|
108
|
-
a_string.empty? ? default : a_string
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
# Search for a prompt with a matching id or keyword
|
113
|
-
def search_for_a_matching_prompt(prompt_id)
|
114
|
-
# TODO: using the rgfzf version of the search_proc should only
|
115
|
-
# return a single prompt_id
|
116
|
-
found_prompts = PromptManager::Prompt.search(prompt_id)
|
117
|
-
|
118
|
-
if found_prompts.empty?
|
119
|
-
if edit?
|
120
|
-
create_prompt(prompt_id)
|
121
|
-
edit_prompt
|
122
|
-
else
|
123
|
-
abort <<~EOS
|
124
|
-
|
125
|
-
No prompts where found for: #{prompt_id}
|
126
|
-
To create a prompt with this ID use the --edit option
|
127
|
-
like this:
|
128
|
-
#{MY_NAME} #{prompt_id} --edit
|
129
|
-
|
130
|
-
EOS
|
131
|
-
end
|
132
|
-
else
|
133
|
-
prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
|
134
|
-
@prompt = PromptManager::Prompt.get(id: prompt_id)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
|
139
|
-
def handle_multiple_prompts(found_these, while_looking_for_this)
|
140
|
-
raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
|
141
|
-
|
142
|
-
# TODO: Make this a class constant for defaults; make the header content
|
143
|
-
# a parameter so it can be varied.
|
144
|
-
fzf_options = [
|
145
|
-
"--tabstop=2", # 2 soaces for a tab
|
146
|
-
"--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
|
147
|
-
"--header-first",
|
148
|
-
"--prompt='Search term: '",
|
149
|
-
'--delimiter :',
|
150
|
-
"--preview 'cat $PROMPTS_DIR/{1}.txt'",
|
151
|
-
"--preview-window=down:50%:wrap"
|
152
|
-
].join(' ')
|
153
|
-
|
154
|
-
|
155
|
-
# Create a temporary file to hold the list of strings
|
156
|
-
temp_file = Tempfile.new('fzf-input')
|
157
|
-
|
158
|
-
begin
|
159
|
-
# Write all strings to the temp file
|
160
|
-
temp_file.puts(found_these)
|
161
|
-
temp_file.close
|
162
|
-
|
163
|
-
# Execute fzf command-line utility to allow selection
|
164
|
-
selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
|
165
|
-
|
166
|
-
# Check if fzf actually returned a string; if not, return nil
|
167
|
-
result = selected.empty? ? nil : selected
|
168
|
-
ensure
|
169
|
-
# Ensure that the tempfile is closed and unlinked
|
170
|
-
temp_file.unlink
|
171
|
-
end
|
172
|
-
|
173
|
-
exit unless result
|
174
|
-
|
175
|
-
result
|
176
|
-
end
|
177
|
-
|
178
|
-
|
179
|
-
def create_prompt(prompt_id)
|
180
|
-
@prompt = PromptManager::Prompt.create(id: prompt_id)
|
181
|
-
# TODO: consider a configurable prompt template
|
182
|
-
# ERB ???
|
183
|
-
end
|
184
|
-
|
185
|
-
|
186
|
-
def edit_prompt
|
187
|
-
# FIXME: replace with the editor from the configuration
|
188
|
-
|
189
|
-
@editor = AIA::Subl.new(
|
190
|
-
file: @prompt.path
|
191
|
-
)
|
192
|
-
|
193
|
-
@editor.run # blocks until file is closed
|
194
|
-
|
195
|
-
@options[:edit?][0] = false # turn off the --edit switch
|
196
|
-
|
197
|
-
# reload the edited prompt
|
198
|
-
@prompt = PromptManager::Prompt.get(id: @prompt.id)
|
199
|
-
end
|
200
|
-
|
201
|
-
|
202
|
-
def show_prompt_without_comments
|
203
|
-
puts remove_comments.wrap(indent: 4)
|
204
|
-
end
|
205
|
-
|
206
|
-
|
207
|
-
def remove_comments
|
208
|
-
lines = @prompt.text
|
209
|
-
.split("\n")
|
210
|
-
.reject{|a_line| a_line.strip.start_with?('#')}
|
211
|
-
|
212
|
-
# Remove empty lines at the start of the prompt
|
213
|
-
#
|
214
|
-
lines = lines.drop_while(&:empty?)
|
215
|
-
|
216
|
-
# Drop all the lines at __END__ and after
|
217
|
-
#
|
218
|
-
logical_end_inx = lines.index("__END__")
|
219
|
-
|
220
|
-
if logical_end_inx
|
221
|
-
lines[0...logical_end_inx] # NOTE: ... means to not include last index
|
222
|
-
else
|
223
|
-
lines
|
224
|
-
end.join("\n")
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
__END__
|
231
|
-
|
232
|
-
# lib/aia/prompt_processing.rb
|
233
|
-
|
234
|
-
class AIA::PromptProcessing
|
235
|
-
KW_HISTORY_MAX = 5
|
236
|
-
|
237
|
-
def initialize(arguments, options)
|
238
|
-
@arguments = arguments
|
239
|
-
@options = options
|
240
|
-
@prompt = nil
|
241
|
-
end
|
242
|
-
|
243
|
-
def execute
|
244
|
-
get_prompt
|
245
|
-
process_prompt
|
246
|
-
end
|
247
|
-
|
248
|
-
private
|
249
|
-
|
250
|
-
def get_prompt
|
251
|
-
prompt_id = @arguments.shift
|
252
|
-
abort("Please provide a prompt id") unless prompt_id
|
253
|
-
search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
|
254
|
-
edit_prompt if edit?
|
255
|
-
end
|
256
|
-
|
257
|
-
def existing_prompt?(prompt_id)
|
258
|
-
@prompt = PromptManager::Prompt.get(id: prompt_id)
|
259
|
-
@prompt.keywords.each do |kw|
|
260
|
-
@prompt.parameters[kw] = Array(@prompt.parameters[kw])
|
261
|
-
end
|
262
|
-
true
|
263
|
-
rescue ArgumentError
|
264
|
-
false
|
265
|
-
end
|
266
|
-
|
267
|
-
def process_prompt
|
268
|
-
return if @prompt.keywords.empty?
|
269
|
-
replace_keywords
|
270
|
-
@prompt.build
|
271
|
-
@prompt.save
|
272
|
-
end
|
273
|
-
|
274
|
-
def replace_keywords
|
275
|
-
puts "ID: #{@prompt.id}"
|
276
|
-
show_prompt_without_comments
|
277
|
-
puts_instructions
|
278
|
-
@prompt.keywords.each do |kw|
|
279
|
-
value = keyword_value(kw, @prompt.parameters[kw])
|
280
|
-
update_keyword_history(kw, value) unless value.nil? || value.strip.empty?
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
def puts_instructions
|
285
|
-
puts "\nPress up/down arrow to scroll through history."
|
286
|
-
puts "Type new input or edit the current input."
|
287
|
-
puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
|
288
|
-
puts
|
289
|
-
end
|
290
|
-
|
291
|
-
def update_keyword_history(kw, value)
|
292
|
-
params = @prompt.parameters[kw]
|
293
|
-
params.delete(value)
|
294
|
-
params << value
|
295
|
-
params.shift if params.size > KW_HISTORY_MAX
|
296
|
-
end
|
297
|
-
|
298
|
-
def keyword_value(kw, history_array)
|
299
|
-
Readline::HISTORY.clear
|
300
|
-
Array(history_array).each { |entry| Readline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
|
301
|
-
puts "Parameter #{kw} ..."
|
302
|
-
begin
|
303
|
-
a_string = Readline.readline("\n-=> ", true)
|
304
|
-
rescue Interrupt
|
305
|
-
a_string = nil
|
306
|
-
end
|
307
|
-
abort("okay. Come back soon.") if a_string.nil?
|
308
|
-
a_string.empty? ? history_array.first : a_string
|
309
|
-
end
|
310
|
-
|
311
|
-
def search_for_a_matching_prompt(prompt_id)
|
312
|
-
found_prompts = PromptManager::Prompt.search(prompt_id)
|
313
|
-
handle_no_prompts_found(prompt_id) if found_prompts.empty?
|
314
|
-
prompt_id = found_prompts.size == 1 ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
|
315
|
-
@prompt = PromptManager::Prompt.get(id: prompt_id)
|
316
|
-
end
|
317
|
-
|
318
|
-
def handle_no_prompts_found(prompt_id)
|
319
|
-
if edit?
|
320
|
-
create_prompt(prompt_id)
|
321
|
-
edit_prompt
|
322
|
-
else
|
323
|
-
abort_no_prompts_error(prompt_id)
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
def abort_no_prompts_error(prompt_id)
|
328
|
-
abort <<~EOS
|
329
|
-
|
330
|
-
No prompts were found for: #{prompt_id}
|
331
|
-
To create a prompt with this ID use the --edit option
|
332
|
-
like this:
|
333
|
-
#{MY_NAME} #{prompt_id} --edit
|
334
|
-
|
335
|
-
EOS
|
336
|
-
end
|
337
|
-
|
338
|
-
def handle_multiple_prompts(found_these, while_looking_for_this)
|
339
|
-
raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
|
340
|
-
result = execute_fzf(found_these, while_looking_for_this)
|
341
|
-
abort unless result
|
342
|
-
result
|
343
|
-
end
|
344
|
-
|
345
|
-
def execute_fzf(found_these, while_looking_for_this)
|
346
|
-
fzf_options = build_fzf_options(while_looking_for_this)
|
347
|
-
temp_file = create_tempfile_with_entries(found_these)
|
348
|
-
selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
|
349
|
-
temp_file.unlink
|
350
|
-
selected.empty? ? nil : selected
|
351
|
-
end
|
352
|
-
|
353
|
-
def build_fzf_options(search_term)
|
354
|
-
[
|
355
|
-
"--tabstop=2",
|
356
|
-
"--header='Prompt IDs which contain: #{search_term}\nPress ESC to cancel.'",
|
357
|
-
"--header-first",
|
358
|
-
"--prompt='Search term: '",
|
359
|
-
'--delimiter :',
|
360
|
-
"--preview 'cat $PROMPTS_DIR/{1}.txt'",
|
361
|
-
"--preview-window=down:50%:wrap"
|
362
|
-
].join(' ')
|
363
|
-
end
|
364
|
-
|
365
|
-
def create_tempfile_with_entries(entries)
|
366
|
-
temp_file = Tempfile.new('fzf-input')
|
367
|
-
temp_file.puts(entries)
|
368
|
-
temp_file.close
|
369
|
-
temp_file
|
370
|
-
end
|
371
|
-
|
372
|
-
def create_prompt(prompt_id)
|
373
|
-
@prompt = PromptManager::Prompt.create(id: prompt_id)
|
374
|
-
# Additional prompt config...
|
375
|
-
end
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
def edit_prompt
|
380
|
-
# FIXME: replace with the editor from the configuration
|
381
|
-
|
382
|
-
@editor = AIA::Subl.new(
|
383
|
-
file: @prompt.path
|
384
|
-
)
|
385
|
-
|
386
|
-
@editor.run # blocks until file is closed
|
387
|
-
|
388
|
-
@options[:edit?][0] = false # turn off the --edit switch
|
389
|
-
|
390
|
-
# reload the edited prompt
|
391
|
-
@prompt = PromptManager::Prompt.get(id: @prompt.id)
|
392
|
-
end
|
393
|
-
|
394
|
-
|
395
|
-
def show_prompt_without_comments
|
396
|
-
puts remove_comments.wrap(indent: 4)
|
397
|
-
end
|
398
|
-
|
399
|
-
def remove_comments
|
400
|
-
lines = @prompt.text.lines
|
401
|
-
.reject { |a_line| a_line.strip.start_with?('#') }
|
402
|
-
.drop_while(&:empty?)
|
403
|
-
logical_end_inx = lines.index("__END__")
|
404
|
-
lines = lines[0...logical_end_inx] if logical_end_inx
|
405
|
-
lines.join("\n")
|
406
|
-
end
|
407
|
-
|
408
|
-
def edit?
|
409
|
-
@options[:edit?] && @options[:edit?][0] == true
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
data/lib/aia/tools/temp.md
DELETED
@@ -1,97 +0,0 @@
|
|
1
|
-
# lib/aia/tools.rb
|
2
|
-
|
3
|
-
```ruby
|
4
|
-
require 'hashie'
|
5
|
-
|
6
|
-
module AIA
|
7
|
-
class Tools
|
8
|
-
@subclasses = {}
|
9
|
-
|
10
|
-
class << self
|
11
|
-
attr_reader :subclasses, :metadata
|
12
|
-
|
13
|
-
def inherited(subclass)
|
14
|
-
@subclasses[subclass.name.split('::').last.downcase] = subclass
|
15
|
-
subclass.instance_variable_set(:@metadata, Jashie::Mash.new)
|
16
|
-
end
|
17
|
-
|
18
|
-
def meta
|
19
|
-
@metadata ||= Jashie::Mash.new
|
20
|
-
end
|
21
|
-
|
22
|
-
def define_metadata(&block)
|
23
|
-
meta.instance_eval(&block)
|
24
|
-
end
|
25
|
-
|
26
|
-
def search_for(name: nil, role: nil)
|
27
|
-
return subclasses[name.downcase] if name
|
28
|
-
return subclasses.values.select { |subclass| subclass.meta.role == role } if role
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.method_missing(name, *args, &block)
|
33
|
-
@metadata.public_send(name, *args, &block)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.respond_to_missing?(method_name, include_private = false)
|
37
|
-
@metadata.respond_to?(method_name) || super
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
```
|
42
|
-
|
43
|
-
# lib/aia/tools/mods.rb
|
44
|
-
|
45
|
-
```ruby
|
46
|
-
require_relative 'tools'
|
47
|
-
|
48
|
-
module AIA
|
49
|
-
class Mods < Tools
|
50
|
-
DEFAULT_PARAMETERS = "--no-limit".freeze
|
51
|
-
|
52
|
-
attr_accessor :command, :extra_options, :text, :files
|
53
|
-
|
54
|
-
define_metadata do
|
55
|
-
role :backend
|
56
|
-
desc 'AI on the command-line'
|
57
|
-
url 'https://github.com/charmbracelet/mods'
|
58
|
-
end
|
59
|
-
|
60
|
-
def initialize(extra_options: "", text: "", files: [])
|
61
|
-
@extra_options = extra_options
|
62
|
-
@text = text
|
63
|
-
@files = files
|
64
|
-
build_command
|
65
|
-
end
|
66
|
-
|
67
|
-
def build_command
|
68
|
-
parameters = DEFAULT_PARAMETERS.dup + " "
|
69
|
-
parameters += "-f " if ::AIA.config.markdown?
|
70
|
-
parameters += "-m #{AIA.config.model} " if ::AIA.config.model
|
71
|
-
parameters += @extra_options
|
72
|
-
@command = "mods #{parameters}"
|
73
|
-
@command += %Q["#{@text}"]
|
74
|
-
|
75
|
-
@files.each { |f| @command += " < #{f}" }
|
76
|
-
|
77
|
-
@command
|
78
|
-
end
|
79
|
-
|
80
|
-
def run
|
81
|
-
`#{@command}`
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
```
|
86
|
-
|
87
|
-
```ruby
|
88
|
-
# Example usage:
|
89
|
-
# mods_class = AIA::Tools.search_for(name: 'mods')
|
90
|
-
# mods_instance = mods_class.new(text: "Hello, mods!")
|
91
|
-
# result = mods_instance.run
|
92
|
-
|
93
|
-
# backend_tools = AIA::Tools.search_for(role: :backend)
|
94
|
-
```
|
95
|
-
|
96
|
-
Note: The `Jashie::Mash` class is assumed to behave like `Hashie::Mash` (or similar) in providing a flexible object for storing metadata. You'll need to define `Jashie::Mash` or import a library that provides a similar functionality to match this example.
|
97
|
-
|