aia 0.9.24 → 0.10.2

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +84 -3
  4. data/README.md +179 -59
  5. data/bin/aia +6 -0
  6. data/docs/cli-reference.md +145 -72
  7. data/docs/configuration.md +156 -19
  8. data/docs/examples/tools/index.md +2 -2
  9. data/docs/faq.md +11 -11
  10. data/docs/guides/available-models.md +11 -11
  11. data/docs/guides/basic-usage.md +18 -17
  12. data/docs/guides/chat.md +57 -11
  13. data/docs/guides/executable-prompts.md +15 -15
  14. data/docs/guides/first-prompt.md +2 -2
  15. data/docs/guides/getting-started.md +6 -6
  16. data/docs/guides/image-generation.md +24 -24
  17. data/docs/guides/local-models.md +2 -2
  18. data/docs/guides/models.md +96 -18
  19. data/docs/guides/tools.md +4 -4
  20. data/docs/installation.md +2 -2
  21. data/docs/prompt_management.md +11 -11
  22. data/docs/security.md +3 -3
  23. data/docs/workflows-and-pipelines.md +1 -1
  24. data/examples/README.md +6 -6
  25. data/examples/headlines +3 -3
  26. data/lib/aia/aia_completion.bash +2 -2
  27. data/lib/aia/aia_completion.fish +4 -4
  28. data/lib/aia/aia_completion.zsh +2 -2
  29. data/lib/aia/chat_processor_service.rb +31 -21
  30. data/lib/aia/config/cli_parser.rb +403 -403
  31. data/lib/aia/config/config_section.rb +87 -0
  32. data/lib/aia/config/defaults.yml +219 -0
  33. data/lib/aia/config/defaults_loader.rb +147 -0
  34. data/lib/aia/config/mcp_parser.rb +151 -0
  35. data/lib/aia/config/model_spec.rb +67 -0
  36. data/lib/aia/config/validator.rb +185 -136
  37. data/lib/aia/config.rb +336 -17
  38. data/lib/aia/directive_processor.rb +14 -6
  39. data/lib/aia/directives/configuration.rb +24 -10
  40. data/lib/aia/directives/models.rb +3 -4
  41. data/lib/aia/directives/utility.rb +3 -2
  42. data/lib/aia/directives/web_and_file.rb +50 -47
  43. data/lib/aia/logger.rb +328 -0
  44. data/lib/aia/prompt_handler.rb +18 -22
  45. data/lib/aia/ruby_llm_adapter.rb +572 -69
  46. data/lib/aia/session.rb +9 -8
  47. data/lib/aia/ui_presenter.rb +20 -16
  48. data/lib/aia/utility.rb +50 -18
  49. data/lib/aia.rb +91 -66
  50. data/lib/extensions/ruby_llm/modalities.rb +2 -0
  51. data/mcp_servers/apple-mcp.json +8 -0
  52. data/mcp_servers/mcp_server_chart.json +11 -0
  53. data/mcp_servers/playwright_one.json +8 -0
  54. data/mcp_servers/playwright_two.json +8 -0
  55. data/mcp_servers/tavily_mcp_server.json +8 -0
  56. metadata +83 -25
  57. data/lib/aia/config/base.rb +0 -308
  58. data/lib/aia/config/defaults.rb +0 -91
  59. data/lib/aia/config/file_loader.rb +0 -163
  60. data/mcp_servers/imcp.json +0 -7
  61. data/mcp_servers/launcher.json +0 -11
  62. data/mcp_servers/timeserver.json +0 -8
data/lib/aia/config.rb CHANGED
@@ -1,30 +1,349 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/aia/config.rb
2
4
  #
