aircana 3.0.0.rc8 → 3.1.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.
@@ -15,12 +15,20 @@ module Aircana
15
15
  class << self # rubocop:disable Metrics/ClassLength
16
16
  def refresh(agent)
17
17
  normalized_agent = normalize_string(agent)
18
+
19
+ # Check if this is a local knowledge base agent
20
+ kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(normalized_agent)
21
+ if kb_type == "local"
22
+ Aircana.human_logger.info "⊘ Skipping #{normalized_agent} (local knowledge base - no refresh needed)"
23
+ return
24
+ end
25
+
18
26
  perform_manifest_aware_refresh(normalized_agent)
19
27
  rescue Aircana::Error => e
20
28
  handle_refresh_error(normalized_agent, e)
21
29
  end
22
30
 
23
- def create # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
31
+ def create
24
32
  prompt = TTY::Prompt.new
25
33
 
26
34
  agent_name = prompt.ask("Agent name:")
@@ -28,6 +36,20 @@ module Aircana
28
36
  model = prompt.select("Select a model for your agent:", SUPPORTED_CLAUDE_MODELS)
29
37
  color = prompt.select("Select a color for your agent:", SUPPORTED_CLAUDE_COLORS)
30
38
 
39
+ # Prompt for knowledge base type
40
+ kb_type = prompt.select("Knowledge base type:", [
41
+ {
42
+ name: "Remote - Stored in ~/.claude/agents/, not version controlled, " \
43
+ "requires refresh",
44
+ value: "remote"
45
+ },
46
+ {
47
+ name: "Local - Stored in agents/<name>/knowledge/, version controlled, " \
48
+ "no refresh needed",
49
+ value: "local"
50
+ }
51
+ ])
52
+
31
53
  description = description_from_claude(short_description)
32
54
  normalized_agent_name = normalize_string(agent_name)
33
55
 
@@ -36,16 +58,17 @@ module Aircana
36
58
  description:,
37
59
  short_description:,
38
60
  model: normalize_string(model),
39
- color: normalize_string(color)
61
+ color: normalize_string(color),
62
+ kb_type:
40
63
  ).generate
41
64
 
42
65
  Aircana.human_logger.success "Agent created at #{file}"
43
66
 
44
67
  # Prompt for knowledge fetching
45
- prompt_for_knowledge_fetch(prompt, normalized_agent_name)
68
+ prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
46
69
 
47
70
  # Prompt for web URL fetching
48
- prompt_for_url_fetch(prompt, normalized_agent_name)
71
+ prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
49
72
 
50
73
  # Prompt for agent file review
51
74
  prompt_for_agent_review(prompt, file)
@@ -63,7 +86,7 @@ module Aircana
63
86
  print_agents_list(agent_folders)
64
87
  end
65
88
 
66
- def add_url(agent, url) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
89
+ def add_url(agent, url)
67
90
  normalized_agent = normalize_string(agent)
68
91
 
69
92
  unless agent_exists?(normalized_agent)
@@ -71,8 +94,11 @@ module Aircana
71
94
  exit 1
72
95
  end
73
96
 
97
+ # Get kb_type from manifest to know where to store
98
+ kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(normalized_agent)
99
+
74
100
  web = Aircana::Contexts::Web.new
75
- result = web.fetch_url_for(agent: normalized_agent, url: url)
101
+ result = web.fetch_url_for(agent: normalized_agent, url: url, kb_type: kb_type)
76
102
 
77
103
  if result
78
104
  # Update manifest with the new URL
@@ -101,7 +127,7 @@ module Aircana
101
127
  exit 1
102
128
  end
103
129
 
104
- def refresh_all # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
130
+ def refresh_all
105
131
  agent_names = all_agents
106
132
 
107
133
  if agent_names.empty?
@@ -115,11 +141,22 @@ module Aircana
115
141
  total: agent_names.size,
116
142
  successful: 0,
117
143
  failed: 0,
144
+ skipped: 0,
118
145
  total_pages: 0,
119
- failed_agents: []
146
+ failed_agents: [],
147
+ skipped_agents: []
120
148
  }
