aircana 3.0.0 → 3.2.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 - Fetched from Confluence/web, stored in ~/.claude/agents/, " \
43
+ "requires refresh",
44
+ value: "remote"
45
+ },
46
+ {
47
+ name: "Local - Version controlled in plugin, auto-synced to ~/.claude/agents/ " \
48
+ "on session start",
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,20 @@ 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
 
67
+ # If local kb_type, ensure SessionStart hook is installed
68
+ ensure_local_knowledge_sync_hook if kb_type == "local"
69
+
44
70
  # Prompt for knowledge fetching
45
- prompt_for_knowledge_fetch(prompt, normalized_agent_name)
71
+ prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
46
72
 
47
73
  # Prompt for web URL fetching
48
- prompt_for_url_fetch(prompt, normalized_agent_name)
74
+ prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
49
75
 
50
76
  # Prompt for agent file review
51
77
  prompt_for_agent_review(prompt, file)
@@ -63,7 +89,7 @@ module Aircana
63
89
  print_agents_list(agent_folders)
64
90
  end
65
91
 
66
- def add_url(agent, url) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
92
+ def add_url(agent, url)
67
93
  normalized_agent = normalize_string(agent)
68
94
 
69
95
  unless agent_exists?(normalized_agent)
@@ -71,8 +97,11 @@ module Aircana
71
97
  exit 1
72
98
  end
73
99
 
100
+ # Get kb_type from manifest to know where to store
101
+ kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(normalized_agent)
102
+
74
103
  web = Aircana::Contexts::Web.new
75
- result = web.fetch_url_for(agent: normalized_agent, url: url)
104
+ result = web.fetch_url_for(agent: normalized_agent, url: url, kb_type: kb_type)
76
105
 
77
106
  if result
78
107
  # Update manifest with the new URL
@@ -101,7 +130,7 @@ module Aircana
101
130
  exit 1
102
131
  end
103
132
 
104
- def refresh_all # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
133
+ def refresh_all
105
134
  agent_names = all_agents
106
135
 
107
136
  if agent_names.empty?
@@ -115,11 +144,22 @@ module Aircana
115
144
  total: agent_names.size,
116
145
  successful: 0,
117
146
  failed: 0,
147
+ skipped: 0,
118
148
  total_pages: 0,
119
- failed_agents: []
149
+ failed_agents: [],
150
+ skipped_agents: []
120
151
  }
121
152
 
122
153
  agent_names.each do |agent_name|
154
+ # Check if this is a local knowledge base agent
155
+ kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name)
156
+ if kb_type == "local"
157
+ Aircana.human_logger.info "⊘ Skipping #{agent_name} (local knowledge base)"
158
+ results[:skipped] += 1
159
+ results[:skipped_agents] << agent_name
160
+ next
161
+ end
162
+
123
163
  result = refresh_single_agent(agent_name)
124
164
  if result[:success]
125
165
  results[:successful] += 1
@@ -133,11 +173,56 @@ module Aircana
133
173
  print_refresh_all_summary(results)
134
174
  end
135
175
 
176
+ def migrate_to_local
177
+ agent_names = all_agents
178
+
179
+ if agent_names.empty?
180
+ Aircana.human_logger.info "No agents found to migrate."
181
+ return
182
+ end
183
+
184
+ # Filter to only remote agents
185
+ remote_agents = agent_names.select do |agent_name|
186
+ Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name) == "remote"
187
+ end
188
+
189
+ if remote_agents.empty?
190
+ Aircana.human_logger.info "No remote agents found. All agents are already using local knowledge bases."
191
+ return
192
+ end
193
+
194
+ Aircana.human_logger.warn "⚠️ This will migrate #{remote_agents.size} agent(s) to local knowledge bases."
195
+ Aircana.human_logger.warn "Knowledge will be version-controlled in agents/<agent>/knowledge/"
196
+ Aircana.human_logger.warn "and auto-synced to ~/.claude/agents/ on session start"
197
+ Aircana.human_logger.info ""
198
+
199
+ results = {
200
+ total: remote_agents.size,
201
+ successful: 0,
202
+ failed: 0,
203
+ migrated_agents: [],
204
+ failed_agents: []
205
+ }
206
+
207
+ remote_agents.each do |agent_name|
208
+ result = migrate_single_agent(agent_name)
209
+ if result[:success]
210
+ results[:successful] += 1
211
+ results[:migrated_agents] << agent_name
212
+ else
213
+ results[:failed] += 1
214
+ results[:failed_agents] << { name: agent_name, error: result[:error] }
215
+ end
216
+ end
217
+
218
+ print_migration_summary(results)
219
+ end
220
+
136
221
  private