3
- # This file contains the configuration settings for the AIA application.
4
- # The Config class is responsible for managing configuration settings
5
- # for the AIA application. It provides methods to parse command-line
6
- # arguments, environment variables, and configuration files.
5
+ # AIA Configuration using Anyway Config
6
+ #
7
+ # Schema is defined in lib/aia/config/defaults.yml (single source of truth)
8
+ # Configuration uses nested sections for better organization:
9
+ # - AIA.config.llm.temperature
10
+ # - AIA.config.prompts.dir
11
+ # - AIA.config.models.first.name
12
+ #
13
+ # Configuration sources (lowest to highest priority):
14
+ # 1. Bundled defaults: lib/aia/config/defaults.yml (ships with gem)
15
+ # 2. User config: ~/.aia/config.yml
16
+ # 3. Environment variables (AIA_*)
17
+ # 4. CLI arguments (applied via overrides)
18
+ # 5. Embedded directives (//config)
19
+
20
+ require 'anyway_config'
21
+ require 'yaml'
22
+ require 'date'
7
23
 
8
- require_relative 'config/base'
24
+ require_relative 'config/config_section'
25
+ require_relative 'config/model_spec'
26
+ require_relative 'config/defaults_loader'
27
+ require_relative 'config/mcp_parser'
9
28
 
10
29
  module AIA
