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