137
222
 
138
- def perform_refresh(normalized_agent)
223
+ def perform_refresh(normalized_agent, kb_type = "remote")
139
224
  confluence = Aircana::Contexts::Confluence.new
140
- result = confluence.fetch_pages_for(agent: normalized_agent)
225
+ result = confluence.fetch_pages_for(agent: normalized_agent, kb_type: kb_type)
141
226
 
142
227
  log_refresh_result(normalized_agent, result[:pages_count])
143
228
  result
@@ -151,7 +236,7 @@ module Aircana
151
236
  end
152
237
  end
153
238
 
154
- def perform_manifest_aware_refresh(normalized_agent) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
239
+ def perform_manifest_aware_refresh(normalized_agent)
155
240
  total_pages = 0
156
241
  all_sources = []
157
242
 
@@ -185,33 +270,62 @@ module Aircana
185
270
  { pages_count: total_pages, sources: all_sources }
186
271
  end
187
272
 
188
- def ensure_gitignore_entry
273
+ def ensure_gitignore_entry(kb_type = "remote")
189
274
  gitignore_path = gitignore_file_path
190
- pattern = gitignore_pattern
191
275
 
276
+ if kb_type == "local"
277
+ # For local agents, ensure version-controlled knowledge is not ignored
278
+ ensure_local_knowledge_not_ignored(gitignore_path)
279
+ else
280
+ # For remote agents, add ignore pattern for .claude/agents/*/knowledge/
281
+ ensure_remote_knowledge_ignored(gitignore_path)
282
+ end
283
+ rescue StandardError => e
284
+ Aircana.human_logger.warn "Could not update .gitignore: #{e.message}"
285
+ end
286
+
287
+ def ensure_remote_knowledge_ignored(gitignore_path)
288
+ pattern = remote_knowledge_pattern
192
289
  return if gitignore_has_pattern?(gitignore_path, pattern)
193
290
 
194
291
  append_to_gitignore(gitignore_path, pattern)
195
- Aircana.human_logger.success "Added knowledge directories to .gitignore"
196
- rescue StandardError => e
197
- Aircana.human_logger.warn "Could not update .gitignore: #{e.message}"
198
- Aircana.human_logger.info "Manually add: #{pattern}"
292
+ Aircana.human_logger.success "Added remote knowledge directories to .gitignore"
293
+ end
294
+
295
+ def ensure_local_knowledge_not_ignored(gitignore_path)
296
+ negation_pattern = local_knowledge_negation_pattern
297
+ return if gitignore_has_pattern?(gitignore_path, negation_pattern)
298
+
299
+ # Add comment and negation pattern
300
+ comment = "# Local agent knowledge IS version controlled (don't ignore)"
301
+ content_to_append = "\n#{comment}\n#{negation_pattern}\n"
302
+
303
+ existing_content = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
304
+ needs_newline = !existing_content.empty? && !existing_content.end_with?("\n")
305
+ content_to_append = "\n#{content_to_append}" if needs_newline
306
+
307
+ File.open(gitignore_path, "a") { |f| f.write(content_to_append) }
308
+ Aircana.human_logger.success "Added local knowledge negation to .gitignore"
199
309
  end
200
310
 
201
311
  def gitignore_file_path
202
312
  File.join(Aircana.configuration.project_dir, ".gitignore")
203
313
  end
204
314
 
205
- def gitignore_pattern
315
+ def remote_knowledge_pattern
206
316
  ".claude/agents/*/knowledge/"
207
317
  end
208
318
 
319
+ def local_knowledge_negation_pattern
320
+ "!agents/*/knowledge/"
321
+ end
322
+
209
323
  def gitignore_has_pattern?(gitignore_path, pattern)
210
324
  return false unless File.exist?(gitignore_path)
211
325
 
212
326
  content = File.read(gitignore_path)
213
327
  if content.lines.any? { |line| line.strip == pattern }
214
- Aircana.human_logger.info "Knowledge directories already in .gitignore"
328
+ Aircana.human_logger.info "Pattern '#{pattern}' already in .gitignore"
215
329
  true
216
330
  else
217
331
  false
@@ -263,24 +377,34 @@ module Aircana
263
377
  PROMPT