11
- class Config
12
- # Delegate all functionality to the modular config system
13
- def self.setup
14
- ConfigModules::Base.setup
30
+ class Config < Anyway::Config
31
+ config_name :aia
32
+ env_prefix :aia
33
+
34
+ # ==========================================================================
35
+ # Schema Definition (loaded from defaults.yml - single source of truth)
36
+ # ==========================================================================
37
+
38
+ DEFAULTS_PATH = File.expand_path('config/defaults.yml', __dir__).freeze
39
+ SCHEMA = Loaders::DefaultsLoader.schema
40
+
41
+ # Nested section attributes (defined as hashes, converted to ConfigSection)
42
+ attr_config :service, :llm, :prompts, :output, :audio, :image, :embedding,
43
+ :tools, :flags, :registry, :paths, :logger
44
+
45
+ # Array/collection attributes
46
+ attr_config :models, :pipeline, :require_libs, :mcp_servers, :context_files
47
+
48
+ # Runtime attributes (not loaded from config files)
49
+ attr_accessor :prompt_id, :stdin_content, :remaining_args, :dump_file,
50
+ :completion, :executable_prompt,
51
+ :executable_prompt_file, :tool_names, :loaded_tools, :next_prompt,
52
+ :log_level_override, :log_file_override,
53
+ :connected_mcp_servers, # Array of successfully connected MCP server names
54
+ :failed_mcp_servers # Array of {name:, error:} hashes for failed MCP servers
55
+
56
+ # Alias for next prompt (for backward compatibility with directives)
57
+ def next
58
+ @next_prompt
59
+ end
60
+
61
+ def next=(value)
62
+ @next_prompt = value
63
+ # Also prepend to pipeline
64
+ pipeline.unshift(value) if value && !value.empty?
65
+ end
66
+
67
+ # ==========================================================================
68
+ # Type Coercion
69
+ # ==========================================================================
70
+
71
+ # Create a coercion that merges incoming value with SCHEMA defaults for a section
72
+ def self.config_section_with_defaults(section_key)
73
+ defaults = SCHEMA[section_key] || {}
74
+ ->(v) {
75
+ return v if v.is_a?(ConfigSection)
76
+ incoming = v || {}
77
+ merged = deep_merge_hashes(defaults.dup, incoming)
78
+ ConfigSection.new(merged)
79
+ }
80
+ end
81
+
82
+ # Deep merge helper for coercion
83
+ def self.deep_merge_hashes(base, overlay)
84
+ base.merge(overlay || {}) do |_key, old_val, new_val|
85
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
86
+ deep_merge_hashes(old_val, new_val)
87
+ else
88
+ new_val.nil? ? old_val : new_val
89
+ end
90
+ end
91
+ end
92
+
93
+ # Convert array of hashes to array of ModelSpec objects
94
+ TO_MODEL_SPECS = ->(v) {
95
+ return [] if v.nil?
96
+ return v if v.is_a?(Array) && v.first.is_a?(ModelSpec)
97
+
98
+ model_counts = Hash.new(0)
99
+
100
+ Array(v).map do |spec|
101
+ # Handle string format from CLI
102
+ if spec.is_a?(String)
103
+ if spec.include?('=')
104
+ name, role = spec.split('=', 2)
105
+ spec = { name: name.strip, role: role.strip }
106
+ else
107
+ spec = { name: spec.strip }
108
+ end
109
+ end
110
+
111
+ spec = spec.transform_keys(&:to_sym) if spec.respond_to?(:transform_keys)
112
+ name = spec[:name]
113
+
114
+ model_counts[name] += 1
115
+ instance = model_counts[name]
116
+
117
+ ModelSpec.new(
118
+ name: name,
119
+ role: spec[:role],
120
+ instance: instance,
121
+ internal_id: instance > 1 ? "#{name}##{instance}" : name
122
+ )
123
+ end
124
+ }
125
+
126
+ coerce_types(
127
+ # Nested sections -> ConfigSection objects (with SCHEMA defaults merged)
128
+ service: config_section_with_defaults(:service),
129
+ llm: config_section_with_defaults(:llm),
130
+ prompts: config_section_with_defaults(:prompts),
131
+ output: config_section_with_defaults(:output),
132
+ audio: config_section_with_defaults(:audio),
133
+ image: config_section_with_defaults(:image),
134
+ embedding: config_section_with_defaults(:embedding),
135
+ tools: config_section_with_defaults(:tools),
136
+ flags: config_section_with_defaults(:flags),
137
+ registry: config_section_with_defaults(:registry),
138
+ paths: config_section_with_defaults(:paths),
139
+
140
+ # Arrays
141
+ models: TO_MODEL_SPECS,
142
+ pipeline: { type: :string, array: true },
143
+ require_libs: { type: :string, array: true },
144
+ context_files: { type: :string, array: true }
145
+ )
146
+
147
+ # ==========================================================================
148
+ # Callbacks
149
+ # ==========================================================================
150
+
151
+ on_load :expand_paths, :ensure_arrays
152
+
153
+ # ==========================================================================
154
+ # Class Methods
155
+ # ==========================================================================
156
+
157
+ class << self
158
+ # Setup configuration with CLI overrides
159
+ #
160
+ # @param cli_overrides [Hash] overrides from CLI parsing
161
+ # @return [Config] configured instance
162
+ def setup(cli_overrides = {})
163
+ new(overrides: cli_overrides)
164
+ end
165
+ end
166
+
167
+ # ==========================================================================
168
+ # Instance Methods
169
+ # ==========================================================================
170
+
171
+ # Mapping of flat CLI keys to their nested config locations
172
+ CLI_TO_NESTED_MAP = {
173
+ # flags section
174
+ chat: [:flags, :chat],
175
+ cost: [:flags, :cost],
176
+ fuzzy: [:flags, :fuzzy],
177
+ tokens: [:flags, :tokens],
178
+ no_mcp: [:flags, :no_mcp],
179
+ terse: [:flags, :terse],
180
+ debug: [:flags, :debug],
181
+ verbose: [:flags, :verbose],
182
+ consensus: [:flags, :consensus],
183
+ # llm section
184
+ adapter: [:llm, :adapter],
185
+ temperature: [:llm, :temperature],
186
+ max_tokens: [:llm, :max_tokens],
187
+ top_p: [:llm, :top_p],
188
+ frequency_penalty: [:llm, :frequency_penalty],
189
+ presence_penalty: [:llm, :presence_penalty],
190
+ # prompts section
191
+ prompts_dir: [:prompts, :dir],
192
+ roles_prefix: [:prompts, :roles_prefix],
193
+ role: [:prompts, :role],
194
+ parameter_regex: [:prompts, :parameter_regex],
195
+ system_prompt: [:prompts, :system_prompt],
196
+ # output section
197
+ output: [:output, :file],
198
+ history_file: [:output, :history_file],
199
+ append: [:output, :append],
200
+ markdown: [:output, :markdown],
201
+ # audio section
202
+ speak: [:audio, :speak],
203
+ voice: [:audio, :voice],
204
+ speech_model: [:audio, :speech_model],
205
+ transcription_model: [:audio, :transcription_model],
206
+ # image section
207
+ image_size: [:image, :size],
208
+ image_quality: [:image, :quality],
209
+ image_style: [:image, :style],
210
+ # tools section
211
+ tool_paths: [:tools, :paths],
212
+ allowed_tools: [:tools, :allowed],
213
+ rejected_tools: [:tools, :rejected],
214
+ # registry section
215
+ refresh: [:registry, :refresh],
216
+ # paths section
217
+ extra_config_file: [:paths, :extra_config_file]
218
+ }.freeze
219
+
220
+ def initialize(overrides: {})
221
+ super()
222
+ apply_overrides(overrides) if overrides && !overrides.empty?
223
+ process_mcp_files(overrides[:mcp_files]) if overrides[:mcp_files]
15
224
  end
