aia 0.5.17 → 0.8.0
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/.envrc +1 -0
- data/.version +1 -2
- data/CHANGELOG.md +61 -22
- data/README.md +387 -227
- data/Rakefile +16 -5
- data/_notes.txt +231 -0
- data/bin/aia +3 -2
- data/examples/README.md +140 -0
- data/examples/headlines +21 -0
- data/justfile +16 -3
- data/lib/aia/ai_client_adapter.rb +210 -0
- data/lib/aia/chat_processor_service.rb +120 -0
- data/lib/aia/config.rb +473 -4
- data/lib/aia/context_manager.rb +58 -0
- data/lib/aia/directive_processor.rb +267 -0
- data/lib/aia/{tools/fzf.rb → fzf.rb} +9 -17
- data/lib/aia/history_manager.rb +85 -0
- data/lib/aia/prompt_handler.rb +178 -0
- data/lib/aia/session.rb +215 -0
- data/lib/aia/shell_command_executor.rb +109 -0
- data/lib/aia/ui_presenter.rb +110 -0
- data/lib/aia/utility.rb +24 -0
- data/lib/aia/version.rb +9 -6
- data/lib/aia.rb +57 -61
- data/lib/extensions/openstruct_merge.rb +44 -0
- metadata +43 -42
- data/LICENSE.txt +0 -21
- data/doc/aia_and_pre_compositional_prompts.md +0 -474
- data/lib/aia/clause.rb +0 -7
- data/lib/aia/cli.rb +0 -452
- data/lib/aia/directives.rb +0 -142
- data/lib/aia/dynamic_content.rb +0 -26
- data/lib/aia/logging.rb +0 -62
- data/lib/aia/main.rb +0 -265
- data/lib/aia/prompt.rb +0 -275
- data/lib/aia/tools/backend_common.rb +0 -58
- data/lib/aia/tools/client.rb +0 -197
- data/lib/aia/tools/editor.rb +0 -52
- data/lib/aia/tools/glow.rb +0 -90
- data/lib/aia/tools/llm.rb +0 -77
- data/lib/aia/tools/mods.rb +0 -100
- data/lib/aia/tools/sgpt.rb +0 -79
- data/lib/aia/tools/subl.rb +0 -68
- data/lib/aia/tools/vim.rb +0 -93
- data/lib/aia/tools.rb +0 -88
- data/lib/aia/user_query.rb +0 -21
- data/lib/core_ext/string_wrap.rb +0 -73
- data/lib/core_ext/tty-spinner_log.rb +0 -25
- data/man/aia.1 +0 -272
- data/man/aia.1.md +0 -236
data/lib/aia/session.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# lib/aia/session.rb
|
2
|
+
|
3
|
+
require 'tty-spinner'
|
4
|
+
require 'tty-screen'
|
5
|
+
require 'reline'
|
6
|
+
require 'prompt_manager'
|
7
|
+
require 'json'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'amazing_print'
|
10
|
+
require_relative 'directive_processor'
|
11
|
+
require_relative 'history_manager'
|
12
|
+
require_relative 'context_manager'
|
13
|
+
require_relative 'ui_presenter'
|
14
|
+
require_relative 'chat_processor_service'
|
15
|
+
require_relative 'prompt_handler'
|
16
|
+
require_relative 'utility'
|
17
|
+
|
18
|
+
module AIA
|
19
|
+
class Session
|
20
|
+
KW_HISTORY_MAX = 5 # Maximum number of history entries per keyword
|
21
|
+
TERSE_PROMPT = "\nKeep your response short and to the point.\n"
|
22
|
+
|
23
|
+
def initialize(prompt_handler)
|
24
|
+
@prompt_handler = prompt_handler
|
25
|
+
|
26
|
+
if AIA.chat? && AIA.config.prompt_id.empty?
|
27
|
+
prompt_instance = nil
|
28
|
+
@history_manager = nil
|
29
|
+
else
|
30
|
+
prompt_instance = @prompt_handler.get_prompt(AIA.config.prompt_id)
|
31
|
+
@history_manager = HistoryManager.new(prompt: prompt_instance)
|
32
|
+
end
|
33
|
+
|
34
|
+
@context_manager = ContextManager.new(system_prompt: AIA.config.system_prompt) # Add this line
|
35
|
+
@ui_presenter = UIPresenter.new
|
36
|
+
@directive_processor = DirectiveProcessor.new
|
37
|
+
@chat_processor = ChatProcessorService.new(@ui_presenter, @directive_processor)
|
38
|
+
|
39
|
+
if AIA.config.out_file && !AIA.append? && File.exist?(AIA.config.out_file)
|
40
|
+
File.open(AIA.config.out_file, 'w') {} # Truncate the file
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Starts the session, processing the initial prompt and handling user
|
45
|
+
# interactions. It manages the flow of prompts, context, and responses.
|
46
|
+
def start
|
47
|
+
prompt_id = AIA.config.prompt_id
|
48
|
+
role_id = AIA.config.role
|
49
|
+
|
50
|
+
# Handle chat mode *only* if NO initial prompt is given
|
51
|
+
if AIA.chat?
|
52
|
+
AIA::Utility.robot
|
53
|
+
if prompt_id.empty? && role_id.empty?
|
54
|
+
start_chat
|
55
|
+
return
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# --- Get and process the initial prompt ---
|
61
|
+
begin
|
62
|
+
prompt = @prompt_handler.get_prompt(prompt_id, role_id)
|
63
|
+
rescue StandardError => e
|
64
|
+
puts "Error: #{e.message}"
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
# Collect variable values if needed
|
69
|
+
variables = prompt.parameters.keys
|
70
|
+
|
71
|
+
if variables && !variables.empty?
|
72
|
+
variable_values = {}
|
73
|
+
history_manager = AIA::HistoryManager.new prompt: prompt
|
74
|
+
|
75
|
+
variables.each do |var_name|
|
76
|
+
# History is based on the prompt ID and the variable name (without brackets)
|
77
|
+
history = prompt.parameters[var_name]
|
78
|
+
|
79
|
+
# Ask the user for the variable
|
80
|
+
value = history_manager.request_variable_value(
|
81
|
+
variable_name: var_name,
|
82
|
+
history_values: history
|
83
|
+
)
|
84
|
+
# Store the value using the original BRACKETED key from prompt.parameters
|
85
|
+
if history.include? value
|
86
|
+
history.delete(value)
|
87
|
+
end
|
88
|
+
history << value
|
89
|
+
if history.size > HistoryManager::MAX_VARIABLE_HISTORY
|
90
|
+
history.shift
|
91
|
+
end
|
92
|
+
variable_values[var_name] = history
|
93
|
+
end
|
94
|
+
|
95
|
+
# Assign collected values back for prompt_manager substitution
|
96
|
+
prompt.parameters = variable_values
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add terse instruction if needed
|
100
|
+
if AIA.terse?
|
101
|
+
prompt.text << TERSE_PROMPT
|
102
|
+
end
|
103
|
+
|
104
|
+
prompt.save
|
105
|
+
|
106
|
+
# Substitute variables and get final prompt text
|
107
|
+
prompt_text = prompt.to_s
|
108
|
+
|
109
|
+
# Add context files if any
|
110
|
+
if AIA.config.context_files && !AIA.config.context_files.empty?
|
111
|
+
context = AIA.config.context_files.map do |file|
|
112
|
+
File.read(file) rescue "Error reading file: #{file}"
|
113
|
+
end.join("\n\n")
|
114
|
+
prompt_text = "#{prompt_text}\n\nContext:\n#{context}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# Determine operation type
|
118
|
+
operation_type = @chat_processor.determine_operation_type(AIA.config.model)
|
119
|
+
|
120
|
+
# Add initial user prompt to context *before* sending to AI
|
121
|
+
@context_manager.add_to_context(role: 'user', content: prompt_text)
|
122
|
+
|
123
|
+
# Process the initial prompt
|
124
|
+
@ui_presenter.display_thinking_animation
|
125
|
+
# Send the current context (which includes the user prompt)
|
126
|
+
response = @chat_processor.process_prompt(@context_manager.get_context, operation_type)
|
127
|
+
|
128
|
+
# Add AI response to context
|
129
|
+
@context_manager.add_to_context(role: 'assistant', content: response)
|
130
|
+
|
131
|
+
# Output the response
|
132
|
+
@chat_processor.output_response(response) # Handles display
|
133
|
+
|
134
|
+
# Process next prompts/pipeline (if any)
|
135
|
+
@chat_processor.process_next_prompts(response, @prompt_handler)
|
136
|
+
|
137
|
+
# --- Enter chat mode AFTER processing initial prompt ---
|
138
|
+
if AIA.chat?
|
139
|
+
@ui_presenter.display_separator # Add separator
|
140
|
+
start_chat # start_chat will use the now populated context
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Starts the interactive chat session.
|
145
|
+
def start_chat
|
146
|
+
# Consider if display_chat_header is needed if robot+separator already shown
|
147
|
+
# For now, let's keep it, maybe add an indicator message
|
148
|
+
puts "\nEntering interactive chat mode..."
|
149
|
+
@ui_presenter.display_chat_header
|
150
|
+
|
151
|
+
Reline::HISTORY.clear # Keep Reline history for user input editing, separate from chat context
|
152
|
+
|
153
|
+
loop do
|
154
|
+
# Get user input
|
155
|
+
prompt = @ui_presenter.ask_question
|
156
|
+
|
157
|
+
|
158
|
+
break if prompt.nil? || prompt.strip.downcase == 'exit' || prompt.strip.empty?
|
159
|
+
|
160
|
+
if AIA.config.out_file
|
161
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
162
|
+
file.puts "\nYou: #{prompt}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
if @directive_processor.directive?(prompt)
|
167
|
+
directive_output = @directive_processor.process(prompt, @context_manager) # Pass context_manager
|
168
|
+
|
169
|
+
# Add check for specific directives like //clear that might modify context
|
170
|
+
if prompt.strip.start_with?('//clear', '#!clear:')
|
171
|
+
# Context is likely cleared within directive_processor.process now
|
172
|
+
# or add @context_manager.clear_context here if not handled internally
|
173
|
+
@ui_presenter.display_info("Chat context cleared.")
|
174
|
+
next # Skip API call after clearing
|
175
|
+
elsif directive_output.nil? || directive_output.strip.empty?
|
176
|
+
next # Skip API call if directive produced no output and wasn't //clear
|
177
|
+
else
|
178
|
+
puts "\n#{directive_output}\n"
|
179
|
+
# Optionally add directive output to context or handle as needed
|
180
|
+
# Example: Add a summary to context
|
181
|
+
# @context_manager.add_to_context(role: 'assistant', content: "Directive executed. Output:\n#{directive_output}")
|
182
|
+
# For now, just use a placeholder prompt modification:
|
183
|
+
prompt = "I executed this directive: #{prompt}\nHere's the output: #{directive_output}\nLet's continue our conversation."
|
184
|
+
# Fall through to add this modified prompt to context and send to AI
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Use ContextManager instead of HistoryManager
|
189
|
+
@context_manager.add_to_context(role: 'user', content: prompt)
|
190
|
+
|
191
|
+
# Use ContextManager to get the conversation
|
192
|
+
conversation = @context_manager.get_context # System prompt handled internally
|
193
|
+
|
194
|
+
# FIXME: remove this comment once verified
|
195
|
+
# is conversation the same thing as the context for a chat session? YES
|
196
|
+
# if so need to somehow delete it when the //clear directive is entered. - Addressed above/in DirectiveProcessor
|
197
|
+
|
198
|
+
operation_type = @chat_processor.determine_operation_type(AIA.config.model)
|
199
|
+
@ui_presenter.display_thinking_animation
|
200
|
+
response = @chat_processor.process_prompt(conversation, operation_type)
|
201
|
+
|
202
|
+
@ui_presenter.display_ai_response(response)
|
203
|
+
|
204
|
+
# Use ContextManager instead of HistoryManager
|
205
|
+
@context_manager.add_to_context(role: 'assistant', content: response)
|
206
|
+
|
207
|
+
@chat_processor.speak(response)
|
208
|
+
|
209
|
+
@ui_presenter.display_separator
|
210
|
+
end
|
211
|
+
|
212
|
+
@ui_presenter.display_chat_end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# lib/aia/shell_command_executor.rb
|
2
|
+
|
3
|
+
module AIA
|
4
|
+
class ShellCommandExecutor
|
5
|
+
DANGEROUS_PATTERNS = [
|
6
|
+
# File system destructive commands
|
7
|
+
/\brm\s+(-[a-z]*)?f/i, # rm with force flag
|
8
|
+
/\bmkfs/i, # format filesystems
|
9
|
+
/\bdd\b.*\bof=/i, # dd with output file
|
10
|
+
/\bshred\b/i, # securely delete files
|
11
|
+
# System modification commands
|
12
|
+
/\bsystemctl\s+(stop|disable|mask)/i, # stopping system services
|
13
|
+
/\bchmod\s+777\b/i, # setting dangerous permissions
|
14
|
+
/\b(halt|poweroff|shutdown|reboot)\b/i, # system power commands
|
15
|
+
# Network security related
|
16
|
+
/\btcpdump\b/i, # packet capturing
|
17
|
+
/\bifconfig\b.*\bdown\b/i, # taking down network interfaces
|
18
|
+
# Process control
|
19
|
+
/\bkill\s+-9\b/i, # force killing processes
|
20
|
+
/\bpkill\b/i # pattern-based process killing
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
|
24
|
+
MAX_COMMAND_LENGTH = 500
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
# Stub method for future implementation
|
30
|
+
end
|
31
|
+
|
32
|
+
# Class-level
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def self.execute_command(command)
|
39
|
+
new.execute_command(command)
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
def execute_command(command)
|
45
|
+
return "No command specified" if blank?(command)
|
46
|
+
|
47
|
+
validation_result = validate_command(command)
|
48
|
+
return validation_result if validation_result
|
49
|
+
|
50
|
+
`#{command}`.chomp
|
51
|
+
rescue StandardError => error
|
52
|
+
"Error executing shell command: #{error.message}"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
def dangerous_command?(command)
|
58
|
+
return false if blank?(command)
|
59
|
+
DANGEROUS_PATTERNS.any? { |pattern| command =~ pattern }
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
def blank?(str)
|
67
|
+
str.nil? || str.strip.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
def validate_command(command)
|
73
|
+
command_length = command.length
|
74
|
+
|
75
|
+
if command_length > MAX_COMMAND_LENGTH
|
76
|
+
return "Error: Command too long (#{command_length} chars). Maximum length is #{MAX_COMMAND_LENGTH}."
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
is_dangerous = dangerous_command?(command)
|
81
|
+
|
82
|
+
|
83
|
+
if AIA.strict_shell_safety? && is_dangerous
|
84
|
+
return "Error: Potentially dangerous command blocked for security reasons: '#{command}'"
|
85
|
+
end
|
86
|
+
|
87
|
+
if AIA.shell_confirm? && is_dangerous
|
88
|
+
return prompt_confirmation(command)
|
89
|
+
end
|
90
|
+
|
91
|
+
nil # Command is valid
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
def prompt_confirmation(command)
|
101
|
+
puts "\n⚠️ WARNING: Potentially dangerous shell command detected:"
|
102
|
+
puts "\n #{command}\n"
|
103
|
+
print "\nDo you want to execute this command? [y/N]: "
|
104
|
+
confirm = STDIN.gets.chomp.downcase
|
105
|
+
return "Command execution canceled by user" unless confirm == 'y'
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# lib/aia/ui_presenter.rb
|
2
|
+
|
3
|
+
require 'tty-screen'
|
4
|
+
require 'reline'
|
5
|
+
|
6
|
+
module AIA
|
7
|
+
class UIPresenter
|
8
|
+
USER_PROMPT = "Follow up (cntl-D or 'exit' to end) #=> "
|
9
|
+
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@terminal_width = TTY::Screen.width
|
13
|
+
end
|
14
|
+
|
15
|
+
def display_chat_header
|
16
|
+
puts "#{'═' * @terminal_width}\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def display_thinking_animation
|
21
|
+
puts "\n⏳ Processing...\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def display_ai_response(response)
|
26
|
+
puts "\nAI: "
|
27
|
+
format_chat_response(response)
|
28
|
+
|
29
|
+
if AIA.config.out_file
|
30
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
31
|
+
file.puts "\nAI: "
|
32
|
+
format_chat_response(response, file)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def format_chat_response(response, output = $stdout)
|
39
|
+
indent = ' '
|
40
|
+
|
41
|
+
in_code_block = false
|
42
|
+
language = ''
|
43
|
+
|
44
|
+
response.each_line do |line|
|
45
|
+
line = line.chomp
|
46
|
+
|
47
|
+
# Check for code block delimiters
|
48
|
+
if line.match?(/^```(\w*)$/) && !in_code_block
|
49
|
+
in_code_block = true
|
50
|
+
language = $1
|
51
|
+
output.puts "#{indent}```#{language}"
|
52
|
+
elsif line.match?(/^```$/) && in_code_block
|
53
|
+
in_code_block = false
|
54
|
+
output.puts "#{indent}```"
|
55
|
+
elsif in_code_block
|
56
|
+
# Print code with special formatting
|
57
|
+
output.puts "#{indent}#{line}"
|
58
|
+
else
|
59
|
+
# Handle regular text
|
60
|
+
output.puts "#{indent}#{line}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def display_separator
|
67
|
+
puts "\n#{'─' * @terminal_width}"
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def display_chat_end
|
72
|
+
puts "\nChat session ended."
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# This is the follow up question in a chat session
|
77
|
+
def ask_question
|
78
|
+
puts USER_PROMPT
|
79
|
+
$stdout.flush # Ensure the prompt is displayed immediately
|
80
|
+
begin
|
81
|
+
input = Reline.readline('', true)
|
82
|
+
return nil if input.nil? # Handle Ctrl+D
|
83
|
+
Reline::HISTORY << input unless input.strip.empty?
|
84
|
+
input
|
85
|
+
rescue Interrupt
|
86
|
+
puts "\nChat session interrupted."
|
87
|
+
return 'exit'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def display_info(message)
|
92
|
+
puts "\n#{message}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def with_spinner(message = "Processing", operation_type = nil)
|
96
|
+
if AIA.verbose?
|
97
|
+
spinner_message = operation_type ? "#{message} #{operation_type}..." : "#{message}..."
|
98
|
+
spinner = TTY::Spinner.new("[:spinner] #{spinner_message}", format: :bouncing_ball)
|
99
|
+
spinner.auto_spin
|
100
|
+
|
101
|
+
result = yield
|
102
|
+
|
103
|
+
spinner.stop
|
104
|
+
result
|
105
|
+
else
|
106
|
+
yield
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/aia/utility.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# lib/aia/utility.rb
|
2
|
+
|
3
|
+
module AIA
|
4
|
+
class Utility
|
5
|
+
class << self
|
6
|
+
# Displays the AIA robot ASCII art
|
7
|
+
def robot
|
8
|
+
puts <<-ROBOT
|
9
|
+
|
10
|
+
, ,
|
11
|
+
(\\____/) AI Assistant
|
12
|
+
(_oo_) #{AIA.config.model}
|
13
|
+
(O) is Online
|
14
|
+
__||__ \\)
|
15
|
+
[/______\\] /
|
16
|
+
/ \\__AI__/ \\/
|
17
|
+
/ /__\\
|
18
|
+
(\\ /____\\
|
19
|
+
|
20
|
+
ROBOT
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/aia/version.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# lib/aia/version.rb
|
2
|
-
#
|
3
|
-
|
4
|
-
|
2
|
+
#
|
3
|
+
# This file defines the version of the AIA application.
|
4
|
+
# The VERSION constant defines the current version of the AIA application,
|
5
|
+
# which is read from the .version file in the project root.
|
5
6
|
|
7
|
+
# The AIA module serves as the namespace for the AIA application, which
|
8
|
+
# provides an interface for interacting with AI models and managing prompts.
|
6
9
|
module AIA
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
+
# The VERSION constant defines the current version of the AIA application,
|
11
|
+
# which is read from the .version file in the project root.
|
12
|
+
VERSION = File.read(File.join(File.dirname(__FILE__), '..', '..', '.version')).strip
|
10
13
|
end
|
data/lib/aia.rb
CHANGED
@@ -1,78 +1,74 @@
|
|
1
1
|
# lib/aia.rb
|
2
|
+
#
|
3
|
+
# This is the main entry point for the AIA application.
|
4
|
+
# The AIA module serves as the namespace for the AIA application, which
|
5
|
+
# provides an interface for interacting with AI models and managing prompts.
|
2
6
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
require 'ai_client'
|
8
|
+
require 'prompt_manager'
|
9
|
+
require 'debug_me'
|
10
|
+
include DebugMe
|
11
|
+
$DEBUG_ME = false
|
12
|
+
DebugMeDefaultOptions[:skip1] = true
|
13
|
+
|
14
|
+
require_relative 'extensions/openstruct_merge'
|
15
|
+
require_relative 'aia/utility'
|
16
|
+
require_relative 'aia/version'
|
17
|
+
require_relative 'aia/config'
|
18
|
+
require_relative 'aia/shell_command_executor'
|
19
|
+
require_relative 'aia/prompt_handler'
|
20
|
+
require_relative 'aia/ai_client_adapter'
|
21
|
+
require_relative 'aia/directive_processor'
|
22
|
+
require_relative 'aia/history_manager'
|
23
|
+
require_relative 'aia/ui_presenter'
|
24
|
+
require_relative 'aia/chat_processor_service'
|
25
|
+
require_relative 'aia/session'
|
26
|
+
|
27
|
+
# The AIA module serves as the namespace for the AIA application, which
|
28
|
+
# provides an interface for interacting with AI models and managing prompts.
|
29
|
+
module AIA
|
30
|
+
at_exit do
|
31
|
+
STDERR.puts "Exiting AIA application..."
|
11
32
|
end
|
12
33
|
|
13
|
-
|
14
|
-
|
15
|
-
require_result
|
16
|
-
end
|
17
|
-
|
18
|
-
tramp_require('debug_me') {
|
19
|
-
include DebugMe
|
20
|
-
}
|
21
|
-
|
22
|
-
require 'hashie'
|
23
|
-
require 'openai'
|
24
|
-
require 'os'
|
25
|
-
require 'pathname'
|
26
|
-
require 'reline'
|
27
|
-
require 'shellwords'
|
28
|
-
require 'tempfile'
|
29
|
-
|
30
|
-
require 'tty-spinner'
|
31
|
-
|
32
|
-
unless TTY::Spinner.new.respond_to?(:log)
|
33
|
-
# Allows messages to be sent to the console while
|
34
|
-
# the spinner is still spinning.
|
35
|
-
require_relative './core_ext/tty-spinner_log'
|
36
|
-
end
|
37
|
-
|
38
|
-
require 'prompt_manager'
|
39
|
-
require 'prompt_manager/storage/file_system_adapter'
|
34
|
+
@config = nil
|
40
35
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
require_relative "core_ext/string_wrap"
|
36
|
+
def self.config
|
37
|
+
@config
|
38
|
+
end
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
attr_accessor :client
|
40
|
+
def self.client
|
41
|
+
@config.client
|
42
|
+
end
|
50
43
|
|
51
|
-
|
52
|
-
|
44
|
+
def self.client=(client)
|
45
|
+
@config.client = client
|
46
|
+
end
|
53
47
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
48
|
+
def self.build_flags
|
49
|
+
@config.each_pair do |key, value|
|
50
|
+
if [TrueClass, FalseClass].include?(value.class)
|
51
|
+
define_singleton_method("#{key}?") do
|
52
|
+
@config[key]
|
53
|
+
end
|
54
|
+
end
|
60
55
|
end
|
56
|
+
end
|
61
57
|
|
58
|
+
def self.run
|
59
|
+
@config = Config.setup
|
62
60
|
|
63
|
-
|
64
|
-
return unless config.speak?
|
61
|
+
build_flags
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
Client.speak(what)
|
70
|
-
end
|
63
|
+
# Load Fzf if fuzzy search is enabled and fzf is installed
|
64
|
+
if @config.fuzzy && system('which fzf >/dev/null 2>&1')
|
65
|
+
require_relative 'aia/fzf'
|
71
66
|
end
|
72
67
|
|
68
|
+
prompt_handler = PromptHandler.new
|
69
|
+
@config.client = AIClientAdapter.new
|
70
|
+
session = Session.new(prompt_handler)
|
73
71
|
|
74
|
-
|
75
|
-
def debug? = AIA.config.debug?
|
72
|
+
session.start
|
76
73
|
end
|
77
74
|
end
|
78
|
-
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# lib/extensions/openstruct_merge.rb
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
class OpenStruct
|
7
|
+
def self.merge(*args)
|
8
|
+
result = OpenStruct.new
|
9
|
+
|
10
|
+
args.each do |arg|
|
11
|
+
unless [Hash, OpenStruct].include?(arg.class)
|
12
|
+
raise ArgumentError, "Only OpenStruct or Hash objects are allowed. bad: #{arg.class}"
|
13
|
+
end
|
14
|
+
|
15
|
+
arg.each_pair do |key, value|
|
16
|
+
set_value(result, key, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets value in result OpenStruct, handling nested OpenStruct and Hash objects
|
24
|
+
def self.set_value(result, key, value)
|
25
|
+
if value.is_a?(OpenStruct) || value.is_a?(Hash)
|
26
|
+
current_value = result[key]
|
27
|
+
current_value = {} if current_value.nil?
|
28
|
+
merged_value = merge(current_value, value.to_h)
|
29
|
+
result[key] = merged_value
|
30
|
+
else
|
31
|
+
result[key] = value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
__END__
|
37
|
+
|
38
|
+
# Usage example
|
39
|
+
os1 = OpenStruct.new(a: 1, b: 2, e: OpenStruct.new(x: 9))
|
40
|
+
os2 = OpenStruct.new(b: 3, c: 4)
|
41
|
+
os3 = {d: 5, e: {y: 10}}
|
42
|
+
|
43
|
+
merged_os = OpenStruct.merge(os1, os2, os3)
|
44
|
+
puts merged_os.inspect
|