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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +84 -3
- data/README.md +179 -59
- data/bin/aia +6 -0
- data/docs/cli-reference.md +145 -72
- data/docs/configuration.md +156 -19
- data/docs/examples/tools/index.md +2 -2
- data/docs/faq.md +11 -11
- data/docs/guides/available-models.md +11 -11
- data/docs/guides/basic-usage.md +18 -17
- data/docs/guides/chat.md +57 -11
- data/docs/guides/executable-prompts.md +15 -15
- data/docs/guides/first-prompt.md +2 -2
- data/docs/guides/getting-started.md +6 -6
- data/docs/guides/image-generation.md +24 -24
- data/docs/guides/local-models.md +2 -2
- data/docs/guides/models.md +96 -18
- data/docs/guides/tools.md +4 -4
- data/docs/installation.md +2 -2
- data/docs/prompt_management.md +11 -11
- data/docs/security.md +3 -3
- data/docs/workflows-and-pipelines.md +1 -1
- data/examples/README.md +6 -6
- data/examples/headlines +3 -3
- data/lib/aia/aia_completion.bash +2 -2
- data/lib/aia/aia_completion.fish +4 -4
- data/lib/aia/aia_completion.zsh +2 -2
- data/lib/aia/chat_processor_service.rb +31 -21
- data/lib/aia/config/cli_parser.rb +403 -403
- data/lib/aia/config/config_section.rb +87 -0
- data/lib/aia/config/defaults.yml +219 -0
- data/lib/aia/config/defaults_loader.rb +147 -0
- data/lib/aia/config/mcp_parser.rb +151 -0
- data/lib/aia/config/model_spec.rb +67 -0
- data/lib/aia/config/validator.rb +185 -136
- data/lib/aia/config.rb +336 -17
- data/lib/aia/directive_processor.rb +14 -6
- data/lib/aia/directives/configuration.rb +24 -10
- data/lib/aia/directives/models.rb +3 -4
- data/lib/aia/directives/utility.rb +3 -2
- data/lib/aia/directives/web_and_file.rb +50 -47
- data/lib/aia/logger.rb +328 -0
- data/lib/aia/prompt_handler.rb +18 -22
- data/lib/aia/ruby_llm_adapter.rb +572 -69
- data/lib/aia/session.rb +9 -8
- data/lib/aia/ui_presenter.rb +20 -16
- data/lib/aia/utility.rb +50 -18
- data/lib/aia.rb +91 -66
- data/lib/extensions/ruby_llm/modalities.rb +2 -0
- data/mcp_servers/apple-mcp.json +8 -0
- data/mcp_servers/mcp_server_chart.json +11 -0
- data/mcp_servers/playwright_one.json +8 -0
- data/mcp_servers/playwright_two.json +8 -0
- data/mcp_servers/tavily_mcp_server.json +8 -0
- metadata +83 -25
- data/lib/aia/config/base.rb +0 -308
- data/lib/aia/config/defaults.rb +0 -91
- data/lib/aia/config/file_loader.rb +0 -163
- data/mcp_servers/imcp.json +0 -7
- data/mcp_servers/launcher.json +0 -11
- data/mcp_servers/timeserver.json +0 -8
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/config/config_section.rb
|
|
4
|
+
#
|
|
5
|
+
# ConfigSection provides method access to nested configuration hashes.
|
|
6
|
+
# This allows dot-notation access like: config.llm.temperature
|
|
7
|
+
# instead of: config[:llm][:temperature]
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
class ConfigSection
|
|
11
|
+
def initialize(hash = {})
|
|
12
|
+
@data = {}
|
|
13
|
+
(hash || {}).each do |key, value|
|
|
14
|
+
@data[key.to_sym] = value.is_a?(Hash) ? ConfigSection.new(value) : value
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(method, *args, &block)
|
|
19
|
+
key = method.to_s
|
|
20
|
+
if key.end_with?('=')
|
|
21
|
+
@data[key.chomp('=').to_sym] = args.first
|
|
22
|
+
elsif @data.key?(method)
|
|
23
|
+
@data[method]
|
|
24
|
+
else
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def respond_to_missing?(method, include_private = false)
|
|
30
|
+
key = method.to_s.chomp('=').to_sym
|
|
31
|
+
@data.key?(key) || super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
@data.transform_values do |v|
|
|
36
|
+
v.is_a?(ConfigSection) ? v.to_h : v
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def [](key)
|
|
41
|
+
@data[key.to_sym]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def []=(key, value)
|
|
45
|
+
@data[key.to_sym] = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge(other)
|
|
49
|
+
other_hash = other.is_a?(ConfigSection) ? other.to_h : other
|
|
50
|
+
ConfigSection.new(deep_merge(to_h, other_hash || {}))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def keys
|
|
54
|
+
@data.keys
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def values
|
|
58
|
+
@data.values
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each(&block)
|
|
62
|
+
@data.each(&block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def empty?
|
|
66
|
+
@data.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def key?(key)
|
|
70
|
+
@data.key?(key.to_sym)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
alias has_key? key?
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def deep_merge(base, overlay)
|
|
78
|
+
base.merge(overlay) do |_key, old_val, new_val|
|
|
79
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
80
|
+
deep_merge(old_val, new_val)
|
|
81
|
+
else
|
|
82
|
+
new_val
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# AIA Configuration Schema and Defaults
|
|
2
|
+
#
|
|
3
|
+
# This file is the SINGLE SOURCE OF TRUTH for AIA configuration.
|
|
4
|
+
# All attributes must be declared here to be recognized.
|
|
5
|
+
# It is bundled with the gem and loaded automatically at lowest priority.
|
|
6
|
+
#
|
|
7
|
+
# Loading priority (lowest to highest):
|
|
8
|
+
# 1. This file (bundled defaults)
|
|
9
|
+
# 2. User config (~/.aia/config.yml)
|
|
10
|
+
# 3. Environment variables (AIA_*)
|
|
11
|
+
# 4. CLI arguments
|
|
12
|
+
# 5. Embedded directives (//config)
|
|
13
|
+
#
|
|
14
|
+
# Environment Variable Naming:
|
|
15
|
+
# Use double underscore for nested keys:
|
|
16
|
+
# AIA_LLM__TEMPERATURE=0.5
|
|
17
|
+
# AIA_PROMPTS__DIR=~/my-prompts
|
|
18
|
+
# AIA_FLAGS__DEBUG=true
|
|
19
|
+
|
|
20
|
+
service:
|
|
21
|
+
name: aia
|
|
22
|
+
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
# LLM Configuration
|
|
25
|
+
# Access: AIA.config.llm.adapter, AIA.config.llm.temperature, etc.
|
|
26
|
+
# Env: AIA_LLM__ADAPTER, AIA_LLM__TEMPERATURE, etc.
|
|
27
|
+
# -----------------------------------------------------------------------------
|
|
28
|
+
llm:
|
|
29
|
+
adapter: ruby_llm
|
|
30
|
+
temperature: 0.7
|
|
31
|
+
max_tokens: 2048
|
|
32
|
+
top_p: 1.0
|
|
33
|
+
frequency_penalty: 0.0
|
|
34
|
+
presence_penalty: 0.0
|
|
35
|
+
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
# Models Configuration
|
|
38
|
+
# Access: AIA.config.models (array of ModelSpec objects)
|
|
39
|
+
# Each model has: name, role, instance, internal_id
|
|
40
|
+
#
|
|
41
|
+
# Example YAML:
|
|
42
|
+
# models:
|
|
43
|
+
# - name: gpt-4o
|
|
44
|
+
# role: architect
|
|
45
|
+
# - name: claude-3-opus
|
|
46
|
+
# role: reviewer
|
|
47
|
+
#
|
|
48
|
+
# Example CLI: --model "gpt-4o=architect,claude-3-opus=reviewer"
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
models:
|
|
51
|
+
- name: gpt-4o-mini
|
|
52
|
+
role: ~
|
|
53
|
+
|
|
54
|
+
# -----------------------------------------------------------------------------
|
|
55
|
+
# Prompts Configuration
|
|
56
|
+
# Access: AIA.config.prompts.dir, AIA.config.prompts.roles_prefix, etc.
|
|
57
|
+
# Env: AIA_PROMPTS__DIR, AIA_PROMPTS__ROLES_PREFIX, etc.
|
|
58
|
+
# -----------------------------------------------------------------------------
|
|
59
|
+
prompts:
|
|
60
|
+
dir: ~/.prompts
|
|
61
|
+
extname: .txt
|
|
62
|
+
roles_prefix: roles
|
|
63
|
+
roles_dir: ~/.prompts/roles
|
|
64
|
+
role: ~
|
|
65
|
+
system_prompt: ~
|
|
66
|
+
parameter_regex: ~
|
|
67
|
+
|
|
68
|
+
# -----------------------------------------------------------------------------
|
|
69
|
+
# Output Configuration
|
|
70
|
+
# Access: AIA.config.output.file, AIA.config.output.append, etc.
|
|
71
|
+
# Env: AIA_OUTPUT__FILE, AIA_OUTPUT__APPEND, etc.
|
|
72
|
+
# -----------------------------------------------------------------------------
|
|
73
|
+
output:
|
|
74
|
+
file: temp.md
|
|
75
|
+
append: false
|
|
76
|
+
markdown: true
|
|
77
|
+
history_file: ~/.prompts/_prompts.log
|
|
78
|
+
|
|
79
|
+
# -----------------------------------------------------------------------------
|
|
80
|
+
# Audio Configuration
|
|
81
|
+
# Access: AIA.config.audio.voice, AIA.config.audio.speak_command, etc.
|
|
82
|
+
# Env: AIA_AUDIO__VOICE, AIA_AUDIO__SPEAK_COMMAND, etc.
|
|
83
|
+
# -----------------------------------------------------------------------------
|
|
84
|
+
audio:
|
|
85
|
+
voice: alloy
|
|
86
|
+
speak_command: afplay
|
|
87
|
+
speech_model: tts-1
|
|
88
|
+
transcription_model: whisper-1
|
|
89
|
+
|
|
90
|
+
# -----------------------------------------------------------------------------
|
|
91
|
+
# Image Configuration
|
|
92
|
+
# Access: AIA.config.image.model, AIA.config.image.size, etc.
|
|
93
|
+
# Env: AIA_IMAGE__MODEL, AIA_IMAGE__SIZE, etc.
|
|
94
|
+
# -----------------------------------------------------------------------------
|
|
95
|
+
image:
|
|
96
|
+
model: dall-e-3
|
|
97
|
+
size: 1024x1024
|
|
98
|
+
quality: standard
|
|
99
|
+
style: vivid
|
|
100
|
+
|
|
101
|
+
# -----------------------------------------------------------------------------
|
|
102
|
+
# Embedding Configuration
|
|
103
|
+
# Access: AIA.config.embedding.model
|
|
104
|
+
# Env: AIA_EMBEDDING__MODEL
|
|
105
|
+
# -----------------------------------------------------------------------------
|
|
106
|
+
embedding:
|
|
107
|
+
model: text-embedding-ada-002
|
|
108
|
+
|
|
109
|
+
# -----------------------------------------------------------------------------
|
|
110
|
+
# Tools Configuration
|
|
111
|
+
# Access: AIA.config.tools.paths, AIA.config.tools.allowed, etc.
|
|
112
|
+
# Env: AIA_TOOLS__PATHS, AIA_TOOLS__ALLOWED, etc.
|
|
113
|
+
# -----------------------------------------------------------------------------
|
|
114
|
+
tools:
|
|
115
|
+
paths: []
|
|
116
|
+
allowed: ~
|
|
117
|
+
rejected: ~
|
|
118
|
+
|
|
119
|
+
# -----------------------------------------------------------------------------
|
|
120
|
+
# Flags (Boolean Options)
|
|
121
|
+
# Access: AIA.config.flags.chat, AIA.config.flags.debug, etc.
|
|
122
|
+
# Env: AIA_FLAGS__CHAT=true, AIA_FLAGS__DEBUG=true, etc.
|
|
123
|
+
# -----------------------------------------------------------------------------
|
|
124
|
+
flags:
|
|
125
|
+
chat: false
|
|
126
|
+
cost: false
|
|
127
|
+
debug: false
|
|
128
|
+
verbose: false
|
|
129
|
+
fuzzy: false
|
|
130
|
+
tokens: false
|
|
131
|
+
no_mcp: false
|
|
132
|
+
speak: false
|
|
133
|
+
terse: false
|
|
134
|
+
shell: true
|
|
135
|
+
erb: true
|
|
136
|
+
clear: false
|
|
137
|
+
consensus: false
|
|
138
|
+
|
|
139
|
+
# -----------------------------------------------------------------------------
|
|
140
|
+
# Logger Configuration
|
|
141
|
+
# Access: AIA.config.logger.aia.file, AIA.config.logger.llm.level, etc.
|
|
142
|
+
# Env: AIA_LOGGER__AIA__FILE, AIA_LOGGER__LLM__LEVEL, etc.
|
|
143
|
+
#
|
|
144
|
+
# Configures logging for three systems:
|
|
145
|
+
# aia: AIA application logging
|
|
146
|
+
# llm: RubyLLM gem logging
|
|
147
|
+
# mcp: RubyLLM::MCP gem logging
|
|
148
|
+
#
|
|
149
|
+
# file: STDOUT, STDERR, or a file path (e.g., ~/.aia/aia.log)
|
|
150
|
+
# All three loggers can write to the same file safely.
|
|
151
|
+
# level: debug, info, warn, error, fatal
|
|
152
|
+
# flush: true = immediate write (no buffering), false = buffered writes
|
|
153
|
+
# -----------------------------------------------------------------------------
|
|
154
|
+
logger:
|
|
155
|
+
aia:
|
|
156
|
+
file: STDOUT
|
|
157
|
+
level: warn
|
|
158
|
+
flush: true
|
|
159
|
+
llm:
|
|
160
|
+
file: STDOUT
|
|
161
|
+
level: warn
|
|
162
|
+
flush: true
|
|
163
|
+
mcp:
|
|
164
|
+
file: STDOUT
|
|
165
|
+
level: warn
|
|
166
|
+
flush: true
|
|
167
|
+
|
|
168
|
+
# -----------------------------------------------------------------------------
|
|
169
|
+
# Pipeline/Workflow Configuration
|
|
170
|
+
# Access: AIA.config.pipeline (array of prompt IDs)
|
|
171
|
+
# -----------------------------------------------------------------------------
|
|
172
|
+
pipeline: []
|
|
173
|
+
|
|
174
|
+
# -----------------------------------------------------------------------------
|
|
175
|
+
# Model Registry Configuration
|
|
176
|
+
# Access: AIA.config.registry.refresh
|
|
177
|
+
# Env: AIA_REGISTRY__REFRESH
|
|
178
|
+
# Note: last_refresh is derived from models.json file modification time
|
|
179
|
+
# -----------------------------------------------------------------------------
|
|
180
|
+
registry:
|
|
181
|
+
refresh: 7 # days between refreshes (0 = disable periodic refresh)
|
|
182
|
+
|
|
183
|
+
# -----------------------------------------------------------------------------
|
|
184
|
+
# Required Ruby Libraries
|
|
185
|
+
# Access: AIA.config.require_libs (array)
|
|
186
|
+
# -----------------------------------------------------------------------------
|
|
187
|
+
require_libs: []
|
|
188
|
+
|
|
189
|
+
# -----------------------------------------------------------------------------
|
|
190
|
+
# MCP Servers Configuration
|
|
191
|
+
# Access: AIA.config.mcp_servers (array of server configs)
|
|
192
|
+
#
|
|
193
|
+
# Example:
|
|
194
|
+
# mcp_servers:
|
|
195
|
+
# - name: my-server
|
|
196
|
+
# command: /path/to/server
|
|
197
|
+
# args: []
|
|
198
|
+
# env: {}
|
|
199
|
+
# timeout: 8000
|
|
200
|
+
# -----------------------------------------------------------------------------
|
|
201
|
+
mcp_servers: []
|
|
202
|
+
|
|
203
|
+
# -----------------------------------------------------------------------------
|
|
204
|
+
# Paths Configuration
|
|
205
|
+
# Access: AIA.config.paths.aia_dir, AIA.config.paths.config_file
|
|
206
|
+
# Env: AIA_PATHS__AIA_DIR, AIA_PATHS__CONFIG_FILE
|
|
207
|
+
#
|
|
208
|
+
# Note: anyway_config uses XDG Base Directory Specification by default.
|
|
209
|
+
# User config is loaded from ~/.config/aia/aia.yml
|
|
210
|
+
# -----------------------------------------------------------------------------
|
|
211
|
+
paths:
|
|
212
|
+
aia_dir: ~/.config/aia
|
|
213
|
+
config_file: ~/.config/aia/aia.yml
|
|
214
|
+
|
|
215
|
+
# -----------------------------------------------------------------------------
|
|
216
|
+
# Context Files (set at runtime)
|
|
217
|
+
# Access: AIA.config.context_files (array of file paths)
|
|
218
|
+
# -----------------------------------------------------------------------------
|
|
219
|
+
context_files: []
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/config/defaults_loader.rb
|
|
4
|
+
#
|
|
5
|
+
# Configuration Loaders for Anyway Config
|
|
6
|
+
#
|
|
7
|
+
# Provides two custom loaders:
|
|
8
|
+
# 1. DefaultsLoader - Loads bundled defaults from defaults.yml
|
|
9
|
+
# 2. UserConfigLoader - Loads user config from ~/.config/aia/aia.yml
|
|
10
|
+
#
|
|
11
|
+
# Loading priority (lowest to highest):
|
|
12
|
+
# 1. Bundled defaults (DefaultsLoader)
|
|
13
|
+
# 2. User config (UserConfigLoader)
|
|
14
|
+
# 3. Environment variables (AIA_*)
|
|
15
|
+
# 4. CLI arguments (applied separately)
|
|
16
|
+
|
|
17
|
+
require 'anyway_config'
|
|
18
|
+
require 'yaml'
|
|
19
|
+
|
|
20
|
+
module AIA
|
|
21
|
+
module Loaders
|
|
22
|
+
# Loads bundled default configuration values from defaults.yml
|
|
23
|
+
class DefaultsLoader < Anyway::Loaders::Base
|
|
24
|
+
DEFAULTS_PATH = File.expand_path('../defaults.yml', __FILE__).freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Returns the path to the bundled defaults file
|
|
28
|
+
#
|
|
29
|
+
# @return [String] path to defaults.yml
|
|
30
|
+
def defaults_path
|
|
31
|
+
DEFAULTS_PATH
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if defaults file exists
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def defaults_exist?
|
|
38
|
+
File.exist?(DEFAULTS_PATH)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Load and parse the raw YAML content
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] parsed YAML with symbolized keys
|
|
44
|
+
def load_raw_yaml
|
|
45
|
+
return {} unless defaults_exist?
|
|
46
|
+
|
|
47
|
+
content = File.read(defaults_path)
|
|
48
|
+
YAML.safe_load(
|
|
49
|
+
content,
|
|
50
|
+
permitted_classes: [Symbol, Date],
|
|
51
|
+
symbolize_names: true,
|
|
52
|
+
aliases: true
|
|
53
|
+
) || {}
|
|
54
|
+
rescue Psych::SyntaxError => e
|
|
55
|
+
warn "AIA: Failed to parse bundled defaults #{defaults_path}: #{e.message}"
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the schema (all configuration keys and their defaults)
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] the complete defaults
|
|
62
|
+
def schema
|
|
63
|
+
load_raw_yaml
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get a list of all top-level configuration sections
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Symbol>] list of section names
|
|
69
|
+
def sections
|
|
70
|
+
schema.keys
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Called by Anyway Config to load configuration
|
|
75
|
+
#
|
|
76
|
+
# @param name [Symbol] the config name (unused, always :aia)
|
|
77
|
+
# @return [Hash] configuration hash
|
|
78
|
+
def call(name:, **_options)
|
|
79
|
+
return {} unless self.class.defaults_exist?
|
|
80
|
+
|
|
81
|
+
trace!(:bundled_defaults, path: self.class.defaults_path) do
|
|
82
|
+
self.class.load_raw_yaml
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Loads user configuration from XDG config directory
|
|
88
|
+
# Follows XDG Base Directory Specification: ~/.config/aia/aia.yml
|
|
89
|
+
class UserConfigLoader < Anyway::Loaders::Base
|
|
90
|
+
class << self
|
|
91
|
+
# Returns the path to the user config file
|
|
92
|
+
# Uses XDG_CONFIG_HOME if set, otherwise defaults to ~/.config
|
|
93
|
+
#
|
|
94
|
+
# @return [String] path to user config file
|
|
95
|
+
def user_config_path
|
|
96
|
+
xdg_config_home = ENV.fetch('XDG_CONFIG_HOME', File.expand_path('~/.config'))
|
|
97
|
+
File.join(xdg_config_home, 'aia', 'aia.yml')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if user config file exists
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def user_config_exist?
|
|
104
|
+
File.exist?(user_config_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Load and parse the user YAML content
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash] parsed YAML with symbolized keys
|
|
110
|
+
def load_user_yaml
|
|
111
|
+
return {} unless user_config_exist?
|
|
112
|
+
|
|
113
|
+
content = File.read(user_config_path)
|
|
114
|
+
YAML.safe_load(
|
|
115
|
+
content,
|
|
116
|
+
permitted_classes: [Symbol, Date],
|
|
117
|
+
symbolize_names: true,
|
|
118
|
+
aliases: true
|
|
119
|
+
) || {}
|
|
120
|
+
rescue Psych::SyntaxError => e
|
|
121
|
+
warn "AIA: Failed to parse user config #{user_config_path}: #{e.message}"
|
|
122
|
+
{}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Called by Anyway Config to load configuration
|
|
127
|
+
#
|
|
128
|
+
# @param name [Symbol] the config name (unused, always :aia)
|
|
129
|
+
# @return [Hash] configuration hash
|
|
130
|
+
def call(name:, **_options)
|
|
131
|
+
return {} unless self.class.user_config_exist?
|
|
132
|
+
|
|
133
|
+
trace!(:user_config, path: self.class.user_config_path) do
|
|
134
|
+
self.class.load_user_yaml
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Register loaders in priority order (lowest to highest)
|
|
142
|
+
# 1. bundled_defaults - gem's default values
|
|
143
|
+
# 2. user_config - user's ~/.config/aia/aia.yml
|
|
144
|
+
# 3. yml - standard anyway_config yml loader (for config/aia.yml in app directory)
|
|
145
|
+
# 4. env - environment variables
|
|
146
|
+
Anyway.loaders.insert_before :yml, :bundled_defaults, AIA::Loaders::DefaultsLoader
|
|
147
|
+
Anyway.loaders.insert_after :bundled_defaults, :user_config, AIA::Loaders::UserConfigLoader
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/config/mcp_parser.rb
|
|
4
|
+
#
|
|
5
|
+
# Parses MCP server JSON configuration files and converts them
|
|
6
|
+
# to the format expected by AIA's config system.
|
|
7
|
+
#
|
|
8
|
+
# Supports two JSON formats:
|
|
9
|
+
#
|
|
10
|
+
# 1. Simple format (single server):
|
|
11
|
+
# {
|
|
12
|
+
# "type": "stdio",
|
|
13
|
+
# "command": ["npx", "-y", "@server/name", "/path"]
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
# 2. mcpServers format (one or more servers):
|
|
17
|
+
# {
|
|
18
|
+
# "mcpServers": {
|
|
19
|
+
# "server_name": {
|
|
20
|
+
# "command": "python",
|
|
21
|
+
# "args": ["-m", "module_name"],
|
|
22
|
+
# "env": {},
|
|
23
|
+
# "timeout": 8000
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
# }
|
|
27
|
+
|
|
28
|
+
require 'json'
|
|
29
|
+
|
|
30
|
+
module AIA
|
|
31
|
+
module McpParser
|
|
32
|
+
class << self
|
|
33
|
+
# Parse MCP server configuration files and return array of server configs
|
|
34
|
+
#
|
|
35
|
+
# @param file_paths [Array<String>] paths to JSON configuration files
|
|
36
|
+
# @return [Array<Hash>] array of server configurations
|
|
37
|
+
def parse_files(file_paths)
|
|
38
|
+
return [] if file_paths.nil? || file_paths.empty?
|
|
39
|
+
|
|
40
|
+
servers = []
|
|
41
|
+
|
|
42
|
+
file_paths.each do |file_path|
|
|
43
|
+
expanded_path = File.expand_path(file_path)
|
|
44
|
+
|
|
45
|
+
unless File.exist?(expanded_path)
|
|
46
|
+
warn "Warning: MCP config file not found: #{file_path}"
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
json_content = File.read(expanded_path)
|
|
52
|
+
parsed = JSON.parse(json_content)
|
|
53
|
+
servers.concat(convert_to_config_format(parsed, file_path))
|
|
54
|
+
rescue JSON::ParserError => e
|
|
55
|
+
warn "Warning: Invalid JSON in MCP config file '#{file_path}': #{e.message}"
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
warn "Warning: Error reading MCP config file '#{file_path}': #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
servers
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Convert parsed JSON to the config format
|
|
67
|
+
#
|
|
68
|
+
# @param parsed [Hash] parsed JSON content
|
|
69
|
+
# @param file_path [String] original file path (for deriving server name)
|
|
70
|
+
# @return [Array<Hash>] array of server configurations
|
|
71
|
+
def convert_to_config_format(parsed, file_path)
|
|
72
|
+
if parsed.key?('mcpServers')
|
|
73
|
+
convert_mcp_servers_format(parsed['mcpServers'])
|
|
74
|
+
else
|
|
75
|
+
convert_simple_format(parsed, file_path)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Convert mcpServers format to config format
|
|
80
|
+
#
|
|
81
|
+
# @param mcp_servers [Hash] the mcpServers hash from JSON
|
|
82
|
+
# @return [Array<Hash>] array of server configurations
|
|
83
|
+
def convert_mcp_servers_format(mcp_servers)
|
|
84
|
+
mcp_servers.map do |name, config|
|
|
85
|
+
server = { name: name }
|
|
86
|
+
|
|
87
|
+
if config['command']
|
|
88
|
+
server[:command] = config['command']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if config['args']
|
|
92
|
+
server[:args] = Array(config['args'])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if config['env']
|
|
96
|
+
server[:env] = config['env']
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if config['timeout']
|
|
100
|
+
server[:timeout] = config['timeout'].to_i
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if config['url']
|
|
104
|
+
server[:url] = config['url']
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if config['headers']
|
|
108
|
+
server[:headers] = config['headers']
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
server
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Convert simple format to config format
|
|
116
|
+
#
|
|
117
|
+
# @param parsed [Hash] parsed JSON with type and command
|
|
118
|
+
# @param file_path [String] file path for deriving server name
|
|
119
|
+
# @return [Array<Hash>] array with single server configuration
|
|
120
|
+
def convert_simple_format(parsed, file_path)
|
|
121
|
+
# Derive name from filename (e.g., "filesystem.json" -> "filesystem")
|
|
122
|
+
name = File.basename(file_path, '.*')
|
|
123
|
+
|
|
124
|
+
server = { name: name }
|
|
125
|
+
|
|
126
|
+
if parsed['command'].is_a?(Array)
|
|
127
|
+
# Command is an array: first element is command, rest are args
|
|
128
|
+
server[:command] = parsed['command'].first
|
|
129
|
+
server[:args] = parsed['command'][1..] || []
|
|
130
|
+
elsif parsed['command']
|
|
131
|
+
server[:command] = parsed['command']
|
|
132
|
+
server[:args] = parsed['args'] || []
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if parsed['env']
|
|
136
|
+
server[:env] = parsed['env']
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if parsed['timeout']
|
|
140
|
+
server[:timeout] = parsed['timeout'].to_i
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if parsed['type']
|
|
144
|
+
server[:type] = parsed['type']
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
[server]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/config/model_spec.rb
|
|
4
|
+
#
|
|
5
|
+
# ModelSpec represents a single model configuration with optional role.
|
|
6
|
+
# This provides typed access to model configuration instead of raw hashes.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# spec = ModelSpec.new(name: 'gpt-4o', role: 'architect')
|
|
10
|
+
# spec.name # => 'gpt-4o'
|
|
11
|
+
# spec.role # => 'architect'
|
|
12
|
+
# spec.internal_id # => 'gpt-4o'
|
|
13
|
+
|
|
14
|
+
module AIA
|
|
15
|
+
class ModelSpec
|
|
16
|
+
attr_accessor :name, :role, :instance, :internal_id
|
|
17
|
+
|
|
18
|
+
def initialize(hash = {})
|
|
19
|
+
hash = hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys)
|
|
20
|
+
|
|
21
|
+
@name = hash[:name]
|
|
22
|
+
@role = hash[:role]
|
|
23
|
+
@instance = hash[:instance] || 1
|
|
24
|
+
@internal_id = hash[:internal_id] || @name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
name: @name,
|
|
30
|
+
role: @role,
|
|
31
|
+
instance: @instance,
|
|
32
|
+
internal_id: @internal_id
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
if @role
|
|
38
|
+
"#{@name}=#{@role}"
|
|
39
|
+
else
|
|
40
|
+
@name.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ==(other)
|
|
45
|
+
return false unless other.is_a?(ModelSpec)
|
|
46
|
+
name == other.name && role == other.role && instance == other.instance
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def eql?(other)
|
|
50
|
+
self == other
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def hash
|
|
54
|
+
[name, role, instance].hash
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if this model has a role assigned
|
|
58
|
+
def role?
|
|
59
|
+
!@role.nil? && !@role.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if this is a duplicate instance of the same model
|
|
63
|
+
def duplicate?
|
|
64
|
+
@instance > 1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|