16
225
 
17
- # Maintain backward compatibility by delegating to Base module
18
- def self.method_missing(method_name, *args, &block)
19
- if ConfigModules::Base.respond_to?(method_name)
20
- ConfigModules::Base.send(method_name, *args, &block)
21
- else
22
- super
226
+ # Apply CLI or runtime overrides to configuration
227
+ #
228
+ # @param overrides [Hash] key-value pairs to override
229
+ def apply_overrides(overrides)
230
+ overrides.each do |key, value|
231
+ next if value.nil?
232
+
233
+ key_sym = key.to_sym
234
+
235
+ # Check if this is a flat CLI key that maps to a nested location
236
+ if CLI_TO_NESTED_MAP.key?(key_sym)
237
+ section, nested_key = CLI_TO_NESTED_MAP[key_sym]
238
+ section_obj = send(section)
239
+ section_obj.send("#{nested_key}=", value) if section_obj.respond_to?("#{nested_key}=")
240
+ elsif respond_to?("#{key}=")
241
+ send("#{key}=", value)
242
+ elsif key.to_s.include?('__')
243
+ # Handle nested keys like 'llm__temperature'
244
+ parts = key.to_s.split('__')
245
+ apply_nested_override(parts, value)
246
+ end
23
247
  end
24
248
  end
25
249
 
26
- def self.respond_to_missing?(method_name, include_private = false)
27
- ConfigModules::Base.respond_to?(method_name, include_private) || super
250
+ # Convert config to hash (for dump, etc.)
251
+ def to_h
252
+ {
253
+ service: service.to_h,
254
+ llm: llm.to_h,
255
+ models: models.map(&:to_h),
256
+ prompts: prompts.to_h,
257
+ output: output.to_h,
258
+ audio: audio.to_h,
259
+ image: image.to_h,
260
+ embedding: embedding.to_h,
261
+ tools: tools.to_h,
262
+ flags: flags.to_h,
263
+ registry: registry.to_h,
264
+ paths: paths.to_h,
265
+ pipeline: pipeline,
266
+ require_libs: require_libs,
267
+ mcp_servers: mcp_servers,
268
+ context_files: context_files
269
+ }
270
+ end
271
+
272
+ private
273
+
274
+ def expand_paths
275
+ # Expand ~ in paths
276
+ if paths.aia_dir
277
+ paths.aia_dir = File.expand_path(paths.aia_dir)
278
+ end
279
+
280
+ if paths.config_file
281
+ paths.config_file = File.expand_path(paths.config_file)
282
+ end
283
+
284
+ if prompts.dir
285
+ prompts.dir = File.expand_path(prompts.dir)
286
+ end
287
+
288
+ if prompts.roles_dir
289
+ prompts.roles_dir = File.expand_path(prompts.roles_dir)
290
+ end
291
+
292
+ if output.history_file
293
+ output.history_file = File.expand_path(output.history_file)
294
+ end
295
+ end
296
+
297
+ def ensure_arrays
298
+ # Ensure array fields are actually arrays
299
+ self.pipeline = [] if pipeline.nil?
300
+ self.require_libs = [] if require_libs.nil?
301
+ self.context_files = [] if context_files.nil?
302
+ self.mcp_servers = [] if mcp_servers.nil?
303
+
304
+ # Ensure tools.paths is an array
305
+ tools.paths = [] if tools.paths.nil?
306
+ end
307
+
308
+ # Process MCP JSON files and merge servers into mcp_servers
309
+ #
310
+ # @param mcp_files [Array<String>] paths to MCP JSON configuration files
311
+ def process_mcp_files(mcp_files)
312
+ return if mcp_files.nil? || mcp_files.empty?
313
+
314
+ servers_from_files = McpParser.parse_files(mcp_files)
315
+ return if servers_from_files.empty?
316
+
317
+ # Merge with existing mcp_servers (CLI files take precedence)
318
+ self.mcp_servers = (mcp_servers || []) + servers_from_files
319
+ end
320
+
321
+ def apply_nested_override(parts, value)
322
+ section = parts[0].to_sym
323
+ key = parts[1].to_sym
324
+
325
+ case section
326
+ when :llm
327
+ llm.send("#{key}=", value) if llm.respond_to?("#{key}=")
328
+ when :prompts
329
+ prompts.send("#{key}=", value) if prompts.respond_to?("#{key}=")
330
+ when :output
331
+ output.send("#{key}=", value) if output.respond_to?("#{key}=")
332
+ when :audio
333
+ audio.send("#{key}=", value) if audio.respond_to?("#{key}=")
334
+ when :image
335
+ image.send("#{key}=", value) if image.respond_to?("#{key}=")
336
+ when :embedding
337
+ embedding.send("#{key}=", value) if embedding.respond_to?("#{key}=")
338
+ when :tools
339
+ tools.send("#{key}=", value) if tools.respond_to?("#{key}=")
340
+ when :flags
341
+ flags.send("#{key}=", value) if flags.respond_to?("#{key}=")
342
+ when :registry
343
+ registry.send("#{key}=", value) if registry.respond_to?("#{key}=")
344
+ when :paths
345
+ paths.send("#{key}=", value) if paths.respond_to?("#{key}=")
346
+ end
28
347
  end
