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 +4 -4
- data/Rakefile +1 -1
- data/bin/aia +4 -269
- data/bin/aia_completion.sh +5 -1
- data/lib/aia/version.rb +1 -1
- data/lib/aia.rb +392 -3
- data/lib/core_ext/string_wrap.rb +73 -0
- metadata +8 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5737dc3f15dc0372865893efba9c8e7bb6c4c41fc335476b638cb55a5dc4ad17
|
4
|
+
data.tar.gz: 91545c6a73d8d6f61cbf878f35df375f3e700961d1d70b25bd3cb0aa0cc25251
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a276b2fdad405245fd29f8aba586504ad2a99cc4c8f3f570af1de58849c1ee2abda2f30c5a72fbd699db37f01b94e79711eb4833495cebeae33c9562d03033f
|
7
|
+
data.tar.gz: 2c570b4339d3f24b6621378481393994272a0f0317ab4edb2ae9ca79007a81b288cc09eeae0f9326f72892ce742d41e9bbfe180de3a8bb6f652a1fc808d1e62f
|
data/Rakefile
CHANGED
data/bin/aia
CHANGED
@@ -1,272 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
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 '
|
24
|
-
HOME = Pathname.new( ENV['HOME'] )
|
25
|
-
PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
|
4
|
+
require 'aia'
|
26
5
|
|
27
|
-
|
28
|
-
|
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
|
data/bin/aia_completion.sh
CHANGED
@@ -2,7 +2,11 @@
|
|
2
2
|
# Setup a prompt completion for use with
|
3
3
|
# aia (a Ruby program) AI Assistant
|
4
4
|
#
|
5
|
-
|
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
data/lib/aia.rb
CHANGED
@@ -1,8 +1,397 @@
|
|
1
|
-
#
|
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
|
7
|
-
|
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.
|
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.
|
113
|
-
supports the
|
114
|
-
\nto search for and select prompt files to send to the backend gen-AI\ntool
|
115
|
-
with supported context files
|
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-
|
134
|
+
summary: AI Assistant (aia) a command-line (CLI) utility
|
161
135
|
test_files: []
|