264
378
  end
265
379
 
266
- def prompt_for_knowledge_fetch(prompt, normalized_agent_name) # rubocop:disable Metrics/MethodLength
380
+ def prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
267
381
  return unless confluence_configured?
268
382
 
269
383
  if prompt.yes?("Would you like to fetch knowledge for this agent from Confluence now?")
270
384
  Aircana.human_logger.info "Fetching knowledge from Confluence..."
271
- result = perform_refresh(normalized_agent_name)
272
- ensure_gitignore_entry if result[:pages_count]&.positive?
385
+ result = perform_refresh(normalized_agent_name, kb_type)
386
+ ensure_gitignore_entry(kb_type) if result[:pages_count]&.positive?
273
387
  else
388
+ refresh_message = if kb_type == "local"
389
+ "fetch knowledge"
390
+ else
391
+ "run 'aircana agents refresh #{normalized_agent_name}'"
392
+ end
274
393
  Aircana.human_logger.info(
275
- "Skipping knowledge fetch. You can run 'aircana agents refresh #{normalized_agent_name}' later."
394
+ "Skipping knowledge fetch. You can #{refresh_message} later."
276
395
  )
277
396
  end
278
397
  rescue Aircana::Error => e
279
398
  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}'"
399
+ refresh_message = if kb_type == "local"
400
+ "fetch knowledge"
401
+ else
402
+ "try again later with 'aircana agents refresh #{normalized_agent_name}'"
403
+ end
404
+ Aircana.human_logger.info "You can #{refresh_message}"
281
405
  end
282
406
 
283
- def prompt_for_url_fetch(prompt, normalized_agent_name) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
407
+ def prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
284
408
  return unless prompt.yes?("Would you like to add web URLs for this agent's knowledge base?")
285
409
 
286
410
  urls = []
@@ -301,11 +425,11 @@ module Aircana
301
425
  begin
302
426
  Aircana.human_logger.info "Fetching #{urls.size} URL(s)..."
303
427
  web = Aircana::Contexts::Web.new
304
- result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls)
428
+ result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls, kb_type: kb_type)
305
429
 
306
430
  if result[:pages_count].positive?
307
431
  Aircana.human_logger.success "Successfully fetched #{result[:pages_count]} URL(s)"
308
- ensure_gitignore_entry
432
+ ensure_gitignore_entry(kb_type)
309
433
  else
310
434
  Aircana.human_logger.warn "No URLs were successfully fetched"
311
435
  end
@@ -325,7 +449,7 @@ module Aircana
325
449
  open_file_in_editor(file_path)
326
450
  end
327
451
 
328
- def confluence_configured? # rubocop:disable Metrics/AbcSize
452
+ def confluence_configured?
329
453
  config = Aircana.configuration
330
454
 
331
455
  base_url_present = !config.confluence_base_url.nil? && !config.confluence_base_url.empty?
@@ -401,7 +525,7 @@ module Aircana
401
525
  find_agent_folders(agent_dir)
402
526
  end
403
527
 
404
- def refresh_single_agent(agent_name) # rubocop:disable Metrics/MethodLength
528
+ def refresh_single_agent(agent_name)
405
529
  Aircana.human_logger.info "Refreshing agent '#{agent_name}'..."
406
530
 
407
531
  begin
@@ -422,12 +546,16 @@ module Aircana
422
546
  end
423
547
  end
424
548
 
425
- def print_refresh_all_summary(results) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
549
+ def print_refresh_all_summary(results)
426
550
  Aircana.human_logger.info ""
427
551
  Aircana.human_logger.info "=== Refresh All Summary ==="
428
552
  Aircana.human_logger.success "✓ Successful: #{results[:successful]}/#{results[:total]} agents"
429
553
  Aircana.human_logger.success "✓ Total pages refreshed: #{results[:total_pages]}"
430
554
 
555
+ if results[:skipped].positive?
556
+ Aircana.human_logger.info "⊘ Skipped: #{results[:skipped]} agent(s) (local knowledge base)"
557
+ end
558
+
431
559
  if results[:failed].positive?
432
560
  Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
433
561
  Aircana.human_logger.info ""
@@ -439,6 +567,166 @@ module Aircana
439
567
 
440
568
  Aircana.human_logger.info ""
441
569
  end