121
149
 
122
150
  agent_names.each do |agent_name|
151
+ # Check if this is a local knowledge base agent
152
+ kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name)
153
+ if kb_type == "local"
154
+ Aircana.human_logger.info "⊘ Skipping #{agent_name} (local knowledge base)"
155
+ results[:skipped] += 1
156
+ results[:skipped_agents] << agent_name
157
+ next
158
+ end
159
+
123
160
  result = refresh_single_agent(agent_name)
124
161
  if result[:success]
125
162
  results[:successful] += 1
@@ -133,11 +170,56 @@ module Aircana
133
170
  print_refresh_all_summary(results)
134
171
  end
135
172
 
173
+ def migrate_to_local
174
+ agent_names = all_agents
175
+
176
+ if agent_names.empty?
177
+ Aircana.human_logger.info "No agents found to migrate."
178
+ return
179
+ end
180
+
181
+ # Filter to only remote agents
182
+ remote_agents = agent_names.select do |agent_name|
183
+ Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name) == "remote"
184
+ end
185
+
186
+ if remote_agents.empty?
187
+ Aircana.human_logger.info "No remote agents found. All agents are already using local knowledge bases."
188
+ return
189
+ end
190
+
191
+ Aircana.human_logger.warn "⚠️ This will migrate #{remote_agents.size} agent(s) to local knowledge bases."
192
+ Aircana.human_logger.warn "Knowledge will be version-controlled and stored in " \
193
+ ".claude/agents/<agent>/knowledge/"
194
+ Aircana.human_logger.info ""
195
+
196
+ results = {
197
+ total: remote_agents.size,
198
+ successful: 0,
199
+ failed: 0,
200
+ migrated_agents: [],
201
+ failed_agents: []
202
+ }
203
+
204
+ remote_agents.each do |agent_name|
205
+ result = migrate_single_agent(agent_name)
206
+ if result[:success]
207
+ results[:successful] += 1
208
+ results[:migrated_agents] << agent_name
209
+ else
210
+ results[:failed] += 1
211
+ results[:failed_agents] << { name: agent_name, error: result[:error] }
212
+ end
213
+ end
214
+
215
+ print_migration_summary(results)
216
+ end
217
+
136
218
  private
137
219
 
138
- def perform_refresh(normalized_agent)
220
+ def perform_refresh(normalized_agent, kb_type = "remote")
139
221
  confluence = Aircana::Contexts::Confluence.new
140
- result = confluence.fetch_pages_for(agent: normalized_agent)
222
+ result = confluence.fetch_pages_for(agent: normalized_agent, kb_type: kb_type)
141
223
 
142
224
  log_refresh_result(normalized_agent, result[:pages_count])
143
225
  result
@@ -151,7 +233,7 @@ module Aircana
151
233
  end
152
234
  end
153
235
 
154
- def perform_manifest_aware_refresh(normalized_agent) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
236
+ def perform_manifest_aware_refresh(normalized_agent)
155
237
  total_pages = 0
156
238
  all_sources = []
157
239
 
@@ -185,7 +267,10 @@ module Aircana
185
267
  { pages_count: total_pages, sources: all_sources }
186
268
  end
187
269
 
188
- def ensure_gitignore_entry
270
+ def ensure_gitignore_entry(kb_type = "remote")
271
+ # Only add to gitignore for remote knowledge bases
272
+ return if kb_type == "local"
273
+
189
274
  gitignore_path = gitignore_file_path
190
275
  pattern = gitignore_pattern
191
276
 
@@ -263,24 +348,34 @@ module Aircana
263
348
  PROMPT
264
349
  end
265
350
 
266
- def prompt_for_knowledge_fetch(prompt, normalized_agent_name) # rubocop:disable Metrics/MethodLength
351
+ def prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
267
352
  return unless confluence_configured?
268
353
 
269
354
  if prompt.yes?("Would you like to fetch knowledge for this agent from Confluence now?")
270
355
  Aircana.human_logger.info "Fetching knowledge from Confluence..."
