aia 0.3.4 → 0.3.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,14 +5,20 @@ module AIA::PromptProcessing
5
5
 
6
6
  # Fetch the first argument which should be the prompt id
7
7
  def get_prompt
8
- prompt_id = @arguments.shift
8
+ prompt_id = AIA.config.arguments.shift
9
9
 
10
10
  # TODO: or maybe go to a generic search and select process
11
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
+
12
18
  abort("Please provide a prompt id") unless prompt_id
13
19
 
14
20
  search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
15
- edit_prompt if edit?
21
+ edit_prompt if AIA.config.edit?
16
22
  end
17
23
 
18
24
 
@@ -178,8 +184,17 @@ module AIA::PromptProcessing
178
184
 
179
185
 
180
186
  def edit_prompt
181
- `#{EDITOR} #{@prompt.path}`
182
- @options[:edit?][0] = false
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
183
198
  @prompt = PromptManager::Prompt.get(id: @prompt.id)
184
199
  end
185
200
 
@@ -210,3 +225,192 @@ module AIA::PromptProcessing
210
225
  end
211
226
  end
212
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
+
@@ -0,0 +1,52 @@
1
+ # lib/aia/tools/editor.rb
2
+ # This is the default editor setup in the
3
+ # system environment variable EDITOR
4
+
5
+
6
+ class AIA::Editor < AIA::Tools
7
+
8
+ meta(
9
+ name: 'editor',
10
+ role: :editor,
11
+ desc: "Your default system $EDITOR",
12
+ url: "unknown",
13
+ install: "should already be installed",
14
+ )
15
+
16
+ DEFAULT_PARAMETERS = ""
17
+
18
+ attr_accessor :command
19
+
20
+
21
+ def initialize(file: "")
22
+ @file = file
23
+
24
+ discover_editor
25
+
26
+ build_command
27
+ end
28
+
29
+
30
+ def discover_editor
31
+ editor = ENV['EDITOR'] # This might be nil
32
+
33
+ if editor.nil?
34
+ @name = "echo"
35
+ @description = "You have no default editor"
36
+ @install = "Set your system environment variable EDITOR"
37
+ else
38
+ @name = editor
39
+ end
40
+ end
41
+
42
+
43
+ def build_command
44
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
45
+ end
46
+
47
+
48
+ def run
49
+ `#{command}`
50
+ end
51
+ end
52
+
@@ -1,22 +1,118 @@
1
- # lib/aia/external/mods.rb
2
-
3
- class AIA::External::Mods < AIA::External::Tool
4
- def initialize
5
- super
6
- @role = :search
7
- @desc = 'AI on the command-line'
8
- @url = 'https://github.com/charmbracelet/mods'
1
+ # lib/aia/tools/mods.rb
2
+
3
+ class AIA::Mods < AIA::Tools
4
+
5
+ meta(
6
+ name: 'mods',
7
+ role: :backend,
8
+ desc: 'AI on the command-line',
9
+ url: 'https://github.com/charmbracelet/mods',
10
+ install: 'brew install mods',
11
+ )
12
+
13
+
14
+ DEFAULT_PARAMETERS = [
15
+ "--no-limit" # no limit on input context
16
+ ].join(' ').freeze
17
+
18
+ attr_accessor :command, :text, :files
19
+
20
+
21
+ def initialize(
22
+ text: "", # prompt text after keyword replacement
23
+ files: [] # context file paths (Array of Pathname)
24
+ )
25
+
26
+ @text = text
27
+ @files = files
28
+
29
+ build_command
9
30
  end
10
31
 
