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.
- checksums.yaml +4 -4
- data/.rspec_status +188 -188
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +55 -0
- data/lib/aircana/cli/app.rb +5 -0
- data/lib/aircana/cli/commands/agents.rb +318 -30
- data/lib/aircana/configuration.rb +21 -1
- data/lib/aircana/contexts/confluence.rb +12 -11
- data/lib/aircana/contexts/confluence_content.rb +3 -2
- data/lib/aircana/contexts/local.rb +5 -5
- data/lib/aircana/contexts/manifest.rb +31 -7
- data/lib/aircana/contexts/web.rb +13 -11
- data/lib/aircana/generators/agents_generator.rb +6 -3
- data/lib/aircana/templates/hooks/sync_local_knowledge.erb +86 -0
- data/lib/aircana/version.rb +1 -1
- data/{agents → spec_target_1760656566_428/agents}/test-agent/manifest.json +1 -0
- data/spec_target_1760656588_38/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656647_612/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656660_113/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656689_268/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656710_387/agents/test-agent/manifest.json +16 -0
- metadata +9 -9
- data/agents/apply_feedback.md +0 -92
- data/agents/executor.md +0 -85
- data/agents/jira.md +0 -46
- data/agents/planner.md +0 -64
- data/agents/reviewer.md +0 -95
- data/agents/sub-agent-coordinator.md +0 -91
@@ -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
|
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)
|
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
|
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)
|
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
|
-
|
197
|
-
|
198
|
-
|
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
|
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 "
|
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)
|
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
|
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
|
-
|
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
|
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?
|
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)
|
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)
|
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
|
-
#
|
24
|
-
agent_dir = config.
|
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
|
|