aircana 4.0.0.rc1 → 4.0.0.rc3
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 +185 -201
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +7 -8
- data/README.md +4 -13
- data/lib/aircana/cli/app.rb +0 -31
- data/lib/aircana/cli/commands/generate.rb +19 -14
- data/lib/aircana/cli/commands/init.rb +3 -31
- data/lib/aircana/cli/commands/kb.rb +21 -10
- data/lib/aircana/cli/help_formatter.rb +1 -2
- data/lib/aircana/contexts/confluence_content.rb +44 -1
- data/lib/aircana/generators/hooks_generator.rb +2 -2
- data/lib/aircana/generators/skills_generator.rb +69 -1
- data/lib/aircana/templates/hooks/refresh_skills.erb +121 -0
- data/lib/aircana/version.rb +1 -1
- metadata +4 -11
- data/commands/apply-feedback.md +0 -15
- data/commands/ask-expert.md +0 -42
- data/commands/execute.md +0 -13
- data/commands/plan.md +0 -33
- data/commands/record.md +0 -17
- data/commands/review.md +0 -12
- data/hooks/hooks.json +0 -31
- data/lib/aircana/cli/commands/hooks.rb +0 -276
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative "../../generators/
|
5
|
-
require_relative "../../generators/
|
6
|
-
require_relative "../../generators/
|
7
|
-
require_relative "../../generators/
|
8
|
-
require_relative "../../generators/
|
3
|
+
# Commands that reference old agent system - commented out for now
|
4
|
+
# require_relative "../../generators/plan_command_generator"
|
5
|
+
# require_relative "../../generators/record_command_generator"
|
6
|
+
# require_relative "../../generators/execute_command_generator"
|
7
|
+
# require_relative "../../generators/review_command_generator"
|
8
|
+
# require_relative "../../generators/apply_feedback_command_generator"
|
9
|
+
# require_relative "../../generators/ask_expert_command_generator"
|
9
10
|
require_relative "../../generators/hooks_generator"
|
10
11
|
|
11
12
|
module Aircana
|
@@ -13,14 +14,18 @@ module Aircana
|
|
13
14
|
module Generate
|
14
15
|
class << self
|
15
16
|
def generators
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
17
|
+
# No default commands for now - they referenced the old agent system
|
18
|
+
@generators ||= []
|
19
|
+
|
20
|
+
# TODO: Re-enable these when we have a new command system
|
21
|
+
# @generators ||= [
|
22
|
+
# Aircana::Generators::PlanCommandGenerator.new,
|
23
|
+
# Aircana::Generators::RecordCommandGenerator.new,
|
24
|
+
# Aircana::Generators::ExecuteCommandGenerator.new,
|
25
|
+
# Aircana::Generators::ReviewCommandGenerator.new,
|
26
|
+
# Aircana::Generators::ApplyFeedbackCommandGenerator.new,
|
27
|
+
# Aircana::Generators::AskExpertCommandGenerator.new
|
28
|
+
# ]
|
24
29
|
end
|
25
30
|
|
26
31
|
def run
|
@@ -132,7 +132,6 @@ module Aircana
|
|
132
132
|
Aircana.create_dir_if_needed(commands_dir)
|
133
133
|
|
134
134
|
copy_command_files(commands_dir)
|
135
|
-
install_default_agents
|
136
135
|
end
|
137
136
|
|
138
137
|
def copy_command_files(destination_dir)
|
@@ -142,33 +141,6 @@ module Aircana
|
|
142
141
|
end
|
143
142
|
end
|
144
143
|
|
145
|
-
def install_default_agents
|
146
|
-
agents_dir = Aircana.configuration.agents_dir
|
147
|
-
Aircana.create_dir_if_needed(agents_dir)
|
148
|
-
|
149
|
-
copy_agent_files(agents_dir)
|
150
|
-
end
|
151
|
-
|
152
|
-
def copy_agent_files(destination_dir)
|
153
|
-
agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
|
154
|
-
Dir.glob(agent_files_pattern).each do |file|
|
155
|
-
agent_name = File.basename(file, ".md")
|
156
|
-
next unless default_agent?(agent_name)
|
157
|
-
|
158
|
-
destination_file = File.join(destination_dir, File.basename(file))
|
159
|
-
# Skip copying if source and destination are the same
|
160
|
-
next if File.expand_path(file) == File.expand_path(destination_file)
|
161
|
-
|
162
|
-
Aircana.human_logger.success("Installing default agent: #{agent_name}")
|
163
|
-
FileUtils.cp(file, destination_dir)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
def default_agent?(agent_name)
|
168
|
-
require_relative "../../generators/agents_generator"
|
169
|
-
Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
|
170
|
-
end
|
171
|
-
|
172
144
|
def install_hooks
|
173
145
|
scripts_dir = Aircana.configuration.scripts_dir
|
174
146
|
return unless Dir.exist?(scripts_dir)
|
@@ -201,7 +173,7 @@ module Aircana
|
|
201
173
|
"post_tool_use" => { event: "PostToolUse", matcher: nil },
|
202
174
|
"user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
|
203
175
|
"session_start" => { event: "SessionStart", matcher: nil },
|
204
|
-
"
|
176
|
+
"refresh_skills" => { event: "SessionStart", matcher: nil },
|
205
177
|
"notification_sqs" => { event: "Notification", matcher: nil },
|
206
178
|
"rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
|
207
179
|
"rspec_test" => { event: "PostToolUse", matcher: "Bash" },
|
@@ -265,11 +237,11 @@ module Aircana
|
|
265
237
|
Aircana.human_logger.info("\nPlugin structure:")
|
266
238
|
Aircana.human_logger.info(" .claude-plugin/plugin.json - Plugin metadata")
|
267
239
|
Aircana.human_logger.info(" commands/ - Slash commands")
|
268
|
-
Aircana.human_logger.info(" agents/ -
|
240
|
+
Aircana.human_logger.info(" agents/ - Knowledge bases")
|
269
241
|
Aircana.human_logger.info(" hooks/ - Event hook configurations")
|
270
242
|
Aircana.human_logger.info(" scripts/ - Hook scripts and utilities")
|
271
243
|
Aircana.human_logger.info("\nNext steps:")
|
272
|
-
Aircana.human_logger.info(" - Create
|
244
|
+
Aircana.human_logger.info(" - Create knowledge bases: aircana kb create")
|
273
245
|
Aircana.human_logger.info(" - Install plugin in Claude Code")
|
274
246
|
Aircana.human_logger.info(" - Run: aircana plugin info")
|
275
247
|
end
|
@@ -29,7 +29,8 @@ module Aircana
|
|
29
29
|
def create # rubocop:disable Metrics/MethodLength
|
30
30
|
prompt = TTY::Prompt.new
|
31
31
|
|
32
|
-
kb_name = prompt.ask("
|
32
|
+
kb_name = prompt.ask("What topic should this knowledge base cover?",
|
33
|
+
default: "e.g., 'Canvas Backend Database', 'API Design'")
|
33
34
|
short_description = prompt.ask("Briefly describe what this KB contains:")
|
34
35
|
|
35
36
|
# Prompt for knowledge base type
|
@@ -48,13 +49,14 @@ module Aircana
|
|
48
49
|
normalized_kb_name = normalize_string(kb_name)
|
49
50
|
|
50
51
|
# Prompt for knowledge fetching
|
51
|
-
prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
|
52
|
+
fetched_confluence = prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
|
52
53
|
|
53
54
|
# Prompt for web URL fetching
|
54
|
-
prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
|
55
|
+
fetched_urls = prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
|
55
56
|
|
56
|
-
# Generate SKILL.md
|
57
|
-
|
57
|
+
# Generate SKILL.md if no content was fetched during the prompts
|
58
|
+
# (the prompt functions already generate it when they successfully fetch content)
|
59
|
+
regenerate_skill_md(normalized_kb_name, short_description) unless fetched_confluence || fetched_urls
|
58
60
|
|
59
61
|
# If remote kb_type, ensure SessionStart hook is installed
|
60
62
|
ensure_remote_knowledge_refresh_hook if kb_type == "remote"
|
@@ -326,7 +328,7 @@ module Aircana
|
|
326
328
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
327
329
|
# rubocop:disable Metrics/PerceivedComplexity
|
328
330
|
def prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
|
329
|
-
return unless confluence_configured?
|
331
|
+
return false unless confluence_configured?
|
330
332
|
|
331
333
|
if prompt.yes?("Would you like to fetch knowledge for this KB from Confluence now?")
|
332
334
|
Aircana.human_logger.info "Fetching knowledge from Confluence..."
|
@@ -340,8 +342,11 @@ module Aircana
|
|
340
342
|
end
|
341
343
|
|
342
344
|
result = perform_refresh(normalized_kb_name, kb_type, label: label)
|
343
|
-
|
344
|
-
|
345
|
+
if result[:pages_count]&.positive?
|
346
|
+
ensure_gitignore_entry(kb_type)
|
347
|
+
regenerate_skill_md(normalized_kb_name, short_description)
|
348
|
+
return true
|
349
|
+
end
|
345
350
|
else
|
346
351
|
refresh_message = if kb_type == "local"
|
347
352
|
"fetch knowledge"
|
@@ -352,6 +357,8 @@ module Aircana
|
|
352
357
|
"Skipping knowledge fetch. You can #{refresh_message} later."
|
353
358
|
)
|
354
359
|
end
|
360
|
+
|
361
|
+
false
|
355
362
|
rescue Aircana::Error => e
|
356
363
|
Aircana.human_logger.warn "Failed to fetch knowledge: #{e.message}"
|
357
364
|
refresh_message = if kb_type == "local"
|
@@ -360,6 +367,7 @@ module Aircana
|
|
360
367
|
"try again later with 'aircana kb refresh #{normalized_kb_name}'"
|
361
368
|
end
|
362
369
|
Aircana.human_logger.info "You can #{refresh_message}"
|
370
|
+
false
|
363
371
|
end
|
364
372
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
365
373
|
# rubocop:enable Metrics/PerceivedComplexity
|
@@ -367,7 +375,7 @@ module Aircana
|
|
367
375
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
368
376
|
# rubocop:disable Metrics/PerceivedComplexity
|
369
377
|
def prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
|
370
|
-
return unless prompt.yes?("Would you like to add web URLs for this KB's knowledge base?")
|
378
|
+
return false unless prompt.yes?("Would you like to add web URLs for this KB's knowledge base?")
|
371
379
|
|
372
380
|
urls = []
|
373
381
|
loop do
|
@@ -382,7 +390,7 @@ module Aircana
|
|
382
390
|
end
|
383
391
|
end
|
384
392
|
|
385
|
-
return if urls.empty?
|
393
|
+
return false if urls.empty?
|
386
394
|
|
387
395
|
begin
|
388
396
|
Aircana.human_logger.info "Fetching #{urls.size} URL(s)..."
|
@@ -393,6 +401,7 @@ module Aircana
|
|
393
401
|
Aircana.human_logger.success "Successfully fetched #{result[:pages_count]} URL(s)"
|
394
402
|
ensure_gitignore_entry(kb_type)
|
395
403
|
regenerate_skill_md(normalized_kb_name)
|
404
|
+
return true
|
396
405
|
else
|
397
406
|
Aircana.human_logger.warn "No URLs were successfully fetched"
|
398
407
|
end
|
@@ -402,6 +411,8 @@ module Aircana
|
|
402
411
|
"You can add URLs later with 'aircana kb add-url #{normalized_kb_name} <URL>'"
|
403
412
|
)
|
404
413
|
end
|
414
|
+
|
415
|
+
false
|
405
416
|
end
|
406
417
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
407
418
|
# rubocop:enable Metrics/PerceivedComplexity
|
@@ -14,9 +14,52 @@ module Aircana
|
|
14
14
|
def convert_to_markdown(html_content)
|
15
15
|
return "" if html_content.nil? || html_content.empty?
|
16
16
|
|
17
|
-
|
17
|
+
# Preprocess Confluence macros before converting to Markdown
|
18
|
+
cleaned_html = preprocess_confluence_macros(html_content)
|
19
|
+
ReverseMarkdown.convert(cleaned_html, github_flavored: true)
|
18
20
|
end
|
19
21
|
|
22
|
+
# rubocop:disable Layout/LineLength, Metrics/MethodLength
|
23
|
+
def preprocess_confluence_macros(html)
|
24
|
+
# Process Confluence structured macros to make them compatible with Markdown conversion
|
25
|
+
cleaned = html.dup
|
26
|
+
|
27
|
+
# Remove empty code blocks (common issue with Confluence API)
|
28
|
+
cleaned.gsub!(
|
29
|
+
%r{<ac:structured-macro[^>]*ac:name="code"[^>]*>.*?<ac:plain-text-body>\s*</ac:plain-text-body>.*?</ac:structured-macro>}m, ""
|
30
|
+
)
|
31
|
+
|
32
|
+
# Convert panel macros to blockquotes, preserving inner content
|
33
|
+
cleaned.gsub!(
|
34
|
+
%r{<ac:structured-macro[^>]*ac:name="panel"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote>\1</blockquote>'
|
35
|
+
)
|
36
|
+
|
37
|
+
# Convert info/note/warning macros to blockquotes with indicators
|
38
|
+
cleaned.gsub!(
|
39
|
+
%r{<ac:structured-macro[^>]*ac:name="info"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>ℹ️ Info:</strong> \1</blockquote>'
|
40
|
+
)
|
41
|
+
cleaned.gsub!(
|
42
|
+
%r{<ac:structured-macro[^>]*ac:name="note"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>📝 Note:</strong> \1</blockquote>'
|
43
|
+
)
|
44
|
+
cleaned.gsub!(
|
45
|
+
%r{<ac:structured-macro[^>]*ac:name="warning"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>⚠️ Warning:</strong> \1</blockquote>'
|
46
|
+
)
|
47
|
+
|
48
|
+
# Strip other structured macros but preserve rich text body content
|
49
|
+
cleaned.gsub!(
|
50
|
+
%r{<ac:structured-macro[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '\1'
|
51
|
+
)
|
52
|
+
|
53
|
+
# Remove any remaining Confluence-specific tags
|
54
|
+
cleaned.gsub!(%r{</?ac:[^>]*>}m, "")
|
55
|
+
|
56
|
+
# Clean up Confluence parameter tags
|
57
|
+
cleaned.gsub!(%r{<ac:parameter[^>]*>.*?</ac:parameter>}m, "")
|
58
|
+
|
59
|
+
cleaned
|
60
|
+
end
|
61
|
+
# rubocop:enable Layout/LineLength, Metrics/MethodLength
|
62
|
+
|
20
63
|
def log_pages_found(count, kb_name)
|
21
64
|
Aircana.human_logger.info "Found #{count} pages for KB '#{kb_name}'"
|
22
65
|
end
|
@@ -11,7 +11,7 @@ module Aircana
|
|
11
11
|
post_tool_use
|
12
12
|
user_prompt_submit
|
13
13
|
session_start
|
14
|
-
|
14
|
+
refresh_skills
|
15
15
|
notification_sqs
|
16
16
|
rubocop_pre_commit
|
17
17
|
rspec_test
|
@@ -21,7 +21,7 @@ module Aircana
|
|
21
21
|
# Default hooks that are auto-installed
|
22
22
|
DEFAULT_HOOK_TYPES = %w[
|
23
23
|
session_start
|
24
|
-
|
24
|
+
refresh_skills
|
25
25
|
notification_sqs
|
26
26
|
].freeze
|
27
27
|
|
@@ -5,6 +5,7 @@ require_relative "../contexts/manifest"
|
|
5
5
|
|
6
6
|
module Aircana
|
7
7
|
module Generators
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
8
9
|
class SkillsGenerator < BaseGenerator
|
9
10
|
attr_reader :kb_name, :short_description, :skill_description, :knowledge_files
|
10
11
|
|
@@ -24,11 +25,20 @@ module Aircana
|
|
24
25
|
# rubocop:enable Metrics/ParameterLists
|
25
26
|
|
26
27
|
# Generate SKILL.md based on manifest data
|
28
|
+
# rubocop:disable Metrics/MethodLength
|
27
29
|
def self.from_manifest(kb_name)
|
28
30
|
manifest = Contexts::Manifest.read_manifest(kb_name)
|
29
31
|
raise Error, "No manifest found for knowledge base '#{kb_name}'" unless manifest
|
30
32
|
|
31
33
|
knowledge_files = extract_knowledge_files_from_manifest(manifest)
|
34
|
+
|
35
|
+
# Warn if no knowledge files were found
|
36
|
+
if knowledge_files.empty?
|
37
|
+
Aircana.human_logger.warn "No knowledge files found for KB '#{kb_name}'. " \
|
38
|
+
"SKILL.md will be generated but will be empty. " \
|
39
|
+
"Run 'aircana kb refresh #{kb_name}' to fetch knowledge."
|
40
|
+
end
|
41
|
+
|
32
42
|
skill_description = generate_skill_description_from_manifest(manifest, kb_name)
|
33
43
|
|
34
44
|
new(
|
@@ -37,11 +47,45 @@ module Aircana
|
|
37
47
|
knowledge_files: knowledge_files
|
38
48
|
)
|
39
49
|
end
|
50
|
+
# rubocop:enable Metrics/MethodLength
|
40
51
|
|
41
52
|
# Class methods for manifest processing
|
53
|
+
def self.extract_knowledge_files_from_manifest(manifest)
|
54
|
+
kb_name = manifest["name"]
|
55
|
+
|
56
|
+
# If kb_dir exists, scan actual files on disk (preferred method)
|
57
|
+
if kb_name && Aircana.configuration.kb_knowledge_dir
|
58
|
+
kb_dir = Aircana.configuration.kb_path(kb_name)
|
59
|
+
|
60
|
+
return extract_files_from_disk(manifest, kb_dir) if Dir.exist?(kb_dir)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Fallback: extract from manifest metadata (for tests or before files are created)
|
64
|
+
extract_files_from_manifest_metadata(manifest)
|
65
|
+
end
|
66
|
+
|
67
|
+
# rubocop:disable Metrics/MethodLength
|
68
|
+
def self.extract_files_from_disk(manifest, kb_dir)
|
69
|
+
actual_files = Dir.glob(File.join(kb_dir, "*.md"))
|
70
|
+
.reject { |f| File.basename(f) == "SKILL.md" }
|
71
|
+
.sort
|
72
|
+
|
73
|
+
# Build file list with summaries from manifest
|
74
|
+
actual_files.map do |filepath|
|
75
|
+
filename = File.basename(filepath)
|
76
|
+
summary = find_summary_for_file(manifest, filename)
|
77
|
+
|
78
|
+
{
|
79
|
+
summary: summary || File.basename(filename, ".md").tr("-", " ").capitalize,
|
80
|
+
filename: filename
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# rubocop:enable Metrics/MethodLength
|
85
|
+
|
42
86
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
43
87
|
# rubocop:disable Metrics/PerceivedComplexity
|
44
|
-
def self.
|
88
|
+
def self.extract_files_from_manifest_metadata(manifest)
|
45
89
|
files = []
|
46
90
|
|
47
91
|
manifest["sources"]&.each do |source|
|
@@ -68,6 +112,29 @@ module Aircana
|
|
68
112
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
69
113
|
# rubocop:enable Metrics/PerceivedComplexity
|
70
114
|
|
115
|
+
# Find the summary for a given filename from the manifest
|
116
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
117
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
118
|
+
def self.find_summary_for_file(manifest, filename)
|
119
|
+
manifest["sources"]&.each do |source|
|
120
|
+
case source["type"]
|
121
|
+
when "confluence"
|
122
|
+
source["pages"]&.each do |page|
|
123
|
+
return page["summary"] if filename.include?(page["id"])
|
124
|
+
end
|
125
|
+
when "web"
|
126
|
+
source["urls"]&.each do |url_entry|
|
127
|
+
sanitized_url_part = sanitize_filename_from_url(url_entry["url"])
|
128
|
+
return url_entry["summary"] if filename.include?(sanitized_url_part)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
136
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
137
|
+
|
71
138
|
def self.generate_skill_description_from_manifest(manifest, kb_name)
|
72
139
|
# Generate a description optimized for Claude's skill discovery
|
73
140
|
source_count = manifest["sources"]&.size || 0
|
@@ -91,6 +158,7 @@ module Aircana
|
|
91
158
|
rescue URI::InvalidURIError
|
92
159
|
"web_resource"
|
93
160
|
end
|
161
|
+
# rubocop:enable Metrics/ClassLength
|
94
162
|
|
95
163
|
protected
|
96
164
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Auto-refresh remote knowledge bases on session start
|
3
|
+
# Runs at most once per 24 hours to avoid API rate limits
|
4
|
+
# Only refreshes remote KBs, skips local KBs
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
# Create log directory if it doesn't exist
|
9
|
+
mkdir -p ~/.aircana
|
10
|
+
LOG_FILE="$HOME/.aircana/hooks.log"
|
11
|
+
|
12
|
+
# Claude Code provides this environment variable
|
13
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
|
14
|
+
|
15
|
+
if [ -z "$PLUGIN_ROOT" ]; then
|
16
|
+
echo "$(date): Warning - CLAUDE_PLUGIN_ROOT not set, skipping KB refresh" >> "$LOG_FILE"
|
17
|
+
echo "{}"
|
18
|
+
exit 0
|
19
|
+
fi
|
20
|
+
|
21
|
+
TIMESTAMP_FILE="${PLUGIN_ROOT}/.last_refresh"
|
22
|
+
REFRESH_INTERVAL_SECONDS=86400 # 24 hours
|
23
|
+
|
24
|
+
# Check if we've refreshed recently
|
25
|
+
if [ -f "$TIMESTAMP_FILE" ]; then
|
26
|
+
LAST_REFRESH=$(cat "$TIMESTAMP_FILE")
|
27
|
+
CURRENT_TIME=$(date +%s)
|
28
|
+
TIME_DIFF=$((CURRENT_TIME - LAST_REFRESH))
|
29
|
+
|
30
|
+
if [ $TIME_DIFF -lt $REFRESH_INTERVAL_SECONDS ]; then
|
31
|
+
HOURS_SINCE=$((TIME_DIFF / 3600))
|
32
|
+
echo "$(date): Knowledge bases refreshed ${HOURS_SINCE}h ago, skipping refresh" >> "$LOG_FILE"
|
33
|
+
echo "{}"
|
34
|
+
exit 0
|
35
|
+
fi
|
36
|
+
fi
|
37
|
+
|
38
|
+
# Tell aircana where the plugin lives
|
39
|
+
export AIRCANA_PLUGIN_ROOT="$PLUGIN_ROOT"
|
40
|
+
|
41
|
+
echo "$(date): Starting knowledge base refresh from plugin root: $PLUGIN_ROOT" >> "$LOG_FILE"
|
42
|
+
|
43
|
+
# Find all knowledge bases in agents directory
|
44
|
+
AGENTS_DIR="${PLUGIN_ROOT}/agents"
|
45
|
+
|
46
|
+
if [ ! -d "$AGENTS_DIR" ]; then
|
47
|
+
echo "$(date): No agents directory found, skipping refresh" >> "$LOG_FILE"
|
48
|
+
echo "{}"
|
49
|
+
exit 0
|
50
|
+
fi
|
51
|
+
|
52
|
+
# Track if we refreshed any KBs
|
53
|
+
REFRESHED_COUNT=0
|
54
|
+
SKIPPED_COUNT=0
|
55
|
+
|
56
|
+
# Iterate through each KB directory
|
57
|
+
for kb_dir in "${AGENTS_DIR}"/*/ ; do
|
58
|
+
# Skip if no directories found
|
59
|
+
[ -d "$kb_dir" ] || continue
|
60
|
+
|
61
|
+
kb_name=$(basename "$kb_dir")
|
62
|
+
manifest_file="${kb_dir}manifest.json"
|
63
|
+
|
64
|
+
# Skip if no manifest exists
|
65
|
+
if [ ! -f "$manifest_file" ]; then
|
66
|
+
echo "$(date): No manifest found for KB '$kb_name', skipping" >> "$LOG_FILE"
|
67
|
+
continue
|
68
|
+
fi
|
69
|
+
|
70
|
+
# Determine KB type from manifest (default to remote if not specified)
|
71
|
+
kb_type="remote"
|
72
|
+
|
73
|
+
# Try to parse with jq if available, otherwise use grep
|
74
|
+
if command -v jq >/dev/null 2>&1; then
|
75
|
+
kb_type=$(jq -r '.kb_type // "remote"' "$manifest_file" 2>/dev/null || echo "remote")
|
76
|
+
else
|
77
|
+
# Fallback: grep for kb_type field
|
78
|
+
if grep -q '"kb_type"[[:space:]]*:[[:space:]]*"local"' "$manifest_file" 2>/dev/null; then
|
79
|
+
kb_type="local"
|
80
|
+
fi
|
81
|
+
fi
|
82
|
+
|
83
|
+
# Only refresh remote KBs
|
84
|
+
if [ "$kb_type" = "local" ]; then
|
85
|
+
echo "$(date): Skipping local KB '$kb_name'" >> "$LOG_FILE"
|
86
|
+
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
87
|
+
continue
|
88
|
+
fi
|
89
|
+
|
90
|
+
# Refresh remote KB
|
91
|
+
echo "$(date): Refreshing remote KB '$kb_name'" >> "$LOG_FILE"
|
92
|
+
if aircana kb refresh "$kb_name" >> "$LOG_FILE" 2>&1; then
|
93
|
+
echo "$(date): Successfully refreshed KB '$kb_name'" >> "$LOG_FILE"
|
94
|
+
REFRESHED_COUNT=$((REFRESHED_COUNT + 1))
|
95
|
+
else
|
96
|
+
echo "$(date): Warning - Failed to refresh KB '$kb_name'" >> "$LOG_FILE"
|
97
|
+
fi
|
98
|
+
done
|
99
|
+
|
100
|
+
# Update timestamp on completion (even if some refreshes failed)
|
101
|
+
date +%s > "$TIMESTAMP_FILE"
|
102
|
+
|
103
|
+
# Log summary
|
104
|
+
echo "$(date): KB refresh completed - refreshed: $REFRESHED_COUNT, skipped (local): $SKIPPED_COUNT" >> "$LOG_FILE"
|
105
|
+
|
106
|
+
# Return success with context
|
107
|
+
if [ $REFRESHED_COUNT -gt 0 ]; then
|
108
|
+
CONTEXT="Refreshed $REFRESHED_COUNT remote knowledge base(s)"
|
109
|
+
else
|
110
|
+
CONTEXT="No remote knowledge bases to refresh"
|
111
|
+
fi
|
112
|
+
|
113
|
+
ESCAPED_CONTEXT=$(echo -n "$CONTEXT" | sed 's/"/\\"/g')
|
114
|
+
cat << EOF
|
115
|
+
{
|
116
|
+
"hookSpecificOutput": {
|
117
|
+
"hookEventName": "SessionStart",
|
118
|
+
"additionalContext": "$ESCAPED_CONTEXT"
|
119
|
+
}
|
120
|
+
}
|
121
|
+
EOF
|
data/lib/aircana/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aircana
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.0.
|
4
|
+
version: 4.0.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Weston Dransfield
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: httparty
|
@@ -115,15 +115,8 @@ files:
|
|
115
115
|
- README.md
|
116
116
|
- Rakefile
|
117
117
|
- SECURITY.md
|
118
|
-
- commands/apply-feedback.md
|
119
|
-
- commands/ask-expert.md
|
120
|
-
- commands/execute.md
|
121
|
-
- commands/plan.md
|
122
|
-
- commands/record.md
|
123
|
-
- commands/review.md
|
124
118
|
- compose.yml
|
125
119
|
- exe/aircana
|
126
|
-
- hooks/hooks.json
|
127
120
|
- lib/aircana.rb
|
128
121
|
- lib/aircana/cli.rb
|
129
122
|
- lib/aircana/cli/app.rb
|
@@ -132,7 +125,6 @@ files:
|
|
132
125
|
- lib/aircana/cli/commands/doctor_helpers.rb
|
133
126
|
- lib/aircana/cli/commands/dump_context.rb
|
134
127
|
- lib/aircana/cli/commands/generate.rb
|
135
|
-
- lib/aircana/cli/commands/hooks.rb
|
136
128
|
- lib/aircana/cli/commands/init.rb
|
137
129
|
- lib/aircana/cli/commands/kb.rb
|
138
130
|
- lib/aircana/cli/commands/plugin.rb
|
@@ -177,6 +169,7 @@ files:
|
|
177
169
|
- lib/aircana/templates/hooks/notification_sqs.erb
|
178
170
|
- lib/aircana/templates/hooks/post_tool_use.erb
|
179
171
|
- lib/aircana/templates/hooks/pre_tool_use.erb
|
172
|
+
- lib/aircana/templates/hooks/refresh_skills.erb
|
180
173
|
- lib/aircana/templates/hooks/rspec_test.erb
|
181
174
|
- lib/aircana/templates/hooks/rubocop_pre_commit.erb
|
182
175
|
- lib/aircana/templates/hooks/session_start.erb
|
@@ -207,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
207
200
|
- !ruby/object:Gem::Version
|
208
201
|
version: '0'
|
209
202
|
requirements: []
|
210
|
-
rubygems_version: 3.6.
|
203
|
+
rubygems_version: 3.6.9
|
211
204
|
specification_version: 4
|
212
205
|
summary: CLI for creating and managing Claude Code plugins with per-agent knowledge
|
213
206
|
bases
|
data/commands/apply-feedback.md
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
INSTRUCTIONS : Use the Task tool with subagent_type 'apply-feedback' to apply code review feedback from the previous /air-review command.
|
2
|
-
|
3
|
-
Pass the review feedback from the conversation context to the apply-feedback agent.
|
4
|
-
|
5
|
-
The apply-feedback agent will:
|
6
|
-
1. Parse review feedback from the previous review
|
7
|
-
2. Create todo list of changes prioritized by severity
|
8
|
-
3. Present plan to user for approval
|
9
|
-
4. Apply approved changes
|
10
|
-
5. Re-run unit tests to verify changes
|
11
|
-
6. Fix any test failures
|
12
|
-
7. Amend the HEAD commit with improvements using 'git commit --amend --no-edit'
|
13
|
-
8. Summarize changes made
|
14
|
-
|
15
|
-
IMPORTANT: This command reads the review output from the conversation context, so it must be run in the same conversation as /air-review.
|
data/commands/ask-expert.md
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
INSTRUCTIONS : You are coordinating expert consultation to answer a question by leveraging multiple specialized sub-agents. Follow this precise workflow:
|
2
|
-
|
3
|
-
STEP 1: QUESTION VALIDATION
|
4
|
-
Ask the user: \"What is your question?\" and wait for their response before proceeding.
|
5
|
-
|
6
|
-
STEP 2: COORDINATION PHASE
|
7
|
-
Use the Task tool with subagent_type 'sub-agent-coordinator' to analyze the question and identify relevant sub-agents. Provide the coordinator with the complete question context.
|
8
|
-
|
9
|
-
STEP 3: PARALLEL EXPERT CONSULTATION
|
10
|
-
Based on the coordinator's recommendations, use the Task tool to consult each identified relevant sub-agent in parallel. For each agent:
|
11
|
-
- Use the appropriate subagent_type for each recommended agent
|
12
|
-
- Provide the original question plus any agent-specific context the coordinator suggested
|
13
|
-
- Execute multiple Task tool calls in a single message for parallel processing
|
14
|
-
|
15
|
-
STEP 4: SYNTHESIS AND RESPONSE
|
16
|
-
After receiving responses from all consulted agents:
|
17
|
-
- Analyze and synthesize the expert feedback
|
18
|
-
- Identify common themes, conflicting viewpoints, and complementary insights
|
19
|
-
- Provide a comprehensive answer that leverages the collective expertise
|
20
|
-
- Cite which agents contributed specific insights where relevant
|
21
|
-
- Note any areas where experts disagreed and provide your assessment
|
22
|
-
|
23
|
-
STEP 5: FOLLOW-UP GUIDANCE
|
24
|
-
If the question requires further clarification or the expert responses suggest additional considerations:
|
25
|
-
- Suggest specific follow-up questions
|
26
|
-
- Recommend additional agents to consult if needed
|
27
|
-
- Provide guidance on next steps based on the expert consensus
|
28
|
-
|
29
|
-
IMPORTANT EXECUTION NOTES:
|
30
|
-
- Always start with the sub-agent-coordinator for proper agent selection
|
31
|
-
- Use parallel Task tool execution when consulting multiple agents (single message with multiple tool calls)
|
32
|
-
- Ensure each agent receives context appropriate to their expertise domain
|
33
|
-
- Synthesize responses rather than simply concatenating them
|
34
|
-
- Maintain focus on providing actionable, comprehensive answers
|
35
|
-
|
36
|
-
EXAMPLE PARALLEL EXECUTION:
|
37
|
-
If coordinator recommends agents A, B, and C, send one message with three Task tool calls:
|
38
|
-
1. Task(subagent_type='agent-A', prompt='[question + A-specific context]')
|
39
|
-
2. Task(subagent_type='agent-B', prompt='[question + B-specific context]')
|
40
|
-
3. Task(subagent_type='agent-C', prompt='[question + C-specific context]')
|
41
|
-
|
42
|
-
This approach ensures you leverage the full expertise available while maintaining efficient coordination.
|
data/commands/execute.md
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
INSTRUCTIONS : Use the Task tool with subagent_type 'executor' to execute the implementation plan from a Jira ticket.
|
2
|
-
|
3
|
-
INSTRUCTIONS FOR EXECUTOR AGENT:
|
4
|
-
Ask the user to provide a Jira ticket key/ID to execute.
|
5
|
-
|
6
|
-
The executor agent will:
|
7
|
-
1. Read the plan from the Jira ticket via the 'jira' sub-agent
|
8
|
-
2. Review and validate the plan structure
|
9
|
-
3. Create a detailed execution todo list in Claude Code planning mode
|
10
|
-
4. Present the plan for your approval
|
11
|
-
5. Execute the approved implementation tasks
|
12
|
-
|
13
|
-
IMPORTANT: All Jira operations are delegated to the 'jira' sub-agent using Task tool with subagent_type 'jira'.
|