aia 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '007318bde5f637b132ff058c7836e3a8a359507e035cff689d0f7387d6f7ec5d'
4
- data.tar.gz: 74883f4d1f9912ffbc1943191925ebf468c2bea1c9bca473582012ec2b3a1997
3
+ metadata.gz: 5737dc3f15dc0372865893efba9c8e7bb6c4c41fc335476b638cb55a5dc4ad17
4
+ data.tar.gz: 91545c6a73d8d6f61cbf878f35df375f3e700961d1d70b25bd3cb0aa0cc25251
5
5
  SHA512:
6
- metadata.gz: 7ffea7e6341a88e6ed95536436c2d0ded48dd0a7fd18bbc8755c4eae9cd394991c783212634d698f403eb8255a7f816b4aa65972a40a5247121a4ec50d224020
7
- data.tar.gz: c2b08ce3ba532bf00041cf31534973766f95c3d35fa38703876844c7ba68950e85975db0681b5b50ba1bc3bf5475a815d1d820ecf3968007e66ed58b55d54b2b
6
+ metadata.gz: 7a276b2fdad405245fd29f8aba586504ad2a99cc4c8f3f570af1de58849c1ee2abda2f30c5a72fbd699db37f01b94e79711eb4833495cebeae33c9562d03033f
7
+ data.tar.gz: 2c570b4339d3f24b6621378481393994272a0f0317ab4edb2ae9ca79007a81b288cc09eeae0f9326f72892ce742d41e9bbfe180de3a8bb6f652a1fc808d1e62f
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ require "rake/testtask"
15
15
  Rake::TestTask.new(:test) do |t|
16
16
  t.libs << "test"
17
17
  t.libs << "lib"
18
- t.test_files = FileList["test/**/test_*.rb"]
18
+ t.test_files = FileList["test/**/*_test.rb"]
19
19
  end
20
20
 
21
21
  task default: :test
data/bin/aia CHANGED
@@ -1,272 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
- # encoding: utf-8
3
- # frozen_string_literal: true
4
- # warn_indent: true
5
- ##########################################################
6
- ###
7
- ## File: aia
8
- ## Desc: AI Assistant
9
- ## Use generative AI with saved parameterized prompts
10
- ## By: Dewayne VanHoozer (dvanhoozer@gmail.com)
11
- ##
12
- ## This program makes use of the gem word_wrap's
13
- ## CLI tool: ww
14
- ##
15
- ## brew install fzf mods the_silver_searcher ripgrep
16
- #
17
- # TODO: refactor with a goal to isolate the search_proc and mods functionality
18
- # TODO: consider a config file.
19
- # TODO: remove use of CLI Helper.
20
- # TODO: consider --no-log
21
- #
2
+ # aia
22
3
 
23
- require 'pathname'
24
- HOME = Pathname.new( ENV['HOME'] )
25
- PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
4
+ require 'aia'
26
5
 