11
- def command(extra_options = [])
12
- model = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
13
- ai_default_opts = "-m #{model} --no-limit -f"
14
- "#{name} #{ai_default_opts} #{extra_options.join(' ')}"
32
+
33
+ def sanitize(input)
34
+ Shellwords.escape(input)
35
+ end
36
+
37
+
38
+ def build_command
39
+ parameters = DEFAULT_PARAMETERS.dup + " "
40
+ parameters += "-f " if AIA.config.markdown?
41
+ parameters += "-m #{AIA.config.model} " if AIA.config.model
42
+ parameters += AIA.config.extra
43
+ @command = "mods #{parameters} "
44
+ @command += sanitize(@text)
45
+
46
+ # context = @files.join(' ')
47
+ #
48
+ # unless context.empty?
49
+ # if @files.size > 1
50
+ # # FIXME: This syntax breaks mods which does not know how
51
+ # # to read the temporary file descriptor created
52
+ # # by the shell
53
+ # @command += " <(cat #{context})"
54
+ # else
55
+ # @command += " < #{context}"
56
+ # end
57
+ # end
58
+
59
+ @command
60
+ end
61
+
62
+
63
+ def run
64
+ case @files.size
65
+ when 0
66
+ @result = `#{build_command}`
67
+ when 1
68
+ @result = `#{build_command} < #{@files.first}`
69
+ else
70
+ create_temp_file_with_contexts
71
+ run_mods_with_temp_file
72
+ clean_up_temp_file
73
+ end
74
+
75
+ @result
76
+ end
77
+
78
+
79
+ # Create a temporary file that concatenates all contexts,
80
+ # to be used as STDIN for the 'mods' utility
81
+ def create_temp_file_with_contexts
82
+ @temp_file = Tempfile.new('mods-context')
83
+
84
+ @files.each do |file|
85
+ content = File.read(file)
86
+ @temp_file.write(content)
87
+ @temp_file.write("\n")
88
+ end
89
+
90
+ @temp_file.close
91
+ end
92
+
93
+
94
+ # Run 'mods' with the temporary file as STDIN
95
+ def run_mods_with_temp_file
96
+ command = "#{build_command} < #{@temp_file.path}"
97
+ @result = `#{command}`
98
+ end
99
+
100
+
101
+ # Clean up the temporary file after use
102
+ def clean_up_temp_file
103
+ @temp_file.unlink if @temp_file
15
104
  end
16
105
  end
17
106
 
18
107
  __END__
19
108
 
109
+
110
+
111
+
112
+
113
+ ##########################################################
114
+
115
+
20
116
  GPT on the command line. Built for pipelines.
21
117
 
22
118
  Usage:
@@ -1,12 +1,17 @@
1
- # lib/aia/external/sgpt.rb
1
+ # lib/aia/tools/sgpt.rb
2
+
3
+ class AIA::Sgpt < AIA::Tools
4
+
5
+ meta(
6
+ name: 'sgpt',
7
+ role: :backend,
8
+ desc: "shell-gpt",
9
+ url: "https://github.com/TheR1D/shell_gpt",
10
+ install: "pip install shell-gpt",
11
+ )
2
12
 
3
- class AIA::External::Sgpt < AIA::External::Tool
4
13
  def initialize
5
- super
6
- @role = :backend
7
- @desc = "shell-gpt"
8
- @url = "https://github.com/TheR1D/shell_gpt"
9
- @install = "pip install shell-gpt"
14
+ # TODO: something
10
15
  end
11
16
  end
12
17
 
@@ -1,17 +1,38 @@
1
- # lib/aia/external/subl.rb
2
-
3
- class AIA::External::Subl < AIA::External::Tool
4
- def initialize
5
- super
6
- @role = :editor
7
- @desc = "Sublime Text Editor"
8
- @url = "https://www.sublimetext.com/"
9
- @install = "echo 'Download from website'"
1
+ # lib/aia/tools/subl.rb
2
+
3
+ class AIA::Subl < AIA::Tools
4
+
5
+ meta(
6
+ name: 'subl',
7
+ role: :editor,
8
+ desc: "Sublime Text Editor",
9
+ url: "https://www.sublimetext.com/",
10
+ install: "echo 'Download from website'",
11
+ )
12
+
13
+
14
+ DEFAULT_PARAMETERS = [
15
+ "--new-window", # Open a new window
16
+ "--wait", # Wait for the files to be closed before returning
17
+ ].join(' ')
18
+
19
+ attr_accessor :command
20
+
21
+
22
+ def initialize(file: "")
23
+ @file = file
24
+
25
+ build_command
26
+ end
27
+
28
+
29
+ def build_command
30
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
10
31
  end