271
- result = perform_refresh(normalized_agent_name)
272
- ensure_gitignore_entry if result[:pages_count]&.positive?
356
+ result = perform_refresh(normalized_agent_name, kb_type)
357
+ ensure_gitignore_entry(kb_type) if result[:pages_count]&.positive?
273
358
  else
359
+ refresh_message = if kb_type == "local"
360
+ "fetch knowledge"
361
+ else
362
+ "run 'aircana agents refresh #{normalized_agent_name}'"
363
+ end
274
364
  Aircana.human_logger.info(
275
- "Skipping knowledge fetch. You can run 'aircana agents refresh #{normalized_agent_name}' later."
365
+ "Skipping knowledge fetch. You can #{refresh_message} later."
276
366
  )
277
367
  end
278
368
  rescue Aircana::Error => e
279
369
  Aircana.human_logger.warn "Failed to fetch knowledge: #{e.message}"
280
- Aircana.human_logger.info "You can try again later with 'aircana agents refresh #{normalized_agent_name}'"
370
+ refresh_message = if kb_type == "local"
371
+ "fetch knowledge"
372
+ else
373
+ "try again later with 'aircana agents refresh #{normalized_agent_name}'"
374
+ end
375
+ Aircana.human_logger.info "You can #{refresh_message}"
281
376
  end
282
377
 
283
- def prompt_for_url_fetch(prompt, normalized_agent_name) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
378
+ def prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
284
379
  return unless prompt.yes?("Would you like to add web URLs for this agent's knowledge base?")
285
380
 
286
381
  urls = []
@@ -301,11 +396,11 @@ module Aircana
301
396
  begin
302
397
  Aircana.human_logger.info "Fetching #{urls.size} URL(s)..."
303
398
  web = Aircana::Contexts::Web.new
304
- result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls)
399
+ result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls, kb_type: kb_type)
305
400
 
306
401
  if result[:pages_count].positive?
307
402
  Aircana.human_logger.success "Successfully fetched #{result[:pages_count]} URL(s)"
308
- ensure_gitignore_entry
403
+ ensure_gitignore_entry(kb_type)
309
404
  else
310
405
  Aircana.human_logger.warn "No URLs were successfully fetched"
311
406
  end
@@ -325,7 +420,7 @@ module Aircana
325
420
  open_file_in_editor(file_path)
326
421
  end
327
422
 
328
- def confluence_configured? # rubocop:disable Metrics/AbcSize
423
+ def confluence_configured?
329
424
  config = Aircana.configuration
330
425
 
331
426
  base_url_present = !config.confluence_base_url.nil? && !config.confluence_base_url.empty?
@@ -401,7 +496,7 @@ module Aircana
401
496
  find_agent_folders(agent_dir)
402
497
  end
403
498
 
404
- def refresh_single_agent(agent_name) # rubocop:disable Metrics/MethodLength
499
+ def refresh_single_agent(agent_name)
405
500
  Aircana.human_logger.info "Refreshing agent '#{agent_name}'..."
406
501
 
407
502
  begin
@@ -422,12 +517,125 @@ module Aircana
422
517
  end
423
518
  end
424
519
 
425
- def print_refresh_all_summary(results) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
520
+ def print_refresh_all_summary(results)
426
521
  Aircana.human_logger.info ""
427
522
  Aircana.human_logger.info "=== Refresh All Summary ==="
428
523
  Aircana.human_logger.success "✓ Successful: #{results[:successful]}/#{results[:total]} agents"
429
524
  Aircana.human_logger.success "✓ Total pages refreshed: #{results[:total_pages]}"
430
525
 
