askcii 0.3.0 → 0.4.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/.gitignore +3 -0
- data/CLAUDE.md +131 -0
- data/README.md +348 -109
- data/completions/README.md +64 -0
- data/completions/askcii.bash +29 -0
- data/completions/askcii.zsh +27 -0
- data/lib/askcii/application.rb +81 -48
- data/lib/askcii/chat_factory.rb +80 -0
- data/lib/askcii/chat_session.rb +71 -44
- data/lib/askcii/cli.rb +45 -1
- data/lib/askcii/commands/base_command.rb +45 -0
- data/lib/askcii/commands/chat_command.rb +56 -0
- data/lib/askcii/commands/clear_history_command.rb +31 -0
- data/lib/askcii/commands/configure_command.rb +17 -0
- data/lib/askcii/commands/help_command.rb +52 -0
- data/lib/askcii/commands/history_command.rb +46 -0
- data/lib/askcii/commands/last_response_command.rb +31 -0
- data/lib/askcii/commands/list_sessions_command.rb +36 -0
- data/lib/askcii/config_validator.rb +90 -0
- data/lib/askcii/configuration_manager.rb +170 -151
- data/lib/askcii/errors.rb +15 -0
- data/lib/askcii/models/chat.rb +6 -3
- data/lib/askcii/models/config.rb +12 -4
- data/lib/askcii/provider_config.rb +116 -0
- data/lib/askcii/version.rb +1 -1
- data/lib/askcii.rb +72 -34
- metadata +17 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#compdef askcii
|
|
2
|
+
|
|
3
|
+
_askcii() {
|
|
4
|
+
local -a opts
|
|
5
|
+
opts=(
|
|
6
|
+
'-p[Start a private session]'
|
|
7
|
+
'--private[Start a private session]'
|
|
8
|
+
'-r[Output the last response]'
|
|
9
|
+
'--last-response[Output the last response]'
|
|
10
|
+
'-c[Manage configurations]'
|
|
11
|
+
'--configure[Manage configurations]'
|
|
12
|
+
'-m[Use specific configuration ID]:config_id:'
|
|
13
|
+
'--model[Use specific configuration ID]:config_id:'
|
|
14
|
+
'-v[Show detailed information]'
|
|
15
|
+
'--verbose[Show detailed information]'
|
|
16
|
+
'--session[Use specific session name]:session_name:'
|
|
17
|
+
'--list-sessions[List all available sessions]'
|
|
18
|
+
'--history[Show current session history]'
|
|
19
|
+
'--clear-history[Clear current session history]'
|
|
20
|
+
'-h[Show help message]'
|
|
21
|
+
'--help[Show help message]'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_arguments -s $opts '*:prompt text:'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_askcii "$@"
|
data/lib/askcii/application.rb
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'cli'
|
|
4
|
-
require_relative '
|
|
5
|
-
require_relative '
|
|
4
|
+
require_relative 'commands/base_command'
|
|
5
|
+
require_relative 'commands/help_command'
|
|
6
|
+
require_relative 'commands/configure_command'
|
|
7
|
+
require_relative 'commands/last_response_command'
|
|
8
|
+
require_relative 'commands/chat_command'
|
|
9
|
+
require_relative 'commands/list_sessions_command'
|
|
10
|
+
require_relative 'commands/history_command'
|
|
11
|
+
require_relative 'commands/clear_history_command'
|
|
6
12
|
|
|
7
13
|
module Askcii
|
|
14
|
+
# Main application controller that routes to appropriate commands
|
|
8
15
|
class Application
|
|
16
|
+
attr_reader :cli
|
|
17
|
+
|
|
9
18
|
def initialize(args = ARGV.dup)
|
|
10
19
|
@cli = CLI.new(args)
|
|
11
20
|
end
|
|
@@ -13,74 +22,98 @@ module Askcii
|
|
|
13
22
|
def run
|
|
14
23
|
@cli.parse!
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
if @cli.configure?
|
|
22
|
-
ConfigurationManager.new.run
|
|
23
|
-
exit 0
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
selected_config = determine_configuration
|
|
27
|
-
configure_llm(selected_config)
|
|
28
|
-
|
|
29
|
-
chat_session = ChatSession.new(@cli.options, selected_config)
|
|
30
|
-
chat_session.handle_last_response if @cli.last_response?
|
|
31
|
-
|
|
32
|
-
prompt, input = determine_prompt_and_input
|
|
33
|
-
|
|
34
|
-
if prompt.empty? && input.nil?
|
|
35
|
-
puts @cli.usage_message
|
|
25
|
+
# Handle usage display
|
|
26
|
+
if @cli.show_usage?
|
|
27
|
+
puts Commands::HelpCommand.new(@cli).execute
|
|
36
28
|
exit 1
|
|
37
29
|
end
|
|
38
30
|
|
|
39
|
-
|
|
31
|
+
# Determine and execute the appropriate command
|
|
32
|
+
command = determine_command
|
|
33
|
+
command.execute
|
|
34
|
+
rescue ConfigurationError => e
|
|
35
|
+
$stderr.puts "Configuration Error: #{e.message}"
|
|
36
|
+
$stderr.puts "Run 'askcii -c' to configure providers."
|
|
37
|
+
exit 1
|
|
38
|
+
rescue ValidationError => e
|
|
39
|
+
$stderr.puts "Validation Error: #{e.message}"
|
|
40
|
+
exit 1
|
|
41
|
+
rescue SessionError => e
|
|
42
|
+
$stderr.puts "Session Error: #{e.message}"
|
|
43
|
+
exit 1
|
|
44
|
+
rescue => e
|
|
45
|
+
$stderr.puts "Error: #{e.message}"
|
|
46
|
+
$stderr.puts e.backtrace.join("\n") if @cli.verbose?
|
|
47
|
+
exit 1
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
private
|
|
43
51
|
|
|
52
|
+
# Determines which command to execute based on CLI flags
|
|
53
|
+
# @return [Commands::BaseCommand]
|
|
54
|
+
def determine_command
|
|
55
|
+
return Commands::HelpCommand.new(@cli) if @cli.help?
|
|
56
|
+
return Commands::ConfigureCommand.new(@cli) if @cli.configure?
|
|
57
|
+
return Commands::ListSessionsCommand.new(@cli) if @cli.list_sessions?
|
|
58
|
+
return Commands::HistoryCommand.new(@cli) if @cli.history?
|
|
59
|
+
return Commands::ClearHistoryCommand.new(@cli) if @cli.clear_history?
|
|
60
|
+
return Commands::LastResponseCommand.new(@cli) if @cli.last_response?
|
|
61
|
+
|
|
62
|
+
# Default to chat command
|
|
63
|
+
config = determine_configuration
|
|
64
|
+
configure_llm(config)
|
|
65
|
+
Commands::ChatCommand.new(@cli, config)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Determines which configuration to use based on CLI flags and defaults
|
|
69
|
+
# @return [Hash] Configuration hash
|
|
44
70
|
def determine_configuration
|
|
71
|
+
# Priority 1: Explicit configuration ID from CLI
|
|
45
72
|
if @cli.model_config_id
|
|
46
73
|
config = Askcii::Config.get_configuration(@cli.model_config_id)
|
|
47
|
-
return config if config
|
|
74
|
+
return symbolize_keys(config) if config
|
|
75
|
+
$stderr.puts "Warning: Configuration ID #{@cli.model_config_id} not found, using default"
|
|
48
76
|
end
|
|
49
77
|
|
|
78
|
+
# Priority 2: Default configuration from database
|
|
50
79
|
config = Askcii::Config.current_configuration
|
|
51
|
-
return config if config
|
|
80
|
+
return symbolize_keys(config) if config
|
|
81
|
+
|
|
82
|
+
# Priority 3: Legacy environment variables or error
|
|
83
|
+
legacy_config = load_legacy_env_config
|
|
84
|
+
return symbolize_keys(legacy_config) if legacy_config
|
|
85
|
+
|
|
86
|
+
# No configuration available
|
|
87
|
+
raise ConfigurationError, "No configuration found. Run 'askcii -c' to configure."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Loads legacy configuration from environment variables
|
|
91
|
+
# @return [Hash, nil]
|
|
92
|
+
def load_legacy_env_config
|
|
93
|
+
return nil unless ENV['ASKCII_API_KEY']
|
|
52
94
|
|
|
53
|
-
# Fallback to environment variables
|
|
54
95
|
{
|
|
96
|
+
'provider' => 'openai',
|
|
55
97
|
'api_key' => ENV['ASKCII_API_KEY'],
|
|
56
|
-
'api_endpoint' => ENV['ASKCII_API_ENDPOINT'],
|
|
57
|
-
'model_id' => ENV['ASKCII_MODEL_ID']
|
|
98
|
+
'api_endpoint' => ENV['ASKCII_API_ENDPOINT'] || ProviderConfig.default_endpoint('openai'),
|
|
99
|
+
'model_id' => ENV['ASKCII_MODEL_ID'] || 'gpt-3.5-turbo',
|
|
100
|
+
'name' => 'Legacy (from env vars)'
|
|
58
101
|
}
|
|
59
102
|
end
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def determine_prompt_and_input
|
|
66
|
-
stdin_content = read_stdin_input
|
|
67
|
-
|
|
68
|
-
if @cli.prompt.empty? && stdin_content
|
|
69
|
-
# No prompt provided via args, use stdin as prompt
|
|
70
|
-
[stdin_content.strip, nil]
|
|
71
|
-
elsif !@cli.prompt.empty? && stdin_content
|
|
72
|
-
# Both prompt and stdin provided, use stdin as input context
|
|
73
|
-
[@cli.prompt, stdin_content]
|
|
74
|
-
else
|
|
75
|
-
# Only prompt provided (or neither)
|
|
76
|
-
[@cli.prompt, nil]
|
|
77
|
-
end
|
|
104
|
+
# Configures the LLM library with the selected configuration
|
|
105
|
+
# @param config [Hash] Configuration hash
|
|
106
|
+
def configure_llm(config)
|
|
107
|
+
Askcii.configure_llm(config)
|
|
78
108
|
end
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
# Converts string keys to symbols for consistency
|
|
111
|
+
# @param hash [Hash] Hash with string keys
|
|
112
|
+
# @return [Hash] Hash with symbol keys
|
|
113
|
+
def symbolize_keys(hash)
|
|
114
|
+
return {} unless hash
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
hash.transform_keys { |key| key.to_sym }
|
|
84
117
|
end
|
|
85
118
|
end
|
|
86
119
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Askcii
|
|
6
|
+
# Factory for creating chat instances (private or persistent)
|
|
7
|
+
class ChatFactory
|
|
8
|
+
attr_reader :config, :private, :session_context
|
|
9
|
+
|
|
10
|
+
# @param config [Hash] Configuration with provider, api_key, model_id, etc.
|
|
11
|
+
# @param private [Boolean] Whether to create a private (non-persistent) chat
|
|
12
|
+
# @param session_context [String, nil] Session identifier for persistent chats
|
|
13
|
+
def initialize(config:, private: false, session_context: nil)
|
|
14
|
+
@config = config
|
|
15
|
+
@private = private
|
|
16
|
+
@session_context = session_context || generate_session_id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Creates and returns a chat instance
|
|
20
|
+
# @return [RubyLLM::Chat] Chat instance ready for use
|
|
21
|
+
def create
|
|
22
|
+
private ? create_private_chat : create_persistent_chat
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Gets the system instruction from config or uses default
|
|
26
|
+
# @return [String] System instruction
|
|
27
|
+
def system_instruction
|
|
28
|
+
config[:system_instruction] ||
|
|
29
|
+
config['system_instruction'] ||
|
|
30
|
+
default_system_instruction
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Creates a private (ephemeral) chat with no database persistence
|
|
36
|
+
# @return [RubyLLM::Chat]
|
|
37
|
+
def create_private_chat
|
|
38
|
+
provider = config[:provider] || config['provider']
|
|
39
|
+
model_id = config[:model_id] || config['model_id']
|
|
40
|
+
|
|
41
|
+
RubyLLM.chat(
|
|
42
|
+
provider: provider.to_sym,
|
|
43
|
+
model_id: model_id,
|
|
44
|
+
assume_model_exists: true
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Creates a persistent chat backed by database
|
|
49
|
+
# @return [RubyLLM::Chat]
|
|
50
|
+
def create_persistent_chat
|
|
51
|
+
model_id = config[:model_id] || config['model_id']
|
|
52
|
+
|
|
53
|
+
# Find or create chat record
|
|
54
|
+
chat_record = Chat.find_or_create(context: session_context) do |chat|
|
|
55
|
+
chat.model_id = model_id
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Update model_id if it changed
|
|
59
|
+
if chat_record.model_id != model_id
|
|
60
|
+
chat_record.update(model_id: model_id)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convert to RubyLLM chat with history
|
|
64
|
+
chat_record.to_llm(config)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Generates a unique session identifier
|
|
68
|
+
# @return [String] 32-character hex string
|
|
69
|
+
def generate_session_id
|
|
70
|
+
SecureRandom.hex(16)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Default system instruction for terminal-friendly responses
|
|
74
|
+
# @return [String]
|
|
75
|
+
def default_system_instruction
|
|
76
|
+
'You are a command line application. Provide concise, terminal-friendly responses. ' \
|
|
77
|
+
'Assume technical proficiency. Minimize explanations unless requested.'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/askcii/chat_session.rb
CHANGED
|
@@ -1,70 +1,97 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative 'chat_factory'
|
|
4
4
|
|
|
5
5
|
module Askcii
|
|
6
|
+
# Orchestrates chat execution with LLM providers
|
|
6
7
|
class ChatSession
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
attr_reader :config, :private, :session_context, :verbose
|
|
9
|
+
|
|
10
|
+
# @param config [Hash] Configuration with provider, api_key, model_id, etc.
|
|
11
|
+
# @param private [Boolean] Whether to create a private (non-persistent) chat
|
|
12
|
+
# @param session_context [String, nil] Session identifier for persistent chats
|
|
13
|
+
# @param verbose [Boolean] Whether to output verbose information
|
|
14
|
+
def initialize(config:, private: false, session_context: nil, verbose: false)
|
|
15
|
+
@config = config
|
|
16
|
+
@private = private
|
|
17
|
+
@session_context = session_context
|
|
18
|
+
@verbose = verbose
|
|
10
19
|
end
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
# Executes a chat session with the given prompt
|
|
22
|
+
# @param prompt [String] User prompt
|
|
23
|
+
# @param input [String, nil] Additional input to prepend to prompt
|
|
24
|
+
def execute_chat(prompt, input = nil)
|
|
25
|
+
validate_prompt!(prompt)
|
|
26
|
+
warn_large_input!(prompt, input)
|
|
27
|
+
|
|
28
|
+
chat = create_chat
|
|
29
|
+
full_prompt = build_prompt(prompt, input)
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
model_id = @selected_config['model_id']
|
|
17
|
-
chat_record = Askcii::Chat.find_or_create(context: context, model_id: model_id)
|
|
31
|
+
verbose_output "Sending prompt (#{full_prompt.bytesize} bytes)..."
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
exit 0
|
|
23
|
-
else
|
|
24
|
-
puts 'No previous response found.'
|
|
25
|
-
exit 1
|
|
33
|
+
# Execute the chat with streaming
|
|
34
|
+
chat.ask(full_prompt) do |chunk|
|
|
35
|
+
print chunk.content
|
|
26
36
|
end
|
|
37
|
+
puts '' # Ensure we end with a newline
|
|
27
38
|
end
|
|
28
39
|
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Creates a chat instance using ChatFactory
|
|
43
|
+
# @return [RubyLLM::Chat]
|
|
29
44
|
def create_chat
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
45
|
+
factory = ChatFactory.new(
|
|
46
|
+
config: config,
|
|
47
|
+
private: private,
|
|
48
|
+
session_context: session_context
|
|
49
|
+
)
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
chat = create_chat
|
|
51
|
+
chat = factory.create
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
# Apply system instructions
|
|
54
|
+
chat.with_instructions(factory.system_instruction)
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
verbose_output "Chat created (#{private ? 'private' : 'persistent'})"
|
|
43
57
|
|
|
44
|
-
chat
|
|
45
|
-
print chunk.content
|
|
46
|
-
end
|
|
47
|
-
puts ''
|
|
58
|
+
chat
|
|
48
59
|
end
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
# Validates that the prompt is not empty
|
|
62
|
+
# @param prompt [String]
|
|
63
|
+
# @raise [ValidationError] if prompt is empty
|
|
64
|
+
def validate_prompt!(prompt)
|
|
65
|
+
raise ValidationError, "Prompt cannot be empty" if prompt.to_s.strip.empty?
|
|
66
|
+
end
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
# Warns if input is very large
|
|
69
|
+
# @param prompt [String]
|
|
70
|
+
# @param input [String, nil]
|
|
71
|
+
def warn_large_input!(prompt, input)
|
|
72
|
+
combined = [input, prompt].compact.join("\n\n")
|
|
73
|
+
size_bytes = combined.bytesize
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
return unless size_bytes > 1_000_000 # 1MB
|
|
76
|
+
|
|
77
|
+
size_kb = size_bytes / 1024
|
|
78
|
+
$stderr.puts "Warning: Input size is #{size_kb}KB. This may be slow or fail."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Builds the full prompt by combining input and prompt
|
|
82
|
+
# @param prompt [String]
|
|
83
|
+
# @param input [String, nil]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def build_prompt(prompt, input)
|
|
86
|
+
return prompt unless input
|
|
87
|
+
|
|
88
|
+
"With the following text:\n\n#{input}\n\n#{prompt}"
|
|
61
89
|
end
|
|
62
90
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
chat_record.to_llm
|
|
91
|
+
# Outputs verbose information if verbose mode is enabled
|
|
92
|
+
# @param message [String]
|
|
93
|
+
def verbose_output(message)
|
|
94
|
+
$stderr.puts message if verbose
|
|
68
95
|
end
|
|
69
96
|
end
|
|
70
97
|
end
|
data/lib/askcii/cli.rb
CHANGED
|
@@ -23,7 +23,11 @@ module Askcii
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def show_usage?
|
|
26
|
-
|
|
26
|
+
@prompt.empty? && !configure? && !last_response? && !list_sessions? && !history? && !clear_history?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def help?
|
|
30
|
+
@options[:help]
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def configure?
|
|
@@ -38,10 +42,30 @@ module Askcii
|
|
|
38
42
|
@options[:private]
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
def verbose?
|
|
46
|
+
@options[:verbose]
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def model_config_id
|
|
42
50
|
@options[:model_config_id]
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
def session
|
|
54
|
+
@options[:session]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def list_sessions?
|
|
58
|
+
@options[:list_sessions]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def history?
|
|
62
|
+
@options[:history]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def clear_history?
|
|
66
|
+
@options[:clear_history]
|
|
67
|
+
end
|
|
68
|
+
|
|
45
69
|
def help_message
|
|
46
70
|
option_parser.to_s
|
|
47
71
|
end
|
|
@@ -90,6 +114,26 @@ module Askcii
|
|
|
90
114
|
@options[:model_config_id] = model_id
|
|
91
115
|
end
|
|
92
116
|
|
|
117
|
+
opts.on('-v', '--verbose', 'Show detailed information during execution') do
|
|
118
|
+
@options[:verbose] = true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
opts.on('--session NAME', 'Use specific session name') do |session|
|
|
122
|
+
@options[:session] = session
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
opts.on('--list-sessions', 'List all available sessions') do
|
|
126
|
+
@options[:list_sessions] = true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
opts.on('--history', 'Show current session history') do
|
|
130
|
+
@options[:history] = true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
opts.on('--clear-history', 'Clear current session history') do
|
|
134
|
+
@options[:clear_history] = true
|
|
135
|
+
end
|
|
136
|
+
|
|
93
137
|
opts.on('-h', '--help', 'Show this help message') do
|
|
94
138
|
@options[:help] = true
|
|
95
139
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Base class for all command implementations
|
|
6
|
+
class BaseCommand
|
|
7
|
+
attr_reader :cli, :config
|
|
8
|
+
|
|
9
|
+
# @param cli [CLI] Parsed CLI arguments
|
|
10
|
+
# @param config [Hash, nil] Configuration (if applicable)
|
|
11
|
+
def initialize(cli, config = nil)
|
|
12
|
+
@cli = cli
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Executes the command
|
|
17
|
+
# @return [void]
|
|
18
|
+
def execute
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #execute"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
# Prints error message to stderr and exits
|
|
25
|
+
# @param message [String] Error message
|
|
26
|
+
# @param exit_code [Integer] Exit code (default: 1)
|
|
27
|
+
def error!(message, exit_code: 1)
|
|
28
|
+
$stderr.puts "Error: #{message}"
|
|
29
|
+
exit exit_code
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Prints verbose output if verbose mode is enabled
|
|
33
|
+
# @param message [String] Message to print
|
|
34
|
+
def verbose(message)
|
|
35
|
+
$stderr.puts message if cli.verbose?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Gets the session context from CLI or environment
|
|
39
|
+
# @return [String]
|
|
40
|
+
def session_context
|
|
41
|
+
cli.session || ENV['ASKCII_SESSION'] || SecureRandom.hex(16)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Executes a chat session with the LLM
|
|
6
|
+
class ChatCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
validate_config!
|
|
9
|
+
display_config_info if cli.verbose?
|
|
10
|
+
|
|
11
|
+
# Read from stdin if available
|
|
12
|
+
stdin_input = read_stdin_input
|
|
13
|
+
|
|
14
|
+
# Execute the chat
|
|
15
|
+
require_relative '../chat_session'
|
|
16
|
+
session = ChatSession.new(
|
|
17
|
+
config: config,
|
|
18
|
+
private: cli.private?,
|
|
19
|
+
session_context: session_context,
|
|
20
|
+
verbose: cli.verbose?
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
session.execute_chat(cli.prompt, stdin_input)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_config!
|
|
29
|
+
return if config
|
|
30
|
+
|
|
31
|
+
error! "No configuration available. Run 'askcii -c' to configure."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def display_config_info
|
|
35
|
+
provider = config[:provider] || config['provider']
|
|
36
|
+
model_id = config[:model_id] || config['model_id']
|
|
37
|
+
config_name = config[:name] || config['name'] || 'Unnamed'
|
|
38
|
+
|
|
39
|
+
verbose "Using configuration: #{config_name}"
|
|
40
|
+
verbose "Provider: #{provider}"
|
|
41
|
+
verbose "Model: #{model_id}"
|
|
42
|
+
verbose "Session: #{session_context}"
|
|
43
|
+
verbose "Private mode: #{cli.private?}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_stdin_input
|
|
47
|
+
return nil if $stdin.tty?
|
|
48
|
+
|
|
49
|
+
verbose "Reading from stdin..."
|
|
50
|
+
input = $stdin.read.strip
|
|
51
|
+
verbose "Read #{input.bytesize} bytes from stdin"
|
|
52
|
+
input.empty? ? nil : input
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Clears conversation history for the current session
|
|
6
|
+
class ClearHistoryCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Clearing history for session: #{session_context}"
|
|
9
|
+
|
|
10
|
+
chat = Chat.find(context: session_context)
|
|
11
|
+
|
|
12
|
+
unless chat
|
|
13
|
+
puts "No session found to clear."
|
|
14
|
+
exit 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
message_count = chat.messages.count
|
|
18
|
+
|
|
19
|
+
# Delete all messages for this chat
|
|
20
|
+
chat.messages_dataset.delete
|
|
21
|
+
|
|
22
|
+
# Delete the chat itself
|
|
23
|
+
chat.delete
|
|
24
|
+
|
|
25
|
+
puts "Cleared session '#{session_context}' (#{message_count} messages removed)"
|
|
26
|
+
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Askcii
|
|
4
|
+
module Commands
|
|
5
|
+
# Opens configuration management interface
|
|
6
|
+
class ConfigureCommand < BaseCommand
|
|
7
|
+
def execute
|
|
8
|
+
verbose "Opening configuration manager..."
|
|
9
|
+
|
|
10
|
+
require_relative '../configuration_manager'
|
|
11
|
+
ConfigurationManager.new.run
|
|
12
|
+
|
|
13
|
+
exit 0
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|