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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Askcii
4
+ module Commands
5
+ # Displays help information
6
+ class HelpCommand < BaseCommand
7
+ def execute
8
+ puts <<~HELP
9
+ askcii - Command-line interface for multiple LLM providers
10
+
11
+ Usage:
12
+ askcii [options] "Your prompt here"
13
+ echo "input" | askcii [options] "Your prompt"
14
+
15
+ Options:
16
+ -p, --private Create a private session (no history saved)
17
+ -r, --last-response Retrieve the last assistant response from current session
18
+ -c, --configure Open configuration management interface
19
+ -m, --model ID Use specific configuration by ID
20
+ -v, --verbose Show detailed information during execution
21
+ --session NAME Use specific session name (alternative to ASKCII_SESSION)
22
+ --list-sessions List all available sessions
23
+ --history Show current session history
24
+ --clear-history Clear current session history
25
+ -h, --help Show this help message
26
+
27
+ Environment Variables:
28
+ ASKCII_SESSION Session identifier for conversation persistence
29
+ ASKCII_API_KEY API key (legacy, use -c to configure)
30
+ ASKCII_API_ENDPOINT API endpoint (legacy)
31
+ ASKCII_MODEL_ID Model ID (legacy)
32
+
33
+ Examples:
34
+ askcii "Explain what a Ruby block is"
35
+ askcii -p "What is the capital of France?"
36
+ echo "def hello\nputs 'hi'\nend" | askcii "Explain this Ruby code"
37
+ askcii -m 2 "Use configuration #2"
38
+ askcii --session my-project "Continue conversation in my-project session"
39
+ ASKCII_SESSION=work askcii "Use work session"
40
+
41
+ Configuration:
42
+ Run 'askcii -c' to configure providers interactively.
43
+ Supports: OpenAI, Anthropic, Gemini, DeepSeek, OpenRouter, Ollama
44
+
45
+ For more information, visit: https://github.com/yourusername/askcii
46
+ HELP
47
+
48
+ exit 0
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Askcii
4
+ module Commands
5
+ # Displays conversation history for the current session
6
+ class HistoryCommand < BaseCommand
7
+ def execute
8
+ verbose "Retrieving history for session: #{session_context}"
9
+
10
+ chat = Chat.find(context: session_context)
11
+
12
+ unless chat
13
+ puts "No history found for this session."
14
+ exit 0
15
+ end
16
+
17
+ messages = chat.messages_dataset.order(:created_at).all
18
+
19
+ if messages.empty?
20
+ puts "No messages in this session."
21
+ exit 0
22
+ end
23
+
24
+ puts "Session: #{chat.context}"
25
+ puts "Model: #{chat.model_id}"
26
+ puts "Messages: #{messages.count}"
27
+ puts
28
+ puts "=" * 80
29
+ puts
30
+
31
+ messages.each do |msg|
32
+ role_label = msg.role.to_s.capitalize
33
+ timestamp = msg.created_at.strftime('%Y-%m-%d %H:%M:%S')
34
+
35
+ puts "#{role_label} (#{timestamp}):"
36
+ puts msg.content
37
+ puts
38
+ puts "-" * 80
39
+ puts
40
+ end
41
+
42
+ exit 0
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Askcii
4
+ module Commands
5
+ # Retrieves and displays the last assistant response from the current session
6
+ class LastResponseCommand < BaseCommand
7
+ def execute
8
+ verbose "Retrieving last response from session: #{session_context}"
9
+
10
+ chat = Chat.find(context: session_context)
11
+
12
+ unless chat
13
+ error! "No chat session found for context: #{session_context}"
14
+ end
15
+
16
+ last_message = chat.messages_dataset
17
+ .where(role: 'assistant')
18
+ .order(Sequel.desc(:created_at))
19
+ .first
20
+
21
+ unless last_message
22
+ error! "No assistant messages found in this session"
23
+ end
24
+
25
+ puts last_message.content
26
+
27
+ exit 0
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Askcii
4
+ module Commands
5
+ # Lists all available chat sessions
6
+ class ListSessionsCommand < BaseCommand
7
+ def execute
8
+ verbose "Listing all sessions..."
9
+
10
+ chats = Chat.order(Sequel.desc(:created_at)).all
11
+
12
+ if chats.empty?
13
+ puts "No sessions found."
14
+ exit 0
15
+ end
16
+
17
+ puts "Available sessions:"
18
+ puts
19
+
20
+ chats.each do |chat|
21
+ message_count = chat.messages.count
22
+ last_message = chat.messages_dataset.order(Sequel.desc(:created_at)).first
23
+ last_updated = last_message&.created_at || chat.created_at
24
+
25
+ puts "Session: #{chat.context}"
26
+ puts " Model: #{chat.model_id}"
27
+ puts " Messages: #{message_count}"
28
+ puts " Last updated: #{last_updated}"
29
+ puts
30
+ end
31
+
32
+ exit 0
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Askcii
4
+ # Validates configuration settings before use
5
+ class ConfigValidator
6
+ VALID_PROVIDERS = %w[openai anthropic gemini deepseek openrouter ollama].freeze
7
+
8
+ class << self
9
+ # Validates a configuration hash
10
+ # @param config [Hash] Configuration to validate
11
+ # @raise [ConfigurationError] if configuration is invalid
12
+ # @return [Hash] The validated configuration
13
+ def validate!(config)
14
+ validate_presence!(config)
15
+ validate_provider!(config)
16
+ validate_api_key!(config)
17
+ validate_model_id!(config)
18
+ validate_api_endpoint!(config)
19
+ config
20
+ end
21
+
22
+ # Checks if a provider name is valid
23
+ # @param name [String] Provider name
24
+ # @return [Boolean]
25
+ def valid_provider?(name)
26
+ VALID_PROVIDERS.include?(name.to_s.downcase)
27
+ end
28
+
29
+ # Lists all valid provider names
30
+ # @return [Array<String>]
31
+ def valid_providers
32
+ VALID_PROVIDERS
33
+ end
34
+
35
+ private
36
+
37
+ def validate_presence!(config)
38
+ raise ConfigurationError, "Configuration cannot be nil" if config.nil?
39
+ raise ConfigurationError, "Configuration cannot be empty" if config.empty?
40
+ end
41
+
42
+ def validate_provider!(config)
43
+ provider = config[:provider].to_s.strip
44
+ if provider.empty?
45
+ raise ConfigurationError, "Provider is required"
46
+ end
47
+
48
+ unless valid_provider?(provider)
49
+ raise ConfigurationError,
50
+ "Invalid provider '#{provider}'. Must be one of: #{VALID_PROVIDERS.join(', ')}"
51
+ end
52
+ end
53
+
54
+ def validate_api_key!(config)
55
+ provider = config[:provider].to_s.downcase
56
+
57
+ # Ollama doesn't require an API key
58
+ return if provider == 'ollama'
59
+
60
+ api_key = config[:api_key].to_s.strip
61
+ if api_key.empty?
62
+ raise ConfigurationError, "API key is required for #{provider}"
63
+ end
64
+
65
+ if api_key == 'blank'
66
+ raise ConfigurationError, "API key cannot be 'blank'"
67
+ end
68
+ end
69
+
70
+ def validate_model_id!(config)
71
+ model_id = config[:model_id].to_s.strip
72
+ if model_id.empty?
73
+ raise ConfigurationError, "Model ID is required"
74
+ end
75
+ end
76
+
77
+ def validate_api_endpoint!(config)
78
+ endpoint = config[:api_endpoint].to_s.strip
79
+ if endpoint.empty?
80
+ raise ConfigurationError, "API endpoint is required"
81
+ end
82
+
83
+ # Basic URL format validation
84
+ unless endpoint.start_with?('http://', 'https://')
85
+ raise ConfigurationError, "API endpoint must start with http:// or https://"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Askcii
4
+ # Interactive terminal UI for managing provider configurations
4
5
  class ConfigurationManager
