aia 0.3.4 → 0.3.20

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.
@@ -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