27
- require 'prompt_manager'
28
- require 'prompt_manager/storage/file_system_adapter'
29
-
30
- PromptManager::Prompt.storage_adapter =
31
- PromptManager::Storage::FileSystemAdapter.config do |config|
32
- config.prompts_dir = PROMPTS_DIR
33
- config.prompt_extension = '.txt' # default
34
- config.params_extension = '.json' # default
35
- # config.search_proc = nil # default
36
- end.new
37
-
38
-
39
-
40
- require 'amazing_print'
41
- require 'readline' # TODO: or reline ??
42
- require 'word_wrap'
43
- require 'word_wrap/core_ext'
44
-
45
- require 'debug_me'
46
- include DebugMe
47
-
48
- require 'cli_helper'
49
- include CliHelper
50
-
51
- MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
52
-
53
- AI_CLI_PROGRAM = "mods"
54
- ai_default_opts = "-m #{MODS_MODEL} --no-limit -f"
55
- ai_options = ai_default_opts.dup
56
-
57
- extra_inx = ARGV.index('--')
58
-
59
- if extra_inx
60
- ai_options += " " + ARGV[extra_inx+1..].join(' ')
61
- ARGV.pop(ARGV.size - extra_inx)
62
- end
63
-
64
- AI_COMMAND = "#{AI_CLI_PROGRAM} #{ai_options} "
65
- EDITOR = ENV['EDITOR']
66
- PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
67
-
68
- # PROMPT_EXTNAME = ".txt"
69
- # DEFAULTS_EXTNAME = ".json"
70
-
71
- # SEARCH_COMMAND = "ag -l"
72
- # KEYWORD_REGEX = /(\[[A-Z _|]+\])/
73
-
74
-
75
- configatron.version = '1.2.0'
76
-
77
- AI_CLI_PROGRAM_HELP = `#{AI_CLI_PROGRAM} --help`
78
-
79
- HELP = <<EOHELP
80
- AI Assistant (aia)
81
- ==================
82
-
83
- The AI cli program being used is: #{AI_CLI_PROGRAM}
84
-
85
- The defaul options to #{AI_CLI_PROGRAM} are:
86
- "#{ai_default_opts}"
87
-
88
- You can pass additional CLI options to #{AI_CLI_PROGRAM} like this:
89
- "#{my_name} my options -- options for #{AI_CLI_PROGRAM}"
90
-
91
- EOHELP
92
-
93
- cli_helper("Use generative AI with saved parameterized prompts") do |o|
94
- o.bool '-f', '--fuzzy', 'Allow fuzzy matching', default: false
95
- o.path '-o', '--output', 'The output file', default: Pathname.pwd + "temp.md"
96
- end
97
-
98
-
99
- AG_COMMAND = "ag --file-search-regex '\.txt$' e" # searching for the letter "e"
100
- CD_COMMAND = "cd #{PROMPTS_DIR}"
101
- FIND_COMMAND = "find . -name '*.txt'"
102
-
103
- FZF_OPTIONS = [
104
- "--tabstop=2", # 2 soaces for a tab
105
- "--header='Prompt contents below'",
106
- "--header-first",
107
- "--prompt='Search term: '",
108
- '--delimiter :',
109
- "--preview 'ww {1}'", # ww comes from the word_wrap gem
110
- "--preview-window=down:50%:wrap"
111
- ].join(' ')
112
-
113
- FZF_OPTIONS += " --exact" unless fuzzy?
114
-
115
- FZF_COMMAND = "#{CD_COMMAND} ; #{FIND_COMMAND} | fzf #{FZF_OPTIONS}"
116
- AG_FZF_COMMAND = "#{CD_COMMAND} ; #{AG_COMMAND} | fzf #{FZF_OPTIONS}"
117
-
118
- # use `ag` to build a list of text lines from each prompt
119
- # use `fzf` to search through that list to select a prompt file
120
-
121
- def ag_fzf = `#{AG_FZF_COMMAND}`.split(':')&.first&.strip&.gsub('.txt','')
122
-
123
-
124
-
125
- # The prompt_id is always the first argument
126
- if configatron.arguments.empty?
127
- show_usage
128
- exit
129
- end
130
-
131
-
132
- def process_arguments
133
- prompt_id = configatron.arguments.shift
134
-
135
- if prompt_id.include?('.')
136
- error "Invalid prompt_id: #{configatron.prompt_id}"
137
- else
138
- configatron.input_files = []
139
- configatron.arguments.each do |arg|
140
- file_path = Pathname.new(arg)
141
- if file_path.exist?
142
- configatron.input_files << file_path
143
- else
144
- error "File does not exist: #{file_path}"
145
- end
146
- end
147
- end
148
-
149
- prompt_id # may be invalid
150
- end
151
-
152
- configatron.prompt_id = process_arguments
153
-
154
- abort_if_errors
155
-
156
- ######################################################
157
- # Local methods
158
-
159
- def replace_keywords
160
- defaults = configatron.prompt.parameters
161
-
162
- configatron.prompt.keywords.each do |kw|
163
- defaults[kw] = keyword_value(kw, defaults[kw])
164
- end
165
-
166
- configatron.prompt.parameters = defaults
167
- configatron.prompt.save
168
- end
169
-
170
-
171
- def keyword_value(kw, default)
172
- label = "Default: "
173
- puts "#{kw} ..."
174
- print label
175
- puts default.wrap.split("\n").join("\n"+" "*label.length)
176
- a_string = Readline.readline("\n-=> ", false)
177
- puts
178
- a_string.empty? ? default : a_string
179
- end
180
-
181
-
182
- def log(prompt, answer)
183
- f = File.open(PROMPT_LOG, "ab")
184
-
185
- f.write <<~EOS
186
- =======================================
187
- == #{Time.now}
188
- == #{prompt.path}
189
-
190
- PROMPT: #{prompt}
191
-
192
- RESULT:
193
- #{answer}
194
-
195
- EOS
196
- end
197
-
198
-
199
- def search_for_a_matching_prompt
200
- found_prompt_ids = PromptManager::Prompt.search(
201
- configatron.prompt_id
202
- )
203
- if found_prompt_ids.size > 1
204
- puts <<~EOS
205
-
206
- Search Results
207
- ==============
208
-
209
- The following prompt IDs have the search term '#{configatron.prompt_id}'
210
- #{found_prompt_ids.join(', ').wrap}
211
-
212
- EOS
213
- exit
214
- else
215
- configatron.prompt_id = found_prompt_ids.first
216
- configatron.prompt = PromptManager::Prompt.get(id: configatron.prompt_id)
217
- end
218
- end
219
-
220
- ######################################################
221
- # Main
222
-
223
- at_exit do
224
- puts
225
- puts "Done."
226
- puts
227
- end
228
-
229
- ap configatron.to_h if debug?
230
-
231
-
232
- begin
233
- configatron.prompt = PromptManager::Prompt.get(id: configatron.prompt_id)
234
- rescue ArgumentError
235
- search_for_a_matching_prompt
236
- end
237
-
238
-
239
- puts
240
- puts "PROMPT:"
241
- puts configatron.prompt.text.wrap
242
- puts
243
-
244
- unless configatron.prompt.keywords.empty?
245
- replace_keywords
246
- configatron.prompt.build
247
- configatron.prompt.save
248
- end
249
-
250
- command = AI_COMMAND + configatron.prompt.to_s
251
-
252
- configatron.input_files.each do |input_file|
253
- command += " < #{input_file}"
254
- end
255
-
256
- print "\n\n" if verbose? && !keywords.empty?
257
-
258
- if verbose?
259
- puts "="*42
260
- puts command
261
- puts "="*42
262
- print "\n\n"
263
- end
264
-
265
- result = `#{command}`
266
-
267
- configatron.output.write result
268
-
269
- log configatron.prompt, result
270
-
271
-
272
- __END__
6
+ # Create an instance of the Main class and run the program
7
+ AIA::Main.new.call
@@ -2,7 +2,11 @@
2
2
  # Setup a prompt completion for use with