29
348
  end
30
349
  end
@@ -1,6 +1,6 @@
1
1
  # lib/aia/directive_processor.rb
2
2
 
3
- require 'active_support/all'
3
+ # require 'active_support/all'
4
4
  require 'faraday'
5
5
  require 'word_wrapper'
6
6
  require_relative 'directives/registry'
@@ -9,7 +9,7 @@ module AIA
9
9
  class DirectiveProcessor
10
10
  using Refinements
11
11
 
12
- EXCLUDED_METHODS = %w[ run initialize private? ]
12
+ EXCLUDED_METHODS = %w[run initialize private?]
13
13
 
14
14
  def initialize
15
15
  @prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size
@@ -17,18 +17,24 @@ module AIA
17
17
  Directives::WebAndFile.included_files = @included_files
18
18
  end
19
19
 
20
+
20
21
  def directive?(string)
21
22
  Directives::Registry.directive?(string)
22
23
  end
23
24
 
25
+
24
26
  def process(string, context_manager)
25
27
  return string unless directive?(string)
26
28
 
27
29
  content = if string.is_a?(RubyLLM::Message)
28
- string.content rescue string.to_s
29
- else
30
- string.to_s
31
- end
30
+ begin
31
+ string.content
32
+ rescue StandardError
33
+ string.to_s
34
+ end
35
+ else
36
+ string.to_s
37
+ end
32
38
 
33
39
  key = content.strip
34
40
  sans_prefix = key[@prefix_size..]
@@ -38,6 +44,7 @@ module AIA
38
44
  Directives::Registry.process(method_name, args, context_manager)
39
45
  end
40
46
 
47
+
41
48
  def run(directives)
42
49
  return {} if directives.nil? || directives.empty?
43
50
 
@@ -76,6 +83,7 @@ module AIA
76
83
  end
77
84
  end
78
85
 
86
+
79
87
  def respond_to_missing?(method_name, include_private = false)
80
88
  Directives::Registry.respond_to?(method_name, include_private) || super
81
89
  end
@@ -7,12 +7,17 @@ module AIA
7
7
  args = Array(args)
8
8
 
9
9
  if args.empty?
10
- ap AIA.config
10
+ ap AIA.config.to_h
11
11
  ""
12
12
  elsif args.length == 1
13
13
  config_item = args.first