570
+
571
+ def migrate_single_agent(agent_name)
572
+ Aircana.human_logger.info "Migrating agent '#{agent_name}'..."
573
+
574
+ begin
575
+ # Step 1: Refresh knowledge from sources to ensure we have latest
576
+ Aircana.human_logger.info " Refreshing knowledge from sources..."
577
+ perform_manifest_aware_refresh(agent_name)
578
+
579
+ # Step 2: Copy knowledge files from global to local directory
580
+ Aircana.human_logger.info " Copying knowledge files to local directory..."
581
+ copy_knowledge_files(agent_name)
582
+
583
+ # Step 3: Update manifest kb_type to "local"
584
+ Aircana.human_logger.info " Updating manifest..."
585
+ sources = Aircana::Contexts::Manifest.sources_from_manifest(agent_name)
586
+ Aircana::Contexts::Manifest.update_manifest(agent_name, sources, kb_type: "local")
587
+
588
+ # Step 4: Regenerate agent file with new kb_type
589
+ Aircana.human_logger.info " Regenerating agent file..."
590
+ regenerate_agent_file(agent_name)
591
+
592
+ # Step 5: Ensure SessionStart hook is installed (only install once for all local agents)
593
+ Aircana.human_logger.info " Ensuring SessionStart hook is installed..."
594
+ ensure_local_knowledge_sync_hook
595
+
596
+ Aircana.human_logger.success "✓ Successfully migrated '#{agent_name}'"
597
+ { success: true }
598
+ rescue StandardError => e
599
+ Aircana.human_logger.error "✗ Failed to migrate agent '#{agent_name}': #{e.message}"
600
+ { success: false, error: e.message }
601
+ end
602
+ end
603
+
604
+ def copy_knowledge_files(agent_name)
605
+ config = Aircana.configuration
606
+ source_dir = config.global_agent_knowledge_path(agent_name)
607
+ dest_dir = config.local_agent_knowledge_path(agent_name)
608
+
609
+ unless Dir.exist?(source_dir)
610
+ Aircana.human_logger.warn " No knowledge files found at #{source_dir}"
611
+ return
612
+ end
613
+
614
+ FileUtils.mkdir_p(dest_dir)
615
+
616
+ # Copy all markdown files
617
+ Dir.glob(File.join(source_dir, "*.md")).each do |file|
618
+ filename = File.basename(file)
619
+ dest_file = File.join(dest_dir, filename)
620
+ FileUtils.cp(file, dest_file)
621
+ Aircana.human_logger.info " Copied #{filename}"
622
+ end
623
+ end
624
+
625
+ def regenerate_agent_file(agent_name)
626
+ # Read existing agent file to get metadata
627
+ agent_file_path = File.join(Aircana.configuration.agents_dir, "#{agent_name}.md")
628
+ unless File.exist?(agent_file_path)
629
+ Aircana.human_logger.warn " Agent file not found at #{agent_file_path}, skipping regeneration"
630
+ return
631
+ end
632
+
633
+ # Parse frontmatter to get agent metadata
634
+ content = File.read(agent_file_path)
635
+ frontmatter_match = content.match(/^---\n(.*?)\n---\n(.*)$/m)
636
+ return unless frontmatter_match
637
+
638
+ frontmatter = frontmatter_match[1]
639
+ description = frontmatter_match[2]
640
+
641
+ # Parse YAML-like frontmatter
642
+ metadata = {}
643
+ frontmatter.lines.each do |line|
644
+ key, value = line.strip.split(":", 2)
645
+ metadata[key.strip] = value&.strip
646
+ end
647
+
648
+ # Generate new agent file with kb_type="local"
649
+ Generators::AgentsGenerator.new(
650
+ agent_name: agent_name,
651
+ description: description,
652
+ short_description: metadata["description"],
653
+ model: metadata["model"],
654
+ color: metadata["color"],
655
+ kb_type: "local"
656
+ ).generate
657
+ end
658
+
659
+ def print_migration_summary(results)
660
+ Aircana.human_logger.info ""
661
+ Aircana.human_logger.info "=== Migration Summary ==="
662
+ Aircana.human_logger.success "✓ Successfully migrated: #{results[:successful]}/#{results[:total]} agents"
663
+
664
+ if results[:successful].positive?
665
+ Aircana.human_logger.info ""
666
+ Aircana.human_logger.info "Migrated agents:"
667
+ results[:migrated_agents].each do |agent_name|
668
+ Aircana.human_logger.success " ✓ #{agent_name}"
669
+ end
670
+ end
671
+
672
+ if results[:failed].positive?
673
+ Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
674
+ Aircana.human_logger.info ""
675
+ Aircana.human_logger.info "Failed agents:"
676
+ results[:failed_agents].each do |failed_agent|
677
+ Aircana.human_logger.error " - #{failed_agent[:name]}: #{failed_agent[:error]}"
678
+ end
679
+ end
680
+
681
+ if results[:successful].positive?
682
+ Aircana.human_logger.info ""
683
+ Aircana.human_logger.warn "⚠️ Knowledge is now version-controlled in agents/<agent>/knowledge/"
684
+ Aircana.human_logger.info "A SessionStart hook has been added to auto-sync knowledge to ~/.claude/agents/"
685
+ Aircana.human_logger.info "Review and commit these changes to your repository."
686
+ end
687
+
688
+ Aircana.human_logger.info ""
689
+ end
690
+
691
+ def ensure_local_knowledge_sync_hook
692
+ hooks_manifest = Aircana::HooksManifest.new(Aircana.configuration.plugin_root)
693
+
694
+ # Check if sync hook already exists
695
+ current_hooks = hooks_manifest.read || {}
696
+ session_start_hooks = current_hooks["SessionStart"] || []
697
+
698
+ # Check if our sync script already exists
699
+ sync_hook_exists = session_start_hooks.any? do |hook_group|
700
+ hook_group["hooks"]&.any? { |h| h["command"]&.include?("sync_local_knowledge.sh") }
701
+ end
702
+
703
+ return if sync_hook_exists
704
+
705
+ # Generate the sync script
706
+ generate_sync_script
707
+
708
+ # Add hook to manifest
709
+ hook_entry = {
710
+ "type" => "command",
711
+ "command" => "./scripts/sync_local_knowledge.sh"
712
+ }
713
+
714
+ hooks_manifest.add_hook(event: "SessionStart", hook_entry: hook_entry)
715
+ Aircana.human_logger.success "Added SessionStart hook to sync local knowledge bases"
716
+ end
717
+
718
+ def generate_sync_script
719
+ script_path = File.join(Aircana.configuration.scripts_dir, "sync_local_knowledge.sh")
720
+ return if File.exist?(script_path)
721
+
722
+ template_path = File.join(File.dirname(__FILE__), "..", "..", "templates", "hooks",
723
+ "sync_local_knowledge.erb")
724
+ template_content = File.read(template_path)
725
+
726
+ FileUtils.mkdir_p(Aircana.configuration.scripts_dir)
727
+ File.write(script_path, ERB.new(template_content).result)
728
+ File.chmod(0o755, script_path)
729
+ end
442
730
  end