526
+ if results[:skipped].positive?
527
+ Aircana.human_logger.info "⊘ Skipped: #{results[:skipped]} agent(s) (local knowledge base)"
528
+ end
529
+
530
+ if results[:failed].positive?
531
+ Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
532
+ Aircana.human_logger.info ""
533
+ Aircana.human_logger.info "Failed agents:"
534
+ results[:failed_agents].each do |failed_agent|
535
+ Aircana.human_logger.error " - #{failed_agent[:name]}: #{failed_agent[:error]}"
536
+ end
537
+ end
538
+
539
+ Aircana.human_logger.info ""
540
+ end
541
+
542
+ def migrate_single_agent(agent_name)
543
+ Aircana.human_logger.info "Migrating agent '#{agent_name}'..."
544
+
545
+ begin
546
+ # Step 1: Refresh knowledge from sources to ensure we have latest
547
+ Aircana.human_logger.info " Refreshing knowledge from sources..."
548
+ perform_manifest_aware_refresh(agent_name)
549
+
550
+ # Step 2: Copy knowledge files from global to local directory
551
+ Aircana.human_logger.info " Copying knowledge files to local directory..."
552
+ copy_knowledge_files(agent_name)
553
+
554
+ # Step 3: Update manifest kb_type to "local"
555
+ Aircana.human_logger.info " Updating manifest..."
556
+ sources = Aircana::Contexts::Manifest.sources_from_manifest(agent_name)
557
+ Aircana::Contexts::Manifest.update_manifest(agent_name, sources, kb_type: "local")
558
+
559
+ # Step 4: Regenerate agent file with new kb_type
560
+ Aircana.human_logger.info " Regenerating agent file..."
561
+ regenerate_agent_file(agent_name)
562
+
563
+ Aircana.human_logger.success "✓ Successfully migrated '#{agent_name}'"
564
+ { success: true }
565
+ rescue StandardError => e
566
+ Aircana.human_logger.error "✗ Failed to migrate agent '#{agent_name}': #{e.message}"
567
+ { success: false, error: e.message }
568
+ end
569
+ end
570
+
571
+ def copy_knowledge_files(agent_name)
572
+ config = Aircana.configuration
573
+ source_dir = config.global_agent_knowledge_path(agent_name)
574
+ dest_dir = config.local_agent_knowledge_path(agent_name)
575
+
576
+ unless Dir.exist?(source_dir)
577
+ Aircana.human_logger.warn " No knowledge files found at #{source_dir}"
578
+ return
579
+ end
580
+
581
+ FileUtils.mkdir_p(dest_dir)
582
+
583
+ # Copy all markdown files
584
+ Dir.glob(File.join(source_dir, "*.md")).each do |file|
585
+ filename = File.basename(file)
586
+ dest_file = File.join(dest_dir, filename)
587
+ FileUtils.cp(file, dest_file)
588
+ Aircana.human_logger.info " Copied #{filename}"
589
+ end
590
+ end
591
+
592
+ def regenerate_agent_file(agent_name)
593
+ # Read existing agent file to get metadata
594
+ agent_file_path = File.join(Aircana.configuration.agents_dir, "#{agent_name}.md")
595
+ unless File.exist?(agent_file_path)
596
+ Aircana.human_logger.warn " Agent file not found at #{agent_file_path}, skipping regeneration"
597
+ return
598
+ end
599
+
600
+ # Parse frontmatter to get agent metadata
601
+ content = File.read(agent_file_path)
602
+ frontmatter_match = content.match(/^---\n(.*?)\n---\n(.*)$/m)
603
+ return unless frontmatter_match
604
+
605
+ frontmatter = frontmatter_match[1]
606
+ description = frontmatter_match[2]
607
+
608
+ # Parse YAML-like frontmatter
609
+ metadata = {}
610
+ frontmatter.lines.each do |line|
611
+ key, value = line.strip.split(":", 2)
612
+ metadata[key.strip] = value&.strip
613
+ end
614
+
615
+ # Generate new agent file with kb_type="local"
616
+ Generators::AgentsGenerator.new(
617
+ agent_name: agent_name,
618
+ description: description,
619
+ short_description: metadata["description"],
620
+ model: metadata["model"],
621
+ color: metadata["color"],
622
+ kb_type: "local"
623
+ ).generate
624
+ end
625
+
626
+ def print_migration_summary(results)
627
+ Aircana.human_logger.info ""
628
+ Aircana.human_logger.info "=== Migration Summary ==="
629
+ Aircana.human_logger.success "✓ Successfully migrated: #{results[:successful]}/#{results[:total]} agents"
630
+
631
+ if results[:successful].positive?
632
+ Aircana.human_logger.info ""
633
+ Aircana.human_logger.info "Migrated agents:"
634
+ results[:migrated_agents].each do |agent_name|
635
+ Aircana.human_logger.success " ✓ #{agent_name}"
636
+ end
637
+ end
638
+
431
639
  if results[:failed].positive?