3
3
  # aia (a Ruby program) AI Assistant
4
4
  #
5
- # Requires the $PROMPTS_DIR envar be set
5
+
6
+ if [ -z "$PROMPTS_DIR" ]; then
7
+ echo "Error: PROMPTS_DIR environment variable is not set"
8
+ exit 1
9
+ fi
6
10
 
7
11
  # SMELL: Is this BASH-only or will it work with other shells?
8
12
 
data/lib/aia/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIA
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.4"
5
5
  end
data/lib/aia.rb CHANGED
@@ -1,8 +1,397 @@
1
- # frozen_string_literal: true
1
+ # lib/aia.rb
2
+
3
+ require 'amazing_print'
4
+ require 'pathname'
5
+ require 'readline'
6
+ require 'tempfile'
7
+
8
+
9
+ require 'debug_me'
10
+ include DebugMe
11
+
12
+ $DEBUG_ME = true # ARGV.include?("--debug") || ARGV.include?("-d")
13
+
14
+ require 'prompt_manager'
15
+ require 'prompt_manager/storage/file_system_adapter'
2
16
 
3
17
  require_relative "aia/version"
18
+ require_relative "core_ext/string_wrap"
4
19
 
5
20
  module AIA
6
- class Error < StandardError; end
7
- # Your code goes here...
21
+ class Main
22
+ HOME = Pathname.new(ENV['HOME'])
23
+ PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
24
+
25
+ AI_CLI_PROGRAM = "mods"
26
+ EDITOR = ENV['EDITOR'] || 'edit'
27
+ MY_NAME = Pathname.new(__FILE__).basename.to_s.split('.')[0]
28
+ MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
29
+ OUTPUT = Pathname.pwd + "temp.md"
30
+ PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
31
+
32
+
33
+ # TODO: write the usage text
34
+ USAGE = <<~EOUSAGE
35
+ AI Assistant (aia)
36
+ ==================
37
+
38
+ The AI cli program being used is: #{AI_CLI_PROGRAM}
39
+
40
+ You can pass additional CLI options to #{AI_CLI_PROGRAM} like this:
41
+ "#{MY_NAME} my options -- options for #{AI_CLI_PROGRAM}"
42
+ EOUSAGE
43
+
44
+
45
+ def initialize(args= ARGV)
46
+ @prompt = nil
47
+ @arguments = args
48
+ @options = {
49
+ edit?: false,
50
+ debug?: false,
51
+ verbose?: false,
52
+ version?: false,
53
+ help?: false,
54
+ fuzzy?: false,
55
+ markdown?: true,
56
+ output: OUTPUT,
57
+ log: PROMPT_LOG,
58
+ }
59
+ @extra_options = [] # intended for the backend AI processor
60
+
61
+ build_reader_methods # for the @options keys
62
+ process_arguments
63
+
64
+ PromptManager::Prompt.storage_adapter =
65
+ PromptManager::Storage::FileSystemAdapter.config do |config|
66
+ config.prompts_dir = PROMPTS_DIR
67
+ config.prompt_extension = '.txt'
68
+ config.params_extension = '.json'
69
+ config.search_proc = nil
70
+ # TODO: add the rgfzz script for search_proc
71
+ end.new
72
+
73
+ setup_cli_program
74
+ end
75
+
76
+
77
+ def build_reader_methods
78
+ @options.keys.each do |key|
79
+ define_singleton_method(key) do
80
+ @options[key]
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+ def process_arguments
87
+ @options.keys.each do |option|
88
+ check_for option
89
+ end
90
+
91
+ # get the options meant for the backend AI command
92
+ extract_extra_options
93
+
94
+ bad_options = @arguments.select{|a| a.start_with?('-')}
95
+
96
+ unless bad_options.empty?
97
+ puts <<~EOS
98
+
99
+ ERROR: Unknown options: #{bad_options.join(' ')}
100
+
101
+ EOS
102
+
103
+ show_usage
104
+
105
+ exit
106
+ end
107
+ end
108
+
109
+
110
+ def check_for(an_option)
111
+ switches = [
112
+ "--#{an_option}".gsub('?',''), # Dropping ? in case of a boolean
113
+ "--no-#{an_option}".gsub('?',''),
114
+ "-#{an_option.to_s[0]}" # SMELL: -v for both --verbose and --version
115
+ ]
116
+
117
+ process_option(an_option, switches)
118
+ end
119
+
120
+
121
+ def process_option(option_sym, switches)
122
+ boolean = option_sym.to_s.end_with?('?')
123
+
124
+ switches.each do |switch|
125
+ if @arguments.include?(switch)
126
+ index = @arguments.index(switch)
127
+
128
+ if boolean
129
+ @options[option_sym] = switch.include?('-no-') ? false : true
130
+ @arguments.slice!(index,1)
131
+ else
132
+ if switch.include?('-no-')
133
+ @option[option_sym] = nil
134
+ @arguments.slice!(index,1)
135
+ else
136
+ @option[option_sym] = @arguments[index + 1]
137
+ @arguments.slice!(index,2)
138
+ end
139
+ end
140
+
141
+ break
142
+ end
143
+ end
144
+ end
145
+
146
+
147
+ def show_usage
148
+ puts USAGE
149
+ exit
150
+ end
151
+
152
+
153
+ def show_version
154
+ puts VERSION
155
+ exit
156
+ end
157
+
158
+
159
+ def call
160
+ show_usage if help?
161
+ show_version if version?
162
+
163
+ prompt_id = get_prompt_id
164
+
165
+ search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
166
+ process_prompt
167
+ execute_and_log_command(build_command)
168
+ end
169
+
170
+
171
+ ####################################################
172
+ private
173
+
174
+ # Setup the AI CLI program with necessary variables
175
+ def setup_cli_program
176
+
177
+ ai_default_opts = "-m #{MODS_MODEL} --no-limit "
178
+ ai_default_opts += "-f " if markdown?
179
+ @ai_options = ai_default_opts.dup
180
+
181
+
182
+ @ai_options += @extra_options.join(' ')
183
+
184
+ @ai_command = "#{AI_CLI_PROGRAM} #{@ai_options} "
185
+ end
186
+
187
+
188
+ # Get the additional CLI arguments intended for the
189
+ # backend gen-AI processor.
190
+ def extract_extra_options
191
+ extra_index = @arguments.index('--')
192
+ if extra_index.nil?
193
+ @extra_options = []
194
+ else
195
+ @extra_options = @arguments.slice!(extra_index..-1)[1..]
196
+ end
197
+ end
198
+
199
+
200
+ # Fetch the first argument which should be the prompt id
201
+ def get_prompt_id
202
+ prompt_id = @arguments.shift
203
+
204
+ # TODO: or maybe go to a search and select process
205
+
206
+ abort("Please provide a prompt id") unless prompt_id
207
+ prompt_id
208
+ end
209
+
210
+
211
+ # Check if a prompt with the given id already exists
212
+ def existing_prompt?(prompt_id)
213
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
214
+ true
215
+ rescue ArgumentError
216
+ false
217
+ end
218
+
219
+
220
+ # Process the prompt's associated keywords and parameters
221
+ def process_prompt
222
+ unless @prompt.keywords.empty?
223
+ replace_keywords
224
+ @prompt.build
225
+ @prompt.save
226
+ end
227
+ end
228
+
229
+
230
+ def replace_keywords
231
+ print "\nQuit #{MY_NAME} with a CNTL-D or a CNTL-C\n\n"
232
+
233
+ defaults = @prompt.parameters
234
+
235
+ @prompt.keywords.each do |kw|
236
+ defaults[kw] = keyword_value(kw, defaults[kw])
237
+ end
238
+
239
+ @prompt.parameters = defaults
240
+ end
241
+
242
+
243
+ # query the user for a value to the keyword allow the
244
+ # reuse of the previous value shown as the default
245
+ def keyword_value(kw, default)
246
+ label = "Default: "
247
+ puts "Parameter #{kw} ..."
248
+ default_wrapped = default.wrap(indent: label.size)
249
+ default_wrapped[0..label.size] = label
250
+ puts default_wrapped
251
+
252
+ begin
253
+ a_string = Readline.readline("\n-=> ", false)
254
+ rescue Interrupt
255
+ a_string = nil
256
+ end
257
+
258
+ if a_string.nil?
259
+ puts "okay. Come back soon."
260
+ exit
261
+ end
262
+
263
+
264
+ puts
265
+ a_string.empty? ? default : a_string
266
+ end
267
+
268
+
269
+ # Search for a prompt with a matching id or keyword
270
+ def search_for_a_matching_prompt(prompt_id)
271
+ # TODO: using the rgfzf version of the search_proc should only
272
+ # return a single prompt_id
273
+ found_prompts = PromptManager::Prompt.search(prompt_id)
274
+ prompt_id = found_prompts.size == 1 ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
275
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
276
+ end
277
+
278
+
279
+ def handle_multiple_prompts(found_these, while_looking_for_this)
280
+ raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
281
+
282
+ # TODO: Make this a class constant for defaults; make the header content
283
+ # a parameter so it can be varied.
284
+ fzf_options = [
285
+ "--tabstop=2", # 2 soaces for a tab
286
+ "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
287
+ "--header-first",
288
+ "--prompt='Search term: '",
289
+ '--delimiter :',
290
+ "--preview 'cat $PROMPTS_DIR/{1}.txt'",
291
+ "--preview-window=down:50%:wrap"
292
+ ].join(' ')
293
+
294
+
295
+ # Create a temporary file to hold the list of strings
296
+ temp_file = Tempfile.new('fzf-input')
297
+
298
+ begin
299
+ # Write all strings to the temp file
300
+ temp_file.puts(found_these)
301
+ temp_file.close
302
+
303
+ # Execute fzf command-line utility to allow selection
304
+ selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
305
+
306
+ # Check if fzf actually returned a string; if not, return nil
307
+ result = selected.empty? ? nil : selected
308
+ ensure
309
+ # Ensure that the tempfile is closed and unlinked
310
+ temp_file.unlink
311
+ end
312
+
313
+ exit unless result
314
+
315
+ result
316
+ end
317
+
318
+
319
+ # Build the command to interact with the AI CLI program
320
+ def build_command
321
+ command = @ai_command + %Q["#{@prompt.to_s}"]
322
+
323
+ @arguments.each do |input_file|
324
+ file_path = Pathname.new(input_file)
325
+ abort("File does not exist: #{input_file}") unless file_path.exist?
326
+ command += " < #{input_file}"
327
+ end
328
+
329
+ command
330
+ end
331
+
332
+
333
+ # Execute the command and log the results
334
+ def execute_and_log_command(command)
335
+ puts command if verbose?
336
+ result = `#{command}`
337
+ output.write result
338
+
339
+ write_to_log(result) unless log.nil?
340
+ end
341
+
342
+
343
+ def write_to_log(answer)
344
+ f = File.open(log, "ab")
345
+
346
+ f.write <<~EOS
347
+ =======================================
348
+ == #{Time.now}
349
+ == #{@prompt.path}
350
+
351
+ PROMPT:
352
+ #{@prompt}
353
+
354
+ RESULT:
355
+ #{answer}
356
+
357
+ EOS
358
+ end
359
+ end
360
+ end
361
+
362
+
363
+ # Create an instance of the Main class and run the program
364
+ AIA::Main.new.call if $PROGRAM_NAME == __FILE__
365
+
366
+
367
+ __END__
368
+
369
+
370
+ # TODO: Consider using this history process to preload the default
371
+ # so that an up arrow will bring the previous answer into
372
+ # the read buffer for line editing.
373
+ # Instead of usin the .history file just push the default
374
+ # value from the JSON file.
375
+
376
+ while input = Readline.readline('> ', true)
377
+ # Skip empty entries and duplicates
378
+ if input.empty? || Readline::HISTORY.to_a[-2] == input
379
+ Readline::HISTORY.pop
380
+ end
381
+ break if input == 'exit'
382
+
383
+ # Do something with the input
384
+ puts "You entered: #{input}"
385
+
386
+ # Save the history in case you want to preserve it for the next sessions
387
+ File.open('.history', 'a') { |f| f.puts(input) }
388
+ end
389
+
390
+ # Load history from file at the beginning of the program
391
+ if File.exist?('.history')
392
+ File.readlines('.history').each do |line|
393
+ Readline::HISTORY.push(line.chomp)
394
+ end
8
395
  end