443
731
  end
444
732
  end
@@ -48,12 +48,32 @@ module Aircana
48
48
  File.basename(@plugin_root).downcase.gsub(/[^a-z0-9]+/, "-")
49
49
  end
50
50
 
51
- # Returns the global knowledge directory path for an agent
51
+ # Returns the global knowledge directory path for an agent (runtime location)
52
52
  # Format: ~/.claude/agents/<plugin-name>-<agent-name>/knowledge/
53
+ # Both local and remote agents use this path at runtime
53
54
  def global_agent_knowledge_path(agent_name)
54
55
  File.join(@global_agents_dir, "#{plugin_name}-#{agent_name}", "knowledge")
55
56
  end
56
57
 
58
+ # Returns the local knowledge directory path for an agent (version-controlled source)
59
+ # Format: <plugin-root>/agents/<agent-name>/knowledge/
60
+ # Used only for local agents as the source that gets synced to global path
61
+ def local_agent_knowledge_path(agent_name)
62
+ File.join(@agents_dir, agent_name, "knowledge")
63
+ end
64
+
65
+ # Returns the appropriate knowledge directory path based on kb_type
66
+ # For runtime access, both local and remote agents use global_agent_knowledge_path
67
+ # Local agents are synced there via SessionStart hook from their version-controlled source
68
+ # kb_type can be "remote" or "local" but is not used (kept for backward compatibility)
69
+ def agent_knowledge_path(agent_name, _kb_type = nil)
70
+ # Both types use the global path at runtime
71
+ # The difference is how the content gets there:
72
+ # - Remote: via 'aircana agents refresh' from Confluence/web
73
+ # - Local: via SessionStart hook from version-controlled agents/<name>/knowledge/
74
+ global_agent_knowledge_path(agent_name)
75
+ end
76
+
57
77
  private
58
78
 
59
79
  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