432
640
  Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
433
641
  Aircana.human_logger.info ""
@@ -437,6 +645,12 @@ module Aircana
437
645
  end
438
646
  end
439
647
 
648
+ if results[:successful].positive?
649
+ Aircana.human_logger.info ""
650
+ Aircana.human_logger.warn "⚠️ Knowledge is now version-controlled in .claude/agents/<agent>/knowledge/"
651
+ Aircana.human_logger.info "Review and commit these changes to your repository."
652
+ end
653
+
440
654
  Aircana.human_logger.info ""
441
655
  end
442
656
  end
@@ -25,6 +25,7 @@ module Aircana
25
25
  end
26
26
 
27
27
  def run
28
+ clean_output_directories
28
29
  generators.each(&:generate)
29
30
  generate_default_agents
30
31
  generate_default_hooks
@@ -33,6 +34,16 @@ module Aircana
33
34
 
34
35
  private
35
36
 
37
+ def clean_output_directories
38
+ # Remove stale command files to prevent duplicates during init
39
+ commands_dir = File.join(Aircana.configuration.output_dir, "commands")
40
+ FileUtils.rm_rf(Dir.glob("#{commands_dir}/*")) if Dir.exist?(commands_dir)
41
+
42
+ # Remove stale agent files for consistency
43
+ agents_dir = File.join(Aircana.configuration.output_dir, "agents")
44
+ FileUtils.rm_rf(Dir.glob("#{agents_dir}/*")) if Dir.exist?(agents_dir)
45
+ end
46
+
36
47
  def generate_default_agents
37
48
  Aircana::Generators::AgentsGenerator.available_default_agents.each do |agent_name|
38
49
  Aircana::Generators::AgentsGenerator.create_default_agent(agent_name)
@@ -54,6 +54,25 @@ module Aircana
54
54
  File.join(@global_agents_dir, "#{plugin_name}-#{agent_name}", "knowledge")
55
55
  end
56
56
 
57
+ # Returns the local knowledge directory path for an agent
58
+ # Format: <plugin-root>/agents/<agent-name>/knowledge/
59
+ def local_agent_knowledge_path(agent_name)
60
+ File.join(@agents_dir, agent_name, "knowledge")
61
+ end
62
+
63
+ # Returns the appropriate knowledge directory path based on kb_type
64
+ # kb_type can be "remote" or "local"
65
+ def agent_knowledge_path(agent_name, kb_type)
66
+ case kb_type
67
+ when "local"
68
+ local_agent_knowledge_path(agent_name)
69
+ when "remote"
70
+ global_agent_knowledge_path(agent_name)
71
+ else
72
+ raise ArgumentError, "Invalid kb_type: #{kb_type}. Must be 'remote' or 'local'"
73
+ end
74
+ end
75
+
57
76
  private
58
77
 
59
78
  def setup_directory_paths
@@ -24,21 +24,22 @@ module Aircana
24
24
  @local_storage = Local.new
25
25
  end
26
26
 
27
- def fetch_pages_for(agent:)
27
+ def fetch_pages_for(agent:, kb_type: "remote")
28
28
  validate_configuration!
29
29
  setup_httparty
30
30
 
31
31
  pages = search_and_log_pages(agent)
32
32
  return { pages_count: 0, sources: [] } if pages.empty?
33
33
 
34
- sources = process_pages_with_manifest(pages, agent)
35
- create_or_update_manifest(agent, sources)
34
+ sources = process_pages_with_manifest(pages, agent, kb_type)
35
+ create_or_update_manifest(agent, sources, kb_type)
36
36
 
37
37
  { pages_count: pages.size, sources: sources }
38
38
  end
39
39
 
40
40
  def refresh_from_manifest(agent:)
41
41
  sources = Manifest.sources_from_manifest(agent)