396
+
397
+
@@ -0,0 +1,73 @@
1
+ # lib/string_wrap.rb
2
+
3
+ require 'io/console'
4
+
5
+ # This is a monkey patch to the String class which is
6
+ # okay in this context since this program is a
7
+ # stand-alone terminal utility. Otherwise we would
8
+ # use a refinement or a namespace to keep this from
9
+ # impact other code.
10
+
11
+ class String
12
+ def wrap(line_width: nil, indent: 0)
13
+ # If line_width is not given, try to detect the terminal width
14
+ line_width ||= IO.console ? IO.console.winsize[1] : 80
15
+
16
+ # Prepare the prefix based on the type of the indent parameter
17
+ prefix = indent.is_a?(String) ? indent : ' ' * indent.to_i
18
+
19
+ # Split the string into paragraphs first, preserve paragraph breaks
20
+ paragraphs = self.split(/\n{2,}/)
21
+
22
+ # Create an empty array that will hold all wrapped paragraphs
23
+ wrapped_paragraphs = []
24
+
25
+ # Process each paragraph separately
26
+ paragraphs.each do |paragraph|
27
+ wrapped_lines = [] # Create an empty array for wrapped lines of the current paragraph
28
+
29
+ # Split the paragraph into lines first, in case there are single newlines
30
+ lines = paragraph.split(/(?<=\n)/)
31
+
32
+ # Process each line separately to maintain single newlines
33
+ lines.each do |line|
34
+ words = line.split
35
+ current_line = ""
36
+
37
+ words.each do |word|
38
+ if word.include?("\n") && !word.strip.empty?
39
+ # If the word contains a newline, split and process as separate lines
40
+ parts = word.split(/(?<=\n)/)
41
+
42
+ parts.each_with_index do |part, index|
43
+ if part == "\n"
44
+ wrapped_lines << prefix + current_line
45
+ current_line = ""
46
+ else
47
+ current_line << " " unless current_line.empty? or index == 0
48
+ current_line << part.strip
49
+ end
50
+ end
51
+
52
+ elsif current_line.length + word.length + 1 > line_width - prefix.length
53
+ wrapped_lines << prefix + current_line.rstrip
54
+ current_line = word
55
+
56
+ else
57
+ current_line << " " unless current_line.empty?
58
+ current_line << word
59
+ end
60
+ end
61
+
62
+ # Don't forget to add the last line unless it's empty
63
+ wrapped_lines << prefix + current_line unless current_line.empty?
64
+ end
65
+
66
+ # Preserve the paragraph structure by joining the wrapped lines and append to the wrapped_paragraphs array
67
+ wrapped_paragraphs << wrapped_lines.join("\n")
68
+ end
69
+
70
+ # Join wrapped paragraphs with double newlines into a single string
71
+ wrapped_paragraphs.join("\n\n")
72
+ end
73
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -10,20 +10,6 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 2023-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: cli_helper
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: prompt_manager
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -38,20 +24,6 @@ dependencies:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: word_wrap
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
27
  - !ruby/object:Gem::Dependency