5
- PROVIDER_MAP = {
6
+ # Maps user selection numbers to provider identifiers
7
+ PROVIDER_MENU = {
6
8
  '1' => 'openai',
7
9
  '2' => 'anthropic',
8
10
  '3' => 'gemini',
@@ -11,102 +13,50 @@ module Askcii
11
13
  '6' => 'ollama'
12
14
  }.freeze
13
15
 
14
- DEFAULT_ENDPOINTS = {
15
- 'openai' => 'https://api.openai.com/v1',
16
- 'anthropic' => 'https://api.anthropic.com',
17
- 'gemini' => 'https://generativelanguage.googleapis.com/v1',
18
- 'deepseek' => 'https://api.deepseek.com/v1',
19
- 'openrouter' => 'https://openrouter.ai/api/v1',
20
- 'ollama' => 'http://localhost:11434/v1'
21
- }.freeze
22
-
23
- PROVIDER_MODELS = {
24
- 'openai' => {
25
- default: 'gpt-4o',
26
- models: [
27
- 'gpt-4o',
28
- 'gpt-4o-mini',
29
- 'gpt-4-turbo',
30
- 'gpt-4',
31
- 'gpt-3.5-turbo'
32
- ]
33
- },
34
- 'anthropic' => {
35
- default: 'claude-3-5-sonnet-20241022',
36
- models: [
37
- 'claude-3-5-sonnet-20241022',
38
- 'claude-3-5-haiku-20241022',
39
- 'claude-3-opus-20240229',
40
- 'claude-3-sonnet-20240229',
41
- 'claude-3-haiku-20240307'
42
- ]
43
- },
44
- 'gemini' => {
45
- default: 'gemini-pro',
46
- models: [
47
- 'gemini-pro',
48
- 'gemini-pro-vision',
49
- 'gemini-1.5-pro',
50
- 'gemini-1.5-flash'
51
- ]
52
- },
53
- 'deepseek' => {
54
- default: 'deepseek-chat',
55
- models: %w[
56
- deepseek-chat
57
- deepseek-coder
58
- ]
59
- },
60
- 'openrouter' => {
61
- default: 'anthropic/claude-3.5-sonnet',
62
- models: [
63
- 'anthropic/claude-3.5-sonnet',
64
- 'openai/gpt-4o',
65
- 'google/gemini-pro',
66
- 'meta-llama/llama-3.1-405b-instruct',
67
- 'anthropic/claude-3-opus',
68
- 'openai/gpt-4-turbo'
69
- ]
70
- },
71
- 'ollama' => {
72
- default: 'llama3.2',
73
- models: [
74
- 'llama3.2',
75
- 'llama3.1',
76
- 'mistral',
77
- 'codellama',
78
- 'phi3',
79
- 'gemma2'
80
- ]
81
- }
82
- }.freeze
83
-
84
16
  def run
85
- show_current_configurations
86
- show_menu
87
- handle_user_choice
17
+ loop do
18
+ show_current_configurations
19
+ show_menu
20
+ choice = handle_user_choice
21
+ break if choice == :exit
22
+ end
88
23
  end
89
24
 
90
25
  private
91
26
 
92
27
  def show_current_configurations
28
+ puts
93
29
  puts 'Configuration Management'
94
- puts '======================'
30
+ puts '=' * 50
31
+ puts
95
32
 
96
33
  configs = Askcii::Config.configurations
97
34
  default_id = Askcii::Config.default_configuration_id
98
35
 
99
36
  if configs.empty?
100
37
  puts 'No configurations found.'
38
+ puts 'Add a configuration to get started.'
101
39
  else
102
40
  puts 'Current configurations:'
41
+ puts
103
42
  configs.each do |config|
104
- marker = config['id'] == default_id ? ' (default)' : ''
105
- provider_info = config['provider'] ? " [#{config['provider']}]" : ''
106
- puts " #{config['id']}. #{config['name']}#{provider_info}#{marker}"
43
+ show_configuration_summary(config, default_id)
107
44
  end
108
- puts
109
45
  end
46
+ puts
47
+ end
48
+
49
+ def show_configuration_summary(config, default_id)
50
+ is_default = config['id'] == default_id
51
+ marker = is_default ? ' ★ (default)' : ''
52
+ provider_name = ProviderConfig.display_name(config['provider'])
53
+
54
+ puts " #{config['id']}. #{config['name']}"
55
+ puts " Provider: #{provider_name}"
56
+ puts " Model: #{config['model_id']}"
57
+ puts " Endpoint: #{config['api_endpoint']}"
58
+ puts "#{marker}"
59
+ puts
110
60
  end
111
61
 
112
62
  def show_menu
@@ -114,8 +64,9 @@ module Askcii
114
64
  puts ' 1. Add new configuration'
115
65
  puts ' 2. Set default configuration'
116
66
  puts ' 3. Delete configuration'
117
- puts ' 4. Exit'
118
- print 'Select option (1-4): '
67
+ puts ' 4. Test configuration'
68
+ puts ' 5. Exit'
69
+ print 'Select option (1-5): '
119
70
  end
120
71
 
121
72
  def handle_user_choice
@@ -124,20 +75,32 @@ module Askcii
124
75
  case choice
125
76
  when '1'
126
77
  add_new_configuration
78
+ :continue
127
79
  when '2'
128
80
  set_default_configuration
81
+ :continue
129
82
  when '3'
130
83
  delete_configuration
84
+ :continue
131
85
  when '4'
132
- puts 'Exiting.'
86
+ test_configuration
87
+ :continue
88
+ when '5'
89
+ puts 'Exiting configuration manager.'
90
+ :exit
133
91
  else
134
- puts 'Invalid option.'
92
+ puts 'Invalid option. Please select 1-5.'
93
+ :continue
135
94
  end
136
95
  end
137
96
 
138
97
  def add_new_configuration
98
+ puts
99
+ puts 'Add New Configuration'
100
+ puts '-' * 50
101
+
139
102
  print 'Enter configuration name: '
140
- name = $stdin.gets.chomp
103
+ name = $stdin.gets.chomp.strip
141
104
 
142
105
  provider = select_provider
143
106
  return unless provider
@@ -147,28 +110,65 @@ module Askcii
147
110
 
148
111
  endpoint = get_api_endpoint(provider)
149
112
  model_id = get_model_id(provider)
150
-
151
113
  return unless model_id
152
114
 
115
+ # Use model_id as name if no name provided
153
116
  name = model_id if name.empty?
154
- Askcii::Config.add_configuration(name, api_key || '', endpoint, model_id, provider)
155
- puts 'Configuration added successfully!'
117
+
118
+ # Build configuration hash
119
+ config = {
120
+ name: name,
121
+ api_key: api_key || '',
122
+ api_endpoint: endpoint,
123
+ model_id: model_id,
124
+ provider: provider
125
+ }
126
+
127
+ # Validate configuration
128
+ begin
129
+ ConfigValidator.validate!(config)
130
+ rescue ConfigurationError => e
131
+ puts "Validation failed: #{e.message}"
132
+ return
133
+ end
134
+
135
+ # Save configuration
136
+ Askcii::Config.add_configuration(
137
+ name: name,
138
+ api_key: api_key || '',
139
+ api_endpoint: endpoint,
140
+ model_id: model_id,
141
+ provider: provider
142
+ )
143
+
144
+ puts
145
+ puts '✓ Configuration added successfully!'
146
+ puts
147
+
148
+ # Offer to test the configuration
149
+ print 'Test this configuration now? (y/n): '
150
+ if $stdin.gets.chomp.downcase == 'y'
151
+ # Get the newly added configuration ID
152
+ configs = Askcii::Config.configurations
153
+ new_config = configs.last
154
+ test_specific_configuration(new_config) if new_config
155
+ end
156
156
  end
157
157
 
158
158
  def select_provider
159
+ puts
159
160
  puts 'Select provider:'
160
- puts ' 1. OpenAI'
161
- puts ' 2. Anthropic'
162
- puts ' 3. Gemini'
163
- puts ' 4. DeepSeek'
164
- puts ' 5. OpenRouter'
165
- puts ' 6. Ollama (no API key needed)'
166
- print 'Provider (1-6): '
167
-
168
- provider_choice = $stdin.gets.chomp
169
- provider = PROVIDER_MAP[provider_choice]
170
-
171
- if provider.nil?
161
+ ProviderConfig.list.each_with_index do |provider_id, index|
162
+ provider_meta = ProviderConfig.get(provider_id)
163
+ key_info = provider_meta[:requires_key] ? '' : ' (no API key needed)'
164
+ puts " #{index + 1}. #{provider_meta[:name]}#{key_info}"
165
+ end
166
+ print "Provider (1-#{ProviderConfig.list.size}): "
167
+
168
+ choice = $stdin.gets.chomp
169
+ provider = PROVIDER_MENU[choice]
170
+
171
+ if provider.nil? || !ProviderConfig.valid?(provider)
172
172
  puts 'Invalid provider selection.'
173
173
  return nil
174
174
  end
@@ -179,11 +179,12 @@ module Askcii
179
179
  def get_api_key(provider)
180
180
  return '' if provider == 'ollama'
181
181
 
182
- print "Enter #{provider.capitalize} API key: "
183
- api_key = $stdin.gets.chomp
182
+ provider_name = ProviderConfig.display_name(provider)
183
+ print "Enter #{provider_name} API key: "
184
+ api_key = $stdin.gets.chomp.strip
184
185
 
185
- if api_key.empty?
186
- puts 'API key is required for this provider.'
186
+ if api_key.empty? && ProviderConfig.requires_key?(provider)
187
+ puts "API key is required for #{provider_name}."
187
188
  return nil
188
189
  end
189
190
 
@@ -191,54 +192,24 @@ module Askcii
191
192
  end
192
193
 
193
194
  def get_api_endpoint(provider)
194
- default_endpoint = DEFAULT_ENDPOINTS[provider]
195
+ default_endpoint = ProviderConfig.default_endpoint(provider)
195
196
  print "Enter API endpoint (default: #{default_endpoint}): "
196
- api_endpoint = $stdin.gets.chomp
197
+ api_endpoint = $stdin.gets.chomp.strip
197
198
  api_endpoint.empty? ? default_endpoint : api_endpoint
198
199
  end
199
200
 
200
201
  def get_model_id(provider)
201
- provider_config = PROVIDER_MODELS[provider]
202
-
203
- if provider_config
204
- default_model = provider_config[:default]
205
- available_models = provider_config[:models]
206
-
207
- puts "\nAvailable models for #{provider.capitalize}:"
208
- available_models.each_with_index do |model, index|
209
- marker = model == default_model ? ' (recommended)' : ''
210
- puts " #{index + 1}. #{model}#{marker}"
211
- end
212
-
213
- puts " #{available_models.length + 1}. Enter custom model ID"
214
- print "\nSelect model (1-#{available_models.length + 1}) or press Enter for default [#{default_model}]: "
202
+ examples = ProviderConfig.example_models(provider)
203
+ puts "Example models for #{ProviderConfig.display_name(provider)}: #{examples.join(', ')}"
204
+ print 'Enter model ID: '
205
+ model_id = $stdin.gets.chomp.strip
215
206
 
216
- choice = $stdin.gets.chomp
217
-
218
- if choice.empty?
219
- default_model
220
- elsif choice.to_i.between?(1, available_models.length)
221
- available_models[choice.to_i - 1]
222
- elsif choice.to_i == available_models.length + 1
223
- print 'Enter custom model ID: '
224
- custom_model = $stdin.gets.chomp
225
- custom_model.empty? ? nil : custom_model
226
- else
227
- puts 'Invalid selection.'
228
- nil
229
- end
230
- else
231
- # Fallback for unknown providers
232
- print 'Enter model ID: '
233
- model_id = $stdin.gets.chomp
234
-
235
- if model_id.empty?
236
- puts 'Model ID is required.'
237
- return nil
238
- end
239
-
240
- model_id
207
+ if model_id.empty?
208
+ puts 'Model ID is required.'
209
+ return nil
241
210
  end
211
+
212
+ model_id
242
213
  end
243
214
 
244
215
  def set_default_configuration
@@ -249,14 +220,15 @@ module Askcii
249
220
  return
250
221
  end
251
222
 
223
+ puts
252
224
  print 'Enter configuration ID to set as default: '
253
- new_default = $stdin.gets.chomp
225
+ new_default = $stdin.gets.chomp.strip
254
226
 
255
227
  if configs.any? { |c| c['id'] == new_default }
256
228
  Askcii::Config.set_default_configuration(new_default)
257
- puts "Configuration #{new_default} set as default."
229
+ puts "Configuration #{new_default} set as default."
258
230
  else
259
- puts 'Invalid configuration ID.'
231
+ puts 'Invalid configuration ID.'
260
232
  end
261
233
  end
262
234
 
@@ -268,18 +240,65 @@ module Askcii
268
240
  return
269
241
  end
270
242
 
243
+ puts
271
244
  print 'Enter configuration ID to delete: '
272
- delete_id = $stdin.gets.chomp
245
+ delete_id = $stdin.gets.chomp.strip
273
246
 
274
247
  if configs.any? { |c| c['id'] == delete_id }
248
+ print "Are you sure you want to delete configuration #{delete_id}? (y/n): "
249
+ return unless $stdin.gets.chomp.downcase == 'y'
250
+
275
251
  if Askcii::Config.delete_configuration(delete_id)
276
- puts "Configuration #{delete_id} deleted successfully."
252
+ puts "Configuration #{delete_id} deleted successfully."
277
253
  else
278
- puts 'Failed to delete configuration.'
254
+ puts 'Failed to delete configuration.'
279
255
  end
280
256
  else
281
- puts 'Invalid configuration ID.'
257
+ puts 'Invalid configuration ID.'
282
258
  end
283
259
  end
260
+
261
+ def test_configuration
262
+ configs = Askcii::Config.configurations
263
+
264
+ if configs.empty?
265
+ puts 'No configurations available to test.'
266
+ return
267
+ end
268
+
269
+ puts
270
+ print 'Enter configuration ID to test: '
271
+ config_id = $stdin.gets.chomp.strip
272
+
273
+ config = configs.find { |c| c['id'] == config_id }
274
+
275
+ if config
276
+ test_specific_configuration(config)
277
+ else
278
+ puts '✗ Invalid configuration ID.'
279
+ end
280
+ end
281
+
282
+ def test_specific_configuration(config)
283
+ puts
284
+ puts "Testing configuration: #{config['name']}"
285
+ puts '-' * 50
286
+
287
+ # Validate first
288
+ begin
289
+ symbolized_config = config.transform_keys(&:to_sym)
290
+ ConfigValidator.validate!(symbolized_config)
291
+ puts '✓ Configuration validation passed'
292
+ rescue ConfigurationError => e
293
+ puts "✗ Validation failed: #{e.message}"
294
+ return
295
+ end
296
+
297
+ # TODO: Add actual connectivity test with minimal LLM call
298
+ # For now, just validate the configuration structure
299
+ puts '✓ Configuration appears valid'
300
+ puts
301
+ puts 'Note: Actual connectivity testing will be added in a future update.'
302
+ end
284
303
  end
285
304
  end