aia 0.9.1 → 0.9.3rc1
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 +16 -0
- data/README.md +98 -57
- data/examples/tools/edit_file.rb +26 -0
- data/examples/tools/list_files.rb +18 -0
- data/examples/tools/read_file.rb +16 -0
- data/examples/tools/run_shell_command.rb +21 -0
- data/lib/aia/chat_processor_service.rb +8 -6
- data/lib/aia/config.rb +288 -291
- data/lib/aia/context_manager.rb +1 -1
- data/lib/aia/directive_processor.rb +6 -1
- data/lib/aia/ruby_llm_adapter.rb +175 -137
- data/lib/aia/session.rb +7 -8
- data/lib/aia/utility.rb +17 -7
- data/lib/aia.rb +11 -5
- metadata +16 -27
- data/lib/extensions/ruby_llm/chat.rb +0 -197
data/lib/aia/config.rb
CHANGED
@@ -7,6 +7,7 @@
|
|
7
7
|
|
8
8
|
require 'yaml'
|
9
9
|
require 'toml-rb'
|
10
|
+
require 'date'
|
10
11
|
require 'erb'
|
11
12
|
require 'optparse'
|
12
13
|
require 'json'
|
@@ -29,9 +30,10 @@ module AIA
|
|
29
30
|
role: '',
|
30
31
|
system_prompt: '',
|
31
32
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
33
|
+
# Tools
|
34
|
+
allowed_tools: nil, # nil means all tools are allowed; otherwise an Array of Strings which are the tool names
|
35
|
+
rejected_tools: nil, # nil means no tools are rejected
|
36
|
+
tool_paths: [], # Strings - absolute and relative to tools
|
35
37
|
|
36
38
|
# Flags
|
37
39
|
markdown: true,
|
@@ -51,7 +53,6 @@ module AIA
|
|
51
53
|
pipeline: [],
|
52
54
|
|
53
55
|
# PromptManager::Prompt Tailoring
|
54
|
-
|
55
56
|
parameter_regex: PromptManager::Prompt.parameter_regex.to_s,
|
56
57
|
|
57
58
|
# LLM tuning parameters
|
@@ -63,15 +64,18 @@ module AIA
|
|
63
64
|
image_size: '1024x1024',
|
64
65
|
image_quality: 'standard',
|
65
66
|
image_style: 'vivid',
|
67
|
+
|
66
68
|
model: 'gpt-4o-mini',
|
67
69
|
speech_model: 'tts-1',
|
68
70
|
transcription_model: 'whisper-1',
|
71
|
+
embedding_model: 'text-embedding-ada-002',
|
72
|
+
image_model: 'dall-e-3',
|
73
|
+
refresh: 0, # days between refreshes of model info; 0 means every startup
|
74
|
+
last_refresh: Date.today - 1,
|
75
|
+
|
69
76
|
voice: 'alloy',
|
70
77
|
adapter: 'ruby_llm', # 'ruby_llm' or ???
|
71
78
|
|
72
|
-
# Embedding parameters
|
73
|
-
embedding_model: 'text-embedding-ada-002',
|
74
|
-
|
75
79
|
# Default speak command
|
76
80
|
speak_command: 'afplay', # 'afplay' for audio files
|
77
81
|
|
@@ -97,6 +101,14 @@ module AIA
|
|
97
101
|
)
|
98
102
|
|
99
103
|
tailor_the_config(config)
|
104
|
+
load_libraries(config)
|
105
|
+
load_tools(config)
|
106
|
+
|
107
|
+
if config.dump_file
|
108
|
+
dump_config(config, config.dump_file)
|
109
|
+
end
|
110
|
+
|
111
|
+
config
|
100
112
|
end
|
101
113
|
|
102
114
|
|
@@ -129,7 +141,6 @@ module AIA
|
|
129
141
|
exit 1
|
130
142
|
end
|
131
143
|
|
132
|
-
|
133
144
|
unless config.role.empty?
|
134
145
|
unless config.roles_prefix.empty?
|
135
146
|
unless config.role.start_with?(config.roles_prefix)
|
@@ -177,11 +188,6 @@ module AIA
|
|
177
188
|
and_exit = true
|
178
189
|
end
|
179
190
|
|
180
|
-
if config.dump_file
|
181
|
-
dump_config(config, config.dump_file)
|
182
|
-
and_exit = true
|
183
|
-
end
|
184
|
-
|
185
191
|
exit if and_exit
|
186
192
|
|
187
193
|
# Only require a prompt_id if we're not in chat mode, not using fuzzy search, and no context files
|
@@ -200,16 +206,64 @@ module AIA
|
|
200
206
|
PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
|
201
207
|
end
|
202
208
|
|
203
|
-
|
209
|
+
config
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
def self.load_libraries(config)
|
214
|
+
return if config.require_libs.empty?
|
215
|
+
|
216
|
+
exit_on_error = false
|
217
|
+
|
218
|
+
config.require_libs.each do |library|
|
219
|
+
begin
|
220
|
+
require(library)
|
221
|
+
rescue => e
|
222
|
+
STDERR.puts "Error loading library '#{library}' #{e.message}"
|
223
|
+
exit_on_error = true
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
exit(1) if exit_on_error
|
228
|
+
|
229
|
+
config
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
def self.load_tools(config)
|
234
|
+
return if config.tool_paths.empty?
|
235
|
+
|
236
|
+
exit_on_error = false
|
237
|
+
|
238
|
+
unless config.allowed_tools.nil?
|
239
|
+
config.tool_paths.select! do |path|
|
240
|
+
config.allowed_tools.any? { |allowed| path.include?(allowed) }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
unless config.rejected_tools.nil?
|
245
|
+
config.tool_paths.reject! do |path|
|
246
|
+
config.rejected_tools.any? { |rejected| path.include?(rejected) }
|
247
|
+
end
|
248
|
+
end
|
204
249
|
|
205
|
-
|
206
|
-
|
207
|
-
|
250
|
+
config.tool_paths.each do |tool_path|
|
251
|
+
begin
|
252
|
+
# expands path based on PWD
|
253
|
+
absolute_tool_path = File.expand_path(tool_path)
|
254
|
+
require(absolute_tool_path)
|
255
|
+
rescue => e
|
256
|
+
STDERR.puts "Error loading tool '#{tool_path}' #{e.message}"
|
257
|
+
exit_on_error = true
|
258
|
+
end
|
208
259
|
end
|
209
260
|
|
261
|
+
exit(1) if exit_on_error
|
262
|
+
|
210
263
|
config
|
211
264
|
end
|
212
265
|
|
266
|
+
|
213
267
|
# envar values are always String object so need other config
|
214
268
|
# layers to know the prompter type for each key's value
|
215
269
|
def self.envar_options(default, cli_config)
|
@@ -241,237 +295,264 @@ module AIA
|
|
241
295
|
def self.cli_options
|
242
296
|
config = OpenStruct.new
|
243
297
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
298
|
+
begin
|
299
|
+
opt_parser = OptionParser.new do |opts|
|
300
|
+
opts.banner = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n" +
|
301
|
+
" aia --chat [PROMPT_ID] [CONTEXT_FILE]*\n" +
|
302
|
+
" aia --chat [CONTEXT_FILE]*"
|
303
|
+
|
304
|
+
opts.on("--chat", "Begin a chat session with the LLM after the initial prompt response; will set --no-out_file so that the LLM response comes to STDOUT.") do
|
305
|
+
config.chat = true
|
306
|
+
puts "Debug: Setting chat mode to true" if config.debug
|
307
|
+
end
|
253
308
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
309
|
+
opts.on("--adapter ADAPTER", "Interface that adapts AIA to the LLM") do |adapter|
|
310
|
+
adapter.downcase!
|
311
|
+
valid_adapters = %w[ ruby_llm ] # NOTE: Add additional adapters here when needed
|
312
|
+
if valid_adapters.include? adapter
|
313
|
+
config.adapter = adapter
|
314
|
+
else
|
315
|
+
STDERR.puts "ERROR: Invalid adapter #{adapter} must be one of these: #{valid_adapters.join(', ')}"
|
316
|
+
exit 1
|
317
|
+
end
|
262
318
|
end
|
263
|
-
end
|
264
319
|
|
320
|
+
opts.on("-m MODEL", "--model MODEL", "Name of the LLM model to use") do |model|
|
321
|
+
config.model = model
|
322
|
+
end
|
265
323
|
|
266
|
-
|
267
|
-
|
268
|
-
|
324
|
+
opts.on("--terse", "Adds a special instruction to the prompt asking the AI to keep responses short and to the point") do
|
325
|
+
config.terse = true
|
326
|
+
end
|
269
327
|
|
270
|
-
|
271
|
-
|
272
|
-
|
328
|
+
opts.on("-c", "--config_file FILE", "Load config file") do |file|
|
329
|
+
if File.exist?(file)
|
330
|
+
ext = File.extname(file).downcase
|
331
|
+
content = File.read(file)
|
273
332
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
333
|
+
# Process ERB if filename ends with .erb
|
334
|
+
if file.end_with?('.erb')
|
335
|
+
content = ERB.new(content).result
|
336
|
+
file = file.chomp('.erb')
|
337
|
+
File.write(file, content)
|
338
|
+
end
|
278
339
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
340
|
+
file_config = case ext
|
341
|
+
when '.yml', '.yaml'
|
342
|
+
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
343
|
+
when '.toml'
|
344
|
+
TomlRB.parse(content)
|
345
|
+
else
|
346
|
+
raise "Unsupported config file format: #{ext}"
|
347
|
+
end
|
348
|
+
|
349
|
+
file_config.each do |key, value|
|
350
|
+
config[key.to_sym] = value
|
351
|
+
end
|
352
|
+
else
|
353
|
+
raise "Config file not found: #{file}"
|
284
354
|
end
|
355
|
+
end
|
285
356
|
|
286
|
-
|
287
|
-
|
288
|
-
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
289
|
-
when '.toml'
|
290
|
-
TomlRB.parse(content)
|
291
|
-
else
|
292
|
-
raise "Unsupported config file format: #{ext}"
|
293
|
-
end
|
294
|
-
|
295
|
-
file_config.each do |key, value|
|
296
|
-
config[key.to_sym] = value
|
297
|
-
end
|
298
|
-
else
|
299
|
-
raise "Config file not found: #{file}"
|
357
|
+
opts.on("-p", "--prompts_dir DIR", "Directory containing prompt files") do |dir|
|
358
|
+
config.prompts_dir = dir
|
300
359
|
end
|
301
|
-
end
|
302
360
|
|
303
|
-
|
304
|
-
|
305
|
-
|
361
|
+
opts.on("--roles_prefix PREFIX", "Subdirectory name for role files (default: roles)") do |prefix|
|
362
|
+
config.roles_prefix = prefix
|
363
|
+
end
|
306
364
|
|
307
|
-
|
308
|
-
|
309
|
-
|
365
|
+
opts.on("-r", "--role ROLE_ID", "Role ID to prepend to prompt") do |role|
|
366
|
+
config.role = role
|
367
|
+
end
|
310
368
|
|
311
|
-
|
312
|
-
|
313
|
-
|
369
|
+
opts.on("--refresh DAYS", Integer, "Refresh models database interval in days") do |days|
|
370
|
+
config.refresh = days || 0
|
371
|
+
end
|
314
372
|
|
315
|
-
|
316
|
-
|
317
|
-
|
373
|
+
opts.on('--regex pattern', 'Regex pattern to extract parameters from prompt text') do |pattern|
|
374
|
+
config.parameter_regex = pattern
|
375
|
+
end
|
318
376
|
|
319
|
-
|
320
|
-
|
321
|
-
|
377
|
+
opts.on("-o", "--[no-]out_file [FILE]", "Output file (default: temp.md)") do |file|
|
378
|
+
config.out_file = file ? File.expand_path(file, Dir.pwd) : 'temp.md'
|
379
|
+
end
|
322
380
|
|
323
|
-
|
324
|
-
|
325
|
-
|
381
|
+
opts.on("-a", "--[no-]append", "Append to output file instead of overwriting") do |append|
|
382
|
+
config.append = append
|
383
|
+
end
|
326
384
|
|
327
|
-
|
328
|
-
|
329
|
-
|
385
|
+
opts.on("-l", "--[no-]log_file [FILE]", "Log file") do |file|
|
386
|
+
config.log_file = file
|
387
|
+
end
|
330
388
|
|
331
|
-
|
332
|
-
|
333
|
-
|
389
|
+
opts.on("--md", "--[no-]markdown", "Format with Markdown") do |md|
|
390
|
+
config.markdown = md
|
391
|
+
end
|
334
392
|
|
335
|
-
|
336
|
-
|
337
|
-
|
393
|
+
opts.on("-n", "--next PROMPT_ID", "Next prompt to process") do |next_prompt|
|
394
|
+
config.next = next_prompt
|
395
|
+
end
|
338
396
|
|
339
|
-
|
340
|
-
|
341
|
-
|
397
|
+
opts.on("--pipeline PROMPTS", "Pipeline of prompts to process") do |pipeline|
|
398
|
+
config.pipeline = pipeline.split(',')
|
399
|
+
end
|
342
400
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
401
|
+
opts.on("-f", "--fuzzy", "Use fuzzy matching for prompt search") do
|
402
|
+
unless system("which fzf > /dev/null 2>&1")
|
403
|
+
STDERR.puts "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option."
|
404
|
+
exit 1
|
405
|
+
end
|
406
|
+
config.fuzzy = true
|
347
407
|
end
|
348
|
-
config.fuzzy = true
|
349
|
-
end
|
350
408
|
|
351
|
-
|
352
|
-
|
353
|
-
|
409
|
+
opts.on("-d", "--debug", "Enable debug output") do
|
410
|
+
config.debug = $DEBUG_ME = true
|
411
|
+
end
|
354
412
|
|
355
|
-
|
356
|
-
|
357
|
-
|
413
|
+
opts.on("--no-debug", "Disable debug output") do
|
414
|
+
config.debug = $DEBUG_ME = false
|
415
|
+
end
|
358
416
|
|
359
|
-
|
360
|
-
|
361
|
-
|
417
|
+
opts.on("-v", "--verbose", "Be verbose") do
|
418
|
+
config.verbose = true
|
419
|
+
end
|
362
420
|
|
363
|
-
|
364
|
-
|
365
|
-
|
421
|
+
opts.on("--speak", "Simple implementation. Uses the speech model to convert text to audio, then plays the audio. Fun with --chat. Supports configuration of speech model and voice.") do
|
422
|
+
config.speak = true
|
423
|
+
end
|
366
424
|
|
367
|
-
|
368
|
-
|
369
|
-
|
425
|
+
opts.on("--voice VOICE", "Voice to use for speech") do |voice|
|
426
|
+
config.voice = voice
|
427
|
+
end
|
370
428
|
|
371
|
-
|
372
|
-
|
373
|
-
|
429
|
+
opts.on("--sm", "--speech_model MODEL", "Speech model to use") do |model|
|
430
|
+
config.speech_model = model
|
431
|
+
end
|
374
432
|
|
375
|
-
|
376
|
-
|
377
|
-
|
433
|
+
opts.on("--tm", "--transcription_model MODEL", "Transcription model to use") do |model|
|
434
|
+
config.transcription_model = model
|
435
|
+
end
|
378
436
|
|
379
|
-
|
380
|
-
|
381
|
-
|
437
|
+
opts.on("--is", "--image_size SIZE", "Image size for image generation") do |size|
|
438
|
+
config.image_size = size
|
439
|
+
end
|
382
440
|
|
383
|
-
|
384
|
-
|
385
|
-
|
441
|
+
opts.on("--iq", "--image_quality QUALITY", "Image quality for image generation") do |quality|
|
442
|
+
config.image_quality = quality
|
443
|
+
end
|
386
444
|
|
387
|
-
|
388
|
-
|
389
|
-
|
445
|
+
opts.on("--style", "--image_style STYLE", "Style for image generation") do |style|
|
446
|
+
config.image_style = style
|
447
|
+
end
|
390
448
|
|
391
|
-
|
392
|
-
|
393
|
-
|
449
|
+
opts.on("--system_prompt PROMPT_ID", "System prompt ID to use for chat sessions") do |prompt_id|
|
450
|
+
config.system_prompt = prompt_id
|
451
|
+
end
|
394
452
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
453
|
+
# AI model parameters
|
454
|
+
opts.on("-t", "--temperature TEMP", Float, "Temperature for text generation") do |temp|
|
455
|
+
config.temperature = temp
|
456
|
+
end
|
399
457
|
|
400
|
-
|
401
|
-
|
402
|
-
|
458
|
+
opts.on("--max_tokens TOKENS", Integer, "Maximum tokens for text generation") do |tokens|
|
459
|
+
config.max_tokens = tokens
|
460
|
+
end
|
403
461
|
|
404
|
-
|
405
|
-
|
406
|
-
|
462
|
+
opts.on("--top_p VALUE", Float, "Top-p sampling value") do |value|
|
463
|
+
config.top_p = value
|
464
|
+
end
|
407
465
|
|
408
|
-
|
409
|
-
|
410
|
-
|
466
|
+
opts.on("--frequency_penalty VALUE", Float, "Frequency penalty") do |value|
|
467
|
+
config.frequency_penalty = value
|
468
|
+
end
|
411
469
|
|
412
|
-
|
413
|
-
|
414
|
-
|
470
|
+
opts.on("--presence_penalty VALUE", Float, "Presence penalty") do |value|
|
471
|
+
config.presence_penalty = value
|
472
|
+
end
|
415
473
|
|
416
|
-
|
417
|
-
|
418
|
-
|
474
|
+
opts.on("--dump FILE", "Dump config to file") do |file|
|
475
|
+
config.dump_file = file
|
476
|
+
end
|
419
477
|
|
420
|
-
|
421
|
-
|
422
|
-
|
478
|
+
opts.on("--completion SHELL", "Show completion script for bash|zsh|fish - default is nil") do |shell|
|
479
|
+
config.completion = shell
|
480
|
+
end
|
423
481
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
482
|
+
opts.on("--version", "Show version") do
|
483
|
+
puts AIA::VERSION
|
484
|
+
exit
|
485
|
+
end
|
428
486
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
487
|
+
opts.on("-h", "--help", "Prints this help") do
|
488
|
+
puts opts
|
489
|
+
exit
|
490
|
+
end
|
433
491
|
|
434
|
-
|
435
|
-
|
436
|
-
|
492
|
+
opts.on("--rq LIBS", "--require LIBS", "Ruby libraries to require for Ruby directive") do |libs|
|
493
|
+
config.require_libs ||= []
|
494
|
+
config.require_libs += libs.split(',')
|
495
|
+
end
|
496
|
+
|
497
|
+
opts.on("--tools PATH_LIST", "Add a tool(s)") do |a_path_list|
|
498
|
+
config.tool_paths ||= []
|
499
|
+
|
500
|
+
if a_path_list.empty?
|
501
|
+
STDERR.puts "No list of paths for --tools option"
|
502
|
+
exit 1
|
503
|
+
else
|
504
|
+
paths = a_path_list.split(',').map(&:strip).uniq
|
505
|
+
end
|
506
|
+
|
507
|
+
paths.each do |a_path|
|
508
|
+
if File.exist?(a_path)
|
509
|
+
if File.file?(a_path)
|
510
|
+
if '.rb' == File.extname(a_path)
|
511
|
+
config.tool_paths << a_path
|
512
|
+
else
|
513
|
+
STDERR.puts "file should have *.rb extension: #{a_path}"
|
514
|
+
exit 1
|
515
|
+
end
|
516
|
+
elsif File.directory?(a_path)
|
517
|
+
rb_files = Dir.glob(File.join(a_path, '**', '*.rb'))
|
518
|
+
config.tool_paths += rb_files
|
519
|
+
end
|
520
|
+
else
|
521
|
+
STDERR.puts "file/dir path is not valid: #{a_path}"
|
522
|
+
exit 1
|
523
|
+
end
|
524
|
+
end
|
437
525
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
# }
|
446
|
-
# }
|
447
|
-
# FIXME: need to rurn multiple JSON files into one.
|
448
|
-
if AIA.good_file?(file)
|
449
|
-
config.mcp_servers ||= []
|
450
|
-
config.mcp_servers << file
|
451
|
-
begin
|
452
|
-
server_config = JSON.parse(File.read(file))
|
453
|
-
config.mcp_servers_config ||= []
|
454
|
-
config.mcp_servers_config << server_config
|
455
|
-
rescue JSON::ParserError => e
|
456
|
-
STDERR.puts "Error parsing MCP server config file #{file}: #{e.message}"
|
526
|
+
config.tool_paths.uniq!
|
527
|
+
end
|
528
|
+
|
529
|
+
opts.on("--at", "--allowed_tools TOOLS_LIST", "Allow only these tools to be used") do |tools_list|
|
530
|
+
config.allowed_tools ||= []
|
531
|
+
if tools_list.empty?
|
532
|
+
STDERR.puts "No list of tool names provided for --allowed_tools option"
|
457
533
|
exit 1
|
534
|
+
else
|
535
|
+
config.allowed_tools += tools_list.split(',').map(&:strip)
|
536
|
+
config.allowed_tools.uniq!
|
458
537
|
end
|
459
|
-
else
|
460
|
-
STDERR.puts "MCP server config file not found: #{file}"
|
461
|
-
exit 1
|
462
538
|
end
|
463
|
-
end
|
464
539
|
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
540
|
+
opts.on("--rt", "--rejected_tools TOOLS_LIST", "Reject these tools") do |tools_list|
|
541
|
+
config.rejected_tools ||= []
|
542
|
+
if tools_list.empty?
|
543
|
+
STDERR.puts "No list of tool names provided for --rejected_tools option"
|
544
|
+
exit 1
|
545
|
+
else
|
546
|
+
config.rejected_tools += tools_list.split(',').map(&:strip)
|
547
|
+
config.rejected_tools.uniq!
|
548
|
+
end
|
473
549
|
end
|
474
550
|
end
|
551
|
+
opt_parser.parse!
|
552
|
+
rescue => e
|
553
|
+
STDERR.puts "ERROR: #{e.message}"
|
554
|
+
STDERR.puts " use --help for usage report"
|
555
|
+
exit 1
|
475
556
|
end
|
476
557
|
|
477
558
|
args = ARGV.dup
|
@@ -519,6 +600,12 @@ module AIA
|
|
519
600
|
STDERR.puts "WARNING:Config file not found: #{file}"
|
520
601
|
end
|
521
602
|
|
603
|
+
if config.last_refresh
|
604
|
+
if config.last_refresh.is_a? String
|
605
|
+
config.last_refresh = Date.strptime(config.last_refresh, '%Y-%m-%d')
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
522
609
|
config
|
523
610
|
end
|
524
611
|
|
@@ -537,10 +624,10 @@ module AIA
|
|
537
624
|
def self.dump_config(config, file)
|
538
625
|
# Implementation for config dump
|
539
626
|
ext = File.extname(file).downcase
|
540
|
-
config_hash = config.to_h
|
541
627
|
|
542
|
-
|
543
|
-
|
628
|
+
config.last_refresh = config.last_refresh.to_s if config.last_refresh.is_a? Date
|
629
|
+
|
630
|
+
config_hash = config.to_h
|
544
631
|
|
545
632
|
# Remove dump_file key to prevent automatic exit on next load
|
546
633
|
config_hash.delete(:dump_file)
|
@@ -557,95 +644,5 @@ module AIA
|
|
557
644
|
File.write(file, content)
|
558
645
|
puts "Config successfully dumped to #{file}"
|
559
646
|
end
|
560
|
-
|
561
|
-
|
562
|
-
# Combine multiple MCP server JSON files into a single file
|
563
|
-
def self.combine_mcp_server_json_files(file_paths)
|
564
|
-
raise ArgumentError, "No JSON files provided" if file_paths.nil? || file_paths.empty?
|
565
|
-
|
566
|
-
# The output will have only one top-level key: "mcpServers"
|
567
|
-
mcp_servers = {} # This will store all collected server_name => server_config pairs
|
568
|
-
|
569
|
-
file_paths.each do |file_path|
|
570
|
-
file_content = JSON.parse(File.read(file_path))
|
571
|
-
# Clean basename, e.g., "filesystem.json" -> "filesystem", "foo.json.erb" -> "foo"
|
572
|
-
cleaned_basename = File.basename(file_path).sub(/\.json\.erb$/, '').sub(/\.json$/, '')
|
573
|
-
|
574
|
-
if file_content.is_a?(Hash)
|
575
|
-
if file_content.key?("mcpServers") && file_content["mcpServers"].is_a?(Hash)
|
576
|
-
# Case A: {"mcpServers": {"name1": {...}, "name2": {...}}}
|
577
|
-
file_content["mcpServers"].each do |server_name, server_data|
|
578
|
-
if mcp_servers.key?(server_name)
|
579
|
-
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' found. Overwriting with definition from #{file_path}."
|
580
|
-
end
|
581
|
-
mcp_servers[server_name] = server_data
|
582
|
-
end
|
583
|
-
# Check if the root hash itself is a single server definition
|
584
|
-
elsif is_single_server_definition?(file_content)
|
585
|
-
# Case B: {"type": "stdio", ...} or {"url": "...", ...}
|
586
|
-
# Use "name" property from JSON if present, otherwise use cleaned_basename
|
587
|
-
server_name = file_content["name"] || cleaned_basename
|
588
|
-
if mcp_servers.key?(server_name)
|
589
|
-
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' (from file #{file_path}). Overwriting."
|
590
|
-
end
|
591
|
-
mcp_servers[server_name] = file_content
|
592
|
-
else
|
593
|
-
# Case D: Fallback for {"custom_name1": {server_config1}, "custom_name2": {server_config2}}
|
594
|
-
# This assumes top-level keys are server names and values are server configs.
|
595
|
-
file_content.each do |server_name, server_data|
|
596
|
-
if server_data.is_a?(Hash) && is_single_server_definition?(server_data)
|
597
|
-
if mcp_servers.key?(server_name)
|
598
|
-
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' found in #{file_path}. Overwriting."
|
599
|
-
end
|
600
|
-
mcp_servers[server_name] = server_data
|
601
|
-
else
|
602
|
-
STDERR.puts "Warning: Unrecognized structure for key '#{server_name}' in #{file_path}. Value is not a valid server definition. Skipping."
|
603
|
-
end
|
604
|
-
end
|
605
|
-
end
|
606
|
-
elsif file_content.is_a?(Array)
|
607
|
-
# Case C: [ {server_config1}, {server_config2_with_name} ]
|
608
|
-
file_content.each_with_index do |server_data, index|
|
609
|
-
if server_data.is_a?(Hash) && is_single_server_definition?(server_data)
|
610
|
-
# Use "name" property from JSON if present, otherwise generate one
|
611
|
-
server_name = server_data["name"] || "#{cleaned_basename}_#{index}"
|
612
|
-
if mcp_servers.key?(server_name)
|
613
|
-
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' (from array in #{file_path}). Overwriting."
|
614
|
-
end
|
615
|
-
mcp_servers[server_name] = server_data
|
616
|
-
else
|
617
|
-
STDERR.puts "Warning: Unrecognized item in array in #{file_path} at index #{index}. Skipping."
|
618
|
-
end
|
619
|
-
end
|
620
|
-
else
|
621
|
-
STDERR.puts "Warning: Unrecognized JSON structure in #{file_path}. Skipping."
|
622
|
-
end
|
623
|
-
end
|
624
|
-
|
625
|
-
# Create the final output structure
|
626
|
-
output = {"mcpServers" => mcp_servers}
|
627
|
-
temp_file = Tempfile.new(['combined', '.json'])
|
628
|
-
temp_file.write(JSON.pretty_generate(output))
|
629
|
-
temp_file.close
|
630
|
-
|
631
|
-
temp_file.path
|
632
|
-
end
|
633
|
-
|
634
|
-
# Helper method to determine if a hash represents a valid MCP server definition
|
635
|
-
def self.is_single_server_definition?(config)
|
636
|
-
return false unless config.is_a?(Hash)
|
637
|
-
type = config['type']
|
638
|
-
if type
|
639
|
-
return true if type == 'stdio' && config.key?('command')
|
640
|
-
return true if type == 'sse' && config.key?('url')
|
641
|
-
# Potentially other explicit types if they exist in MCP
|
642
|
-
return false # Known type but missing required fields for it, or unknown type
|
643
|
-
else
|
644
|
-
# Infer type
|
645
|
-
return true if config.key?('command') || config.key?('args') || config.key?('env') # stdio
|
646
|
-
return true if config.key?('url') # sse
|
647
|
-
end
|
648
|
-
false
|
649
|
-
end
|
650
647
|
end
|
651
648
|
end
|