56
28
  name: amazing_print
57
29
  requirement: !ruby/object:Gem::Requirement
@@ -109,10 +81,11 @@ dependencies:
109
81
  - !ruby/object:Gem::Version
110
82
  version: '0'
111
83
  description: "A command-line AI Assistante (aia) that provides\nparameterized prompt
112
- management (via the prompt_manager gem) to\nvarious backend gen-AI processes. Currently
113
- supports the `mods`\nCLI tool. `aia`uses `ripgrep` and `fzf` command-line utilities
114
- \nto search for and select prompt files to send to the backend gen-AI\ntool along
115
- with supported context files.\n"
84
+ management (via the prompt_manager gem) to\nvarious backend gen-AI processes. aia
85
+ currently supports the \"mods\"\nCLI tool. aia uses \"ripgrep\" and \"fzf\" CLI
86
+ utilities \nto search for and select prompt files to send to the backend gen-AI\ntool
87
+ along with supported context files. Example usage: \"aia refactor my_class.rb\"
88
+ \nwhere \"refactor\" is the prompt ID for the file \"refactor.txt\" from your\nRPROMPTS_DIR\n"
116
89
  email:
117
90
  - dvanhoozer@gmail.com
118
91
  executables:
@@ -131,6 +104,7 @@ files:
131
104
  - bin/aia_completion.sh
132
105
  - lib/aia.rb
133
106
  - lib/aia/version.rb
107
+ - lib/core_ext/string_wrap.rb
134
108
  homepage: https://github.com/MadBomber/aia
135
109
  licenses:
136
110
  - MIT
@@ -157,5 +131,5 @@ requirements: []
157
131
  rubygems_version: 3.4.22
158
132
  signing_key:
159
133
  specification_version: 4
160
- summary: AI Assistant (aia) a command-ling utility
134
+ summary: AI Assistant (aia) a command-line (CLI) utility
161
135
  test_files: []