aia 0.0.2 → 0.0.4

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 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: []