14
- local_cfg = Hash.new
15
- local_cfg[config_item] = AIA.config[config_item]
14
+ local_cfg = {}
15
+ # Use method-based access for Anyway::Config
16
+ if AIA.config.respond_to?(config_item)
17
+ local_cfg[config_item] = AIA.config.send(config_item)
18
+ else
19
+ local_cfg[config_item] = nil
20
+ end
16
21
  ap local_cfg
17
22
  ""
18
23
  else
@@ -24,7 +29,13 @@ module AIA
24
29
  new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
25
30
  end
26
31
 
27
- AIA.config[config_item] = new_value
32
+ # Use method-based setter for Anyway::Config
33
+ setter = "#{config_item}="
34
+ if AIA.config.respond_to?(setter)
35
+ AIA.config.send(setter, new_value)
36
+ else
37
+ puts "Warning: Unknown config option '#{config_item}'"
38
+ end
28
39
  ""
29
40
  end
30
41
  end
@@ -33,29 +44,32 @@ module AIA
33
44
  if args.empty?
34
45
  # Display details for all configured models
35
46
  puts
36
- models = Array(AIA.config.model)
47
+ models = AIA.config.models
37
48
 
38
49
  if models.size == 1
39
50
  puts "Current Model:"
40
51
  puts "=============="
41
- puts AIA.config.client.model.to_h.pretty_inspect
52
+ puts AIA.client.model.to_h.pretty_inspect
42
53
  else
43
54
  puts "Multi-Model Configuration:"
44
55
  puts "=========================="
45
56
  puts "Model count: #{models.size}"
46
- puts "Primary model: #{models.first} (used for consensus when --consensus flag is enabled)"
47
- puts "Consensus mode: #{AIA.config.consensus.nil? ? 'auto-detect (disabled by default)' : AIA.config.consensus}"
57
+ first_model = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
58
+ puts "Primary model: #{first_model} (used for consensus when --consensus flag is enabled)"
59
+ consensus = AIA.config.flags.consensus
60
+ puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
48
61
  puts
49
62
  puts "Model Details:"
50
63
  puts "-" * 50
51
64
 
52
- models.each_with_index do |model_name, index|
65
+ models.each_with_index do |model_spec, index|
66
+ model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
53
67
  puts "#{index + 1}. #{model_name}#{index == 0 ? ' (primary)' : ''}"
54
68
 
55
69
  # Try to get model details if available
56
70
  begin
57
71
  # Access the model details from RubyLLM's model registry
58
- model_info = RubyLLM::Models.find(name: model_name)
72
+ model_info = RubyLLM::Models.find(model_name)
59
73
  if model_info
60
74
  puts " Provider: #{model_info.provider || 'Unknown'}"
61
75
  puts " Context window: #{model_info.context_window || 'Unknown'}"
@@ -31,12 +31,11 @@ module AIA
31
31
 
32
32
  def self.available_models(args = nil, context_manager = nil)
33
33
  # Check if we're using a local provider
34
- current_models = AIA.config.model
35
- current_models = [current_models] if current_models.is_a?(String)
34
+ current_models = AIA.config.models
36
35
 
37
- # Extract model names (handles both String and Hash formats)
36
+ # Extract model names (handles ModelSpec objects)
38
37
  model_names = current_models.map do |m|
39
- m.is_a?(Hash) ? m[:model] : m
38
+ m.respond_to?(:name) ? m.name : m.to_s
40
39
  end
41
40
 
42
41
  using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }
@@ -14,10 +14,11 @@ module AIA
14
14
  width = TTY::Screen.width - indent - 2
15
15
  filter = args.first&.downcase
16
16
 
17
- if AIA.config.tools.empty?
17
+ loaded_tools = AIA.config.loaded_tools || []
18
+ if loaded_tools.empty?
18
19
  puts "No tools are available"
19
20
  else
20
- tools_to_display = AIA.config.tools
21
+ tools_to_display = loaded_tools
21
22
 
22
23
  if filter
23
24
  tools_to_display = tools_to_display.select do |tool|
@@ -1,7 +1,7 @@
1
1
  # lib/aia/directives/web_and_file.rb
2
2
 
3
3
  require 'faraday'