42
+ kb_type = Manifest.kb_type_from_manifest(agent)
42
43
  return { pages_count: 0, sources: [] } if sources.empty?
43
44
 
44
45
  validate_configuration!
@@ -55,7 +56,7 @@ module Aircana
55
56
 
56
57
  return { pages_count: 0, sources: [] } if all_pages.empty?
57
58
 
58
- updated_sources = process_pages_with_manifest(all_pages, agent)
59
+ updated_sources = process_pages_with_manifest(all_pages, agent, kb_type)
59
60
 
60
61
  { pages_count: all_pages.size, sources: updated_sources }
61
62
  end
@@ -68,17 +69,17 @@ module Aircana
68
69
  pages
69
70
  end
70
71
 
71
- def process_pages(pages, agent)
72
+ def process_pages(pages, agent, kb_type = "remote")
72
73
  ProgressTracker.with_batch_progress(pages, "Processing pages") do |page, _index|
73
- store_page_as_markdown(page, agent)
74
+ store_page_as_markdown(page, agent, kb_type)
74
75
  end
75
76
  end
76
77
 
77
- def process_pages_with_manifest(pages, agent)
78
+ def process_pages_with_manifest(pages, agent, kb_type = "remote")
78
79
  page_metadata = []
79
80
 
80
81
  ProgressTracker.with_batch_progress(pages, "Processing pages") do |page, _index|
81
- store_page_as_markdown(page, agent)
82
+ store_page_as_markdown(page, agent, kb_type)
82
83
  page_metadata << extract_page_metadata(page)
83
84
  end
84
85
 
@@ -112,11 +113,11 @@ module Aircana
112
113
  ]
113
114
  end
114
115
 
115
- def create_or_update_manifest(agent, sources)
116
+ def create_or_update_manifest(agent, sources, kb_type = "remote")
116
117
  if Manifest.manifest_exists?(agent)
117
- Manifest.update_manifest(agent, sources)
118
+ Manifest.update_manifest(agent, sources, kb_type: kb_type)
118
119
  else
119
- Manifest.create_manifest(agent, sources)
120
+ Manifest.create_manifest(agent, sources, kb_type: kb_type)
120
121
  end
121
122
  end
122
123
 
@@ -21,14 +21,15 @@ module Aircana
21
21
  Aircana.human_logger.info "Found #{count} pages for agent '#{agent}'"
22
22
  end
23
23
 
24
- def store_page_as_markdown(page, agent)
24
+ def store_page_as_markdown(page, agent, kb_type = "remote")
25
25
  content = page&.dig("body", "storage", "value") || fetch_page_content(page&.[]("id"))
26
26
  markdown_content = convert_to_markdown(content)
27
27
 
28
28
  @local_storage.store_content(
29
29
  title: page&.[]("title"),
30
30
  content: markdown_content,
31
- agent: agent
31
+ agent: agent,
32
+ kb_type: kb_type
32
33
  )
33
34
  end
34
35
  end
@@ -5,8 +5,8 @@ require "fileutils"
5
5
  module Aircana
6
6
  module Contexts
7
7
  class Local
8
- def store_content(title:, content:, agent:)
9
- agent_dir = create_agent_knowledge_dir(agent)
8
+ def store_content(title:, content:, agent:, kb_type: "remote")
9
+ agent_dir = create_agent_knowledge_dir(agent, kb_type)
10
10
  filename = sanitize_filename(title)
11
11
  filepath = File.join(agent_dir, "#{filename}.md")
12
12
 
@@ -18,10 +18,10 @@ module Aircana
18
18
 
19
19
  private
20
20
 
21
- def create_agent_knowledge_dir(agent)
21
+ def create_agent_knowledge_dir(agent, kb_type = "remote")
22
22
  config = Aircana.configuration
23
- # Use global agents directory with plugin prefix
24
- agent_dir = config.global_agent_knowledge_path(agent)
23
+ # Route to appropriate directory based on kb_type
24
+ agent_dir = config.agent_knowledge_path(agent, kb_type)
25
25
 
26
26
  FileUtils.mkdir_p(agent_dir)
27
27
 
@@ -7,31 +7,36 @@ module Aircana
7
7
  module Contexts