11
32
 
12
33
 
13
- def open(file)
14
- `#{name} #{file}`
34
+ def run
35
+ `#{command}`
15
36
  end
16
37
  end
17
38
 
@@ -0,0 +1,97 @@
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
+
@@ -0,0 +1,93 @@
1
+ # lib/aia/tools/vim.rb
2
+
3
+ class AIA::Vim < AIA::Tools
4
+
5
+ meta(
6
+ name: 'vim',
7
+ role: :editor,
8
+ desc: "Vi IMproved (VIM)",
9
+ url: "https://www.vim.org",
10
+ install: "brew install vim",
11
+ )
12
+
13
+ DEFAULT_PARAMETERS = [
14
+ " ", # no parameters
15
+ ].join(' ')
16
+
17
+ attr_accessor :command
18
+
19
+
20
+ def initialize(file: "")
21
+ @file = file
22
+
23
+ build_command
24
+ end
25
+
26
+
27
+ def build_command
28
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
29
+ end
30
+
31
+
32
+ def run
33
+ # Using 'system' instead of backticks becuase
34
+ # with the back ticks vim was complaining that it
35
+ # was not connected to a terminal.
36
+ system command
37
+ end
38
+ end
39
+
40
+ __END__
41
+
42
+ VIM - Vi IMproved 9.0 (2022 Jun 28, compiled Sep 30 2023 05:45:56)
43
+
44
+ Usage: vim [arguments] [file ..] edit specified file(s)
45
+ or: vim [arguments] - read text from stdin
46
+ or: vim [arguments] -t tag edit file where tag is defined
47
+ or: vim [arguments] -q [errorfile] edit file with first error
48
+
49
+ Arguments:
50
+ -- Only file names after this
51
+ -v Vi mode (like "vi")
52
+ -e Ex mode (like "ex")
53
+ -E Improved Ex mode
54
+ -s Silent (batch) mode (only for "ex")
55
+ -d Diff mode (like "vimdiff")
56
+ -y Easy mode (like "evim", modeless)
57
+ -R Readonly mode (like "view")
58
+ -Z Restricted mode (like "rvim")
59
+ -m Modifications (writing files) not allowed
60
+ -M Modifications in text not allowed
61
+ -b Binary mode
62
+ -l Lisp mode
63
+ -C Compatible with Vi: 'compatible'
64
+ -N Not fully Vi compatible: 'nocompatible'
65
+ -V[N][fname] Be verbose [level N] [log messages to fname]
66
+ -D Debugging mode
67
+ -n No swap file, use memory only
68
+ -r List swap files and exit
69
+ -r (with file name) Recover crashed session
70
+ -L Same as -r
71
+ -T <terminal> Set terminal type to <terminal>
72
+ --not-a-term Skip warning for input/output not being a terminal
73
+ --ttyfail Exit if input or output is not a terminal
74
+ -u <vimrc> Use <vimrc> instead of any .vimrc
75
+ --noplugin Don't load plugin scripts
76
+ -p[N] Open N tab pages (default: one for each file)
77
+ -o[N] Open N windows (default: one for each file)
78
+ -O[N] Like -o but split vertically
79
+ + Start at end of file
80
+ +<lnum> Start at line <lnum>
81
+ --cmd <command> Execute <command> before loading any vimrc file
82
+ -c <command> Execute <command> after loading the first file
83
+ -S <session> Source file <session> after loading the first file
84
+ -s <scriptin> Read Normal mode commands from file <scriptin>
85
+ -w <scriptout> Append all typed commands to file <scriptout>
86
+ -W <scriptout> Write all typed commands to file <scriptout>
87
+ -x Edit encrypted files
88
+ --startuptime <file> Write startup timing messages to <file>
89
+ --log <file> Start logging to <file> early
90
+ -i <viminfo> Use <viminfo> instead of .viminfo
91
+ --clean 'nocompatible', Vim defaults, no plugins, no viminfo
92
+ -h or --help Print Help (this message) and exit
93
+ --version Print version information and exit