4
- require 'active_support/all'
4
+ # require 'active_support/all'
5
5
  require 'clipboard'
6
6
 
7
7
  module AIA
@@ -9,73 +9,76 @@ module AIA
9
9
  module WebAndFile
10
10
  PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
11
11
 
12
- def self.webpage(args, context_manager = nil)
13
- if PUREMD_API_KEY.nil?
14
- "ERROR: PUREMD_API_KEY is required in order to include a webpage"
12
+ def self.webpage(args, _context_manager = nil)
13
+ if PUREMD_API_KEY.nil?
14
+ 'ERROR: PUREMD_API_KEY is required in order to include a webpage'
15
+ else
16
+ url = `echo #{args.shift}`.strip
17
+ puremd_url = "https://pure.md/#{url}"
18
+
19
+ response = Faraday.get(puremd_url) do |req|
20
+ req.headers['x-puremd-api-token'] = PUREMD_API_KEY
21
+ end
22
+
23
+ if 200 == response.status
24
+ response.body
15
25
  else
16
- url = `echo #{args.shift}`.strip
17
- puremd_url = "https://pure.md/#{url}"
18
-
19
- response = Faraday.get(puremd_url) do |req|
20
- req.headers['x-puremd-api-token'] = PUREMD_API_KEY
21
- end
22
-
23
- if 200 == response.status
24
- response.body
25
- else
26
- "Error: Status was #{response.status}\n#{ap response}"
27
- end
26
+ "Error: Status was #{response.status}\n#{ap response}"
28
27
  end
29
28
  end
29
+ end
30
30
 
31
- def self.include(args, context_manager = nil)
32
- # echo takes care of envars and tilde expansion
33
- file_path = `echo #{args.shift}`.strip
34
31
 
35
- if file_path.start_with?(/http?:\/\//)
36
- webpage(args)
37
- else
38
- include_file(file_path)
39
- end
32
+ def self.include(args, _context_manager = nil)
33
+ # echo takes care of envars and tilde expansion
34
+ file_path = `echo #{args.shift}`.strip
35
+
36
+ if file_path.start_with?(%r{http?://})
37
+ webpage(args)
38
+ else
39
+ include_file(file_path)
40
40
  end
41
+ end
41
42
 
42
- def self.include_file(file_path)
43
- @included_files ||= []
44
- if @included_files.include?(file_path)
45
- ""
46
- else
47
- if File.exist?(file_path) && File.readable?(file_path)
48
- @included_files << file_path
49
- File.read(file_path)
50
- else
51
- "Error: File '#{file_path}' is not accessible"
52
- end
53
- end
43
+
44
+ def self.include_file(file_path)
45
+ @included_files ||= []
46
+ if @included_files.include?(file_path)
47
+ ''
48
+ elsif File.exist?(file_path) && File.readable?(file_path)
49
+ @included_files << file_path
50
+ File.read(file_path)
51
+ else
52
+ "Error: File '#{file_path}' is not accessible"
54
53
  end
54
+ end
55
+
55
56
 
56
57
  def self.included_files
57
58
  @included_files ||= []
58
59
  end
59
60
 
61
+
60
62
  def self.included_files=(files)
61
63
  @included_files = files
62
64
  end
63
65
 
64
- def self.paste(args = [], context_manager = nil)
65
- begin
66
- content = Clipboard.paste
67
- content.to_s
68
- rescue => e
69
- "Error: Unable to paste from clipboard - #{e.message}"
70
- end
66
+
67
+ def self.paste(_args = [], _context_manager = nil)
68
+
69
+ content = Clipboard.paste
70
+ content.to_s
71
+ rescue StandardError => e
72
+ "Error: Unable to paste from clipboard - #{e.message}"
73
+
71
74
  end
72
75
 
73
76
  # Set up aliases - these work on the module's singleton class
74
77
  class << self
75
- alias_method :website, :webpage
76
- alias_method :web, :webpage
77
- alias_method :import, :include
78
- alias_method :clipboard, :paste
78
+ alias website webpage
79
+ alias web webpage
80
+ alias import include
81
+ alias clipboard paste
79
82
  end
80
83
  end
81
84
  end