8
8
  class Manifest
9
9
  class << self
10
- def create_manifest(agent, sources)
10
+ def create_manifest(agent, sources, kb_type: "remote")
11
11
  validate_sources(sources)
12
+ validate_kb_type(kb_type)
12
13
 
13
14
  manifest_path = manifest_path_for(agent)
14
- manifest_data = build_manifest_data(agent, sources)
15
+ manifest_data = build_manifest_data(agent, sources, kb_type)
15
16
 
16
17
  FileUtils.mkdir_p(File.dirname(manifest_path))
17
18
  File.write(manifest_path, JSON.pretty_generate(manifest_data))
18
19
 
19
- Aircana.human_logger.info "Created knowledge manifest for agent '#{agent}'"
20
+ Aircana.human_logger.info "Created knowledge manifest for agent '#{agent}' (kb_type: #{kb_type})"
20
21
  manifest_path
21
22
  end
22
23
 
23
- def update_manifest(agent, sources)
24
+ def update_manifest(agent, sources, kb_type: nil)
24
25
  validate_sources(sources)
25
26
 
26
27
  manifest_path = manifest_path_for(agent)
27
28
 
28
29
  if File.exist?(manifest_path)
29
30
  existing_data = JSON.parse(File.read(manifest_path))
30
- manifest_data = existing_data.merge({ "sources" => sources })
31
+ # Preserve existing kb_type unless explicitly provided
32
+ kb_type_to_use = kb_type || existing_data["kb_type"] || "remote"
33
+ manifest_data = existing_data.merge({ "sources" => sources, "kb_type" => kb_type_to_use })
31
34
  else
32
- manifest_data = build_manifest_data(agent, sources)
35
+ kb_type_to_use = kb_type || "remote"
36
+ manifest_data = build_manifest_data(agent, sources, kb_type_to_use)
33
37
  end
34
38
 
39
+ validate_kb_type(manifest_data["kb_type"])
35
40
  FileUtils.mkdir_p(File.dirname(manifest_path))
36
41
  File.write(manifest_path, JSON.pretty_generate(manifest_data))
37
42
  manifest_path
@@ -61,6 +66,13 @@ module Aircana
61
66
  manifest["sources"] || []
62
67
  end
63
68
 
69
+ def kb_type_from_manifest(agent)
70
+ manifest = read_manifest(agent)
71
+ return "remote" unless manifest
72
+
73
+ manifest["kb_type"] || "remote"
74
+ end
75
+
64
76
  def manifest_exists?(agent)
65
77
  File.exist?(manifest_path_for(agent))
66
78
  end
@@ -76,10 +88,11 @@ module Aircana
76
88
  File.join(Aircana.configuration.agent_knowledge_dir, agent)
77
89
  end
78
90
 
79
- def build_manifest_data(agent, sources)
91
+ def build_manifest_data(agent, sources, kb_type = "remote")
80
92
  {
81
93
  "version" => "1.0",
82
94
  "agent" => agent,
95
+ "kb_type" => kb_type,
83
96
  "sources" => sources
84
97
  }
85
98
  end
@@ -95,6 +108,10 @@ module Aircana
95
108
  raise ManifestError, "Unsupported manifest version: #{manifest_data["version"]}"
96
109
  end
97
110
 
111
+ # kb_type is optional for backward compatibility, defaults to "remote"
112
+ kb_type = manifest_data["kb_type"] || "remote"
113
+ validate_kb_type(kb_type)
114
+
98
115
  validate_sources(manifest_data["sources"])
99
116
  end
100
117
 
@@ -144,6 +161,13 @@ module Aircana
144
161
 
145
162
  raise ManifestError, "URL entry missing required field: url" unless url_entry.key?("url")
146
163
  end
164
+
165
+ def validate_kb_type(kb_type)
166
+ valid_types = %w[remote local]
167
+ return if valid_types.include?(kb_type)
168
+
169
+ raise ManifestError, "Invalid kb_type: #{kb_type}. Must be one of: #{valid_types.join(", ")}"
170
+ end
147
171
  end
148
172
  end
149
173