aircana 0.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.
- checksums.yaml +7 -0
- data/.devcontainer/devcontainer.json +36 -0
- data/.dockerignore +14 -0
- data/.rspec_status +106 -0
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +58 -0
- data/Dockerfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +251 -0
- data/Rakefile +12 -0
- data/SECURITY.md +15 -0
- data/compose.yml +13 -0
- data/exe/aircana +13 -0
- data/lib/aircana/cli/app.rb +93 -0
- data/lib/aircana/cli/commands/add_directory.rb +148 -0
- data/lib/aircana/cli/commands/add_files.rb +26 -0
- data/lib/aircana/cli/commands/agents.rb +152 -0
- data/lib/aircana/cli/commands/clear_files.rb +16 -0
- data/lib/aircana/cli/commands/doctor.rb +85 -0
- data/lib/aircana/cli/commands/doctor_checks.rb +131 -0
- data/lib/aircana/cli/commands/doctor_helpers.rb +119 -0
- data/lib/aircana/cli/commands/dump_context.rb +23 -0
- data/lib/aircana/cli/commands/generate.rb +34 -0
- data/lib/aircana/cli/commands/install.rb +67 -0
- data/lib/aircana/cli/commands/plan.rb +69 -0
- data/lib/aircana/cli/commands/work.rb +69 -0
- data/lib/aircana/cli/shell_command.rb +13 -0
- data/lib/aircana/cli/subcommand.rb +19 -0
- data/lib/aircana/cli.rb +8 -0
- data/lib/aircana/configuration.rb +41 -0
- data/lib/aircana/contexts/confluence.rb +141 -0
- data/lib/aircana/contexts/confluence_content.rb +36 -0
- data/lib/aircana/contexts/confluence_http.rb +41 -0
- data/lib/aircana/contexts/confluence_logging.rb +71 -0
- data/lib/aircana/contexts/confluence_setup.rb +15 -0
- data/lib/aircana/contexts/local.rb +47 -0
- data/lib/aircana/contexts/relevant_files.rb +78 -0
- data/lib/aircana/fzf_helper.rb +117 -0
- data/lib/aircana/generators/agents_generator.rb +75 -0
- data/lib/aircana/generators/base_generator.rb +61 -0
- data/lib/aircana/generators/helpers.rb +16 -0
- data/lib/aircana/generators/relevant_files_command_generator.rb +36 -0
- data/lib/aircana/generators/relevant_files_verbose_results_generator.rb +34 -0
- data/lib/aircana/generators.rb +10 -0
- data/lib/aircana/human_logger.rb +143 -0
- data/lib/aircana/initializers.rb +8 -0
- data/lib/aircana/llm/claude_client.rb +86 -0
- data/lib/aircana/progress_tracker.rb +55 -0
- data/lib/aircana/system_checker.rb +177 -0
- data/lib/aircana/templates/agents/base_agent.erb +30 -0
- data/lib/aircana/templates/agents/defaults/planner.erb +126 -0
- data/lib/aircana/templates/agents/defaults/worker.erb +185 -0
- data/lib/aircana/templates/commands/add_relevant_files.erb +3 -0
- data/lib/aircana/templates/relevant_files_verbose_results.erb +18 -0
- data/lib/aircana/version.rb +5 -0
- data/lib/aircana.rb +53 -0
- data/sig/aircana.rbs +4 -0
- metadata +189 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
require_relative "commands/add_files"
|
6
|
+
require_relative "commands/add_directory"
|
7
|
+
require_relative "commands/clear_files"
|
8
|
+
require_relative "commands/doctor"
|
9
|
+
require_relative "commands/dump_context"
|
10
|
+
require_relative "commands/generate"
|
11
|
+
require_relative "commands/install"
|
12
|
+
require_relative "commands/plan"
|
13
|
+
require_relative "commands/work"
|
14
|
+
|
15
|
+
require_relative "subcommand"
|
16
|
+
require_relative "commands/agents"
|
17
|
+
|
18
|
+
module Aircana
|
19
|
+
module CLI
|
20
|
+
# Thor application for the primary cli
|
21
|
+
class App < Thor
|
22
|
+
package_name "Aircana"
|
23
|
+
|
24
|
+
# TODO: Decide how to represent and store file groups
|
25
|
+
desc "add-files",
|
26
|
+
"interactively add files or file groups to the current context. Use tab to mark multiple files."
|
27
|
+
def add_files
|
28
|
+
AddFiles.run
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "add-dir [DIRECTORY_PATH]",
|
32
|
+
"add all files from the specified directory recursively to the current context"
|
33
|
+
def add_dir(directory_path)
|
34
|
+
AddDirectory.run(directory_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "clear-files",
|
38
|
+
"Removes all files from the current set of 'relevant files'"
|
39
|
+
def clear_files
|
40
|
+
ClearFiles.run
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "doctor", "Check system health and validate all dependencies"
|
44
|
+
option :verbose, type: :boolean, default: false, desc: "Show detailed information about optional dependencies"
|
45
|
+
def doctor
|
46
|
+
exit_code = Doctor.run(verbose: options[:verbose])
|
47
|
+
exit(exit_code)
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "dump-context",
|
51
|
+
"dumps relevant files, knowledge, memories, and decisions for the specified agent"
|
52
|
+
option :verbose, type: :boolean, default: true
|
53
|
+
def dump_context(agent_name)
|
54
|
+
DumpContext.run(agent_name: agent_name, verbose: options[:verbose])
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "generate", "Generates all configured files and dumps the configured output directory"
|
58
|
+
def generate
|
59
|
+
Generate.run
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "install", "Copies the generated files from `generate` to the proper directories in Claude Code config."
|
63
|
+
def install
|
64
|
+
Install.run
|
65
|
+
end
|
66
|
+
|
67
|
+
desc "plan", "Launch Claude Code with planner agent for Jira ticket planning"
|
68
|
+
def plan
|
69
|
+
Plan.run
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "work", "Launch Claude Code with worker agent for Jira ticket implementation"
|
73
|
+
def work
|
74
|
+
Work.run
|
75
|
+
end
|
76
|
+
|
77
|
+
class AgentsSubcommand < Subcommand
|
78
|
+
desc "create", "Create a new agent"
|
79
|
+
def create
|
80
|
+
Agents.create
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "refresh AGENT", "Refresh agent knowledge from Confluence pages with matching labels"
|
84
|
+
def refresh(agent)
|
85
|
+
Agents.refresh(agent)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "agents", "Create and manage agents and their knowledgebases"
|
90
|
+
subcommand "agents", AgentsSubcommand
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require_relative "../../contexts/relevant_files"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
module CLI
|
8
|
+
module AddDirectory
|
9
|
+
class << self
|
10
|
+
def run(directory_path)
|
11
|
+
return unless directory_valid?(directory_path)
|
12
|
+
|
13
|
+
selected_files = collect_files_recursively(directory_path)
|
14
|
+
return log_no_files_found(directory_path) if selected_files.empty?
|
15
|
+
|
16
|
+
return unless confirm_large_operation?(selected_files.size, directory_path)
|
17
|
+
|
18
|
+
process_files(directory_path, selected_files)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def directory_valid?(directory_path)
|
24
|
+
unless File.directory?(directory_path)
|
25
|
+
Aircana.human_logger.error "Directory not found: #{directory_path}"
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
unless File.readable?(directory_path)
|
30
|
+
Aircana.human_logger.error "Directory not readable: #{directory_path}"
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def log_no_files_found(directory_path)
|
38
|
+
Aircana.human_logger.info "No files found in directory: #{directory_path}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def confirm_large_operation?(file_count, directory_path)
|
42
|
+
return true if file_count <= 50
|
43
|
+
|
44
|
+
show_large_operation_warning(file_count, directory_path)
|
45
|
+
TTY::Prompt.new.yes?("Continue with adding #{file_count} files?")
|
46
|
+
end
|
47
|
+
|
48
|
+
def show_large_operation_warning(file_count, directory_path)
|
49
|
+
estimated_size = estimate_total_size(directory_path, file_count)
|
50
|
+
Aircana.human_logger.warn "Large directory operation detected:"
|
51
|
+
Aircana.human_logger.info " Directory: #{directory_path}"
|
52
|
+
Aircana.human_logger.info " Files: #{file_count}"
|
53
|
+
Aircana.human_logger.info " Estimated size: #{estimated_size}"
|
54
|
+
Aircana.human_logger.warn " This may result in high token usage with Claude"
|
55
|
+
end
|
56
|
+
|
57
|
+
def estimate_total_size(directory_path, file_count)
|
58
|
+
sample_files = get_sample_files(directory_path, file_count)
|
59
|
+
return "Unknown" if sample_files.empty?
|
60
|
+
|
61
|
+
total_bytes = calculate_sample_size(sample_files)
|
62
|
+
estimated_total = extrapolate_total_size(total_bytes, sample_files.size, file_count)
|
63
|
+
format_file_size(estimated_total)
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_sample_files(directory_path, file_count)
|
67
|
+
Dir.glob(File.join(directory_path, "**", "*"))
|
68
|
+
.reject { |f| File.directory?(f) }
|
69
|
+
.sample([file_count, 10].min)
|
70
|
+
end
|
71
|
+
|
72
|
+
def calculate_sample_size(sample_files)
|
73
|
+
sample_files.sum do |f|
|
74
|
+
File.size(f)
|
75
|
+
rescue StandardError
|
76
|
+
0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def extrapolate_total_size(total_bytes, sample_size, file_count)
|
81
|
+
avg_size = total_bytes / sample_size.to_f
|
82
|
+
(avg_size * file_count).to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_file_size(bytes)
|
86
|
+
units = %w[B KB MB GB]
|
87
|
+
size = bytes.to_f
|
88
|
+
unit_index = 0
|
89
|
+
|
90
|
+
while size >= 1024 && unit_index < units.length - 1
|
91
|
+
size /= 1024
|
92
|
+
unit_index += 1
|
93
|
+
end
|
94
|
+
|
95
|
+
"#{size.round(1)} #{units[unit_index]}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def process_files(directory_path, selected_files)
|
99
|
+
file_count = selected_files.length
|
100
|
+
Aircana.human_logger.info "Found #{file_count} files in directory: #{directory_path}"
|
101
|
+
|
102
|
+
ProgressTracker.with_spinner("Adding #{file_count} files to context") do
|
103
|
+
Contexts::RelevantFiles.add(selected_files)
|
104
|
+
end
|
105
|
+
|
106
|
+
Aircana.human_logger.success "Successfully added #{file_count} files from directory"
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_token_warning(file_count)
|
110
|
+
Aircana.human_logger.warn "Large number of files (#{file_count}) may result in high token usage"
|
111
|
+
end
|
112
|
+
|
113
|
+
def collect_files_recursively(directory_path)
|
114
|
+
Dir.glob(File.join(directory_path, "**", "*"), File::FNM_DOTMATCH)
|
115
|
+
.reject { |path| File.directory?(path) }
|
116
|
+
.reject { |path| should_ignore_file?(path) }
|
117
|
+
end
|
118
|
+
|
119
|
+
def should_ignore_file?(file_path)
|
120
|
+
ignore_patterns.any? { |pattern| file_path.match?(pattern) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def ignore_patterns
|
124
|
+
directory_patterns + file_patterns
|
125
|
+
end
|
126
|
+
|
127
|
+
def directory_patterns
|
128
|
+
[
|
129
|
+
%r{/\.git/}, %r{/node_modules/}, %r{/\.vscode/}, %r{/\.idea/},
|
130
|
+
%r{/coverage/}, %r{/dist/}, %r{/build/}, %r{/tmp/}, %r{/vendor/},
|
131
|
+
%r{/\.bundle/}, %r{/\.rvm/}, %r{/\.rbenv/}
|
132
|
+
]
|
133
|
+
end
|
134
|
+
|
135
|
+
def file_patterns
|
136
|
+
[
|
137
|
+
%r{/\.DS_Store$}, %r{/log/.*\.log$},
|
138
|
+
/\.(jpg|jpeg|png|gif|bmp|tiff|svg|ico|webp)$/i,
|
139
|
+
/\.(mp4|avi|mkv|mov|wmv|flv|webm)$/i,
|
140
|
+
/\.(mp3|wav|flac|aac|ogg)$/i,
|
141
|
+
/\.(zip|tar|gz|rar|7z|bz2)$/i,
|
142
|
+
/\.(exe|dll|so|dylib)$/i
|
143
|
+
]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../shell_command"
|
4
|
+
require_relative "../../contexts/relevant_files"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
module CLI
|
8
|
+
module AddFiles
|
9
|
+
class << self
|
10
|
+
def run
|
11
|
+
selected_files = FzfHelper.select_files_interactively(
|
12
|
+
header: "Select files for Claude context (Ctrl+A: select all, ?: toggle preview)"
|
13
|
+
)
|
14
|
+
|
15
|
+
if selected_files.empty?
|
16
|
+
Aircana.human_logger.info "No files selected. Exiting."
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
Aircana.human_logger.success "Selected #{selected_files.size} files for context"
|
21
|
+
Contexts::RelevantFiles.add(selected_files)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require_relative "../../generators/agents_generator"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
module CLI
|
8
|
+
module Agents # rubocop:disable Metrics/ModuleLength
|
9
|
+
SUPPORTED_CLAUDE_MODELS = %w[sonnet haiku inherit].freeze
|
10
|
+
SUPPORTED_CLAUDE_COLORS = %w[red blue green yellow purple orange pink cyan].freeze
|
11
|
+
|
12
|
+
class << self # rubocop:disable Metrics/ClassLength
|
13
|
+
def refresh(agent)
|
14
|
+
normalized_agent = normalize_string(agent)
|
15
|
+
perform_refresh(normalized_agent)
|
16
|
+
rescue Aircana::Error => e
|
17
|
+
handle_refresh_error(normalized_agent, e)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
21
|
+
prompt = TTY::Prompt.new
|
22
|
+
|
23
|
+
agent_name = prompt.ask("Agent name:")
|
24
|
+
short_description = prompt.ask("Briefly describe what your agent does:")
|
25
|
+
model = prompt.select("Select a model for your agent:", SUPPORTED_CLAUDE_MODELS)
|
26
|
+
color = prompt.select("Select a color for your agent:", SUPPORTED_CLAUDE_COLORS)
|
27
|
+
|
28
|
+
description = description_from_claude(short_description)
|
29
|
+
normalized_agent_name = normalize_string(agent_name)
|
30
|
+
|
31
|
+
file = Generators::AgentsGenerator.new(
|
32
|
+
agent_name: normalized_agent_name,
|
33
|
+
description:,
|
34
|
+
short_description:,
|
35
|
+
model: normalize_string(model),
|
36
|
+
color: normalize_string(color)
|
37
|
+
).generate
|
38
|
+
|
39
|
+
Aircana.human_logger.success "Agent created at #{file}"
|
40
|
+
|
41
|
+
# Prompt for knowledge fetching
|
42
|
+
prompt_for_knowledge_fetch(prompt, normalized_agent_name)
|
43
|
+
|
44
|
+
# Prompt for agent file review
|
45
|
+
prompt_for_agent_review(prompt, file)
|
46
|
+
|
47
|
+
Aircana.human_logger.success "Agent '#{agent_name}' setup complete!"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def perform_refresh(normalized_agent)
|
53
|
+
confluence = Aircana::Contexts::Confluence.new
|
54
|
+
pages_count = confluence.fetch_pages_for(agent: normalized_agent)
|
55
|
+
|
56
|
+
log_refresh_result(normalized_agent, pages_count)
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_refresh_result(normalized_agent, pages_count)
|
60
|
+
if pages_count.positive?
|
61
|
+
Aircana.human_logger.success "Successfully refreshed #{pages_count} pages for agent '#{normalized_agent}'"
|
62
|
+
else
|
63
|
+
log_no_pages_found(normalized_agent)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def log_no_pages_found(normalized_agent)
|
68
|
+
Aircana.human_logger.info "No pages found for agent '#{normalized_agent}'. " \
|
69
|
+
"Make sure pages are labeled with '#{normalized_agent}' in Confluence."
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_refresh_error(normalized_agent, error)
|
73
|
+
Aircana.human_logger.error "Failed to refresh agent '#{normalized_agent}': #{error.message}"
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
|
77
|
+
def normalize_string(string)
|
78
|
+
string.strip.downcase.gsub(" ", "-")
|
79
|
+
end
|
80
|
+
|
81
|
+
def description_from_claude(description)
|
82
|
+
prompt = build_agent_description_prompt(description)
|
83
|
+
claude_client = Aircana::LLM::ClaudeClient.new
|
84
|
+
claude_client.prompt(prompt)
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_agent_description_prompt(description)
|
88
|
+
<<~PROMPT
|
89
|
+
Create a concise Claude Code agent description file (without frontmatter)
|
90
|
+
for an agent that is described as: #{description}.
|
91
|
+
|
92
|
+
The agent should be specialized and focused on its domain knowledge.
|
93
|
+
Include instructions that the agent should primarily rely on information
|
94
|
+
from its knowledge base rather than general knowledge when answering questions
|
95
|
+
within its domain.
|
96
|
+
|
97
|
+
Print the output to STDOUT only, without any additional commentary.
|
98
|
+
PROMPT
|
99
|
+
end
|
100
|
+
|
101
|
+
def prompt_for_knowledge_fetch(prompt, normalized_agent_name) # rubocop:disable Metrics/MethodLength
|
102
|
+
return unless confluence_configured?
|
103
|
+
|
104
|
+
if prompt.yes?("Would you like to fetch knowledge for this agent from Confluence now?")
|
105
|
+
Aircana.human_logger.info "Fetching knowledge from Confluence..."
|
106
|
+
perform_refresh(normalized_agent_name)
|
107
|
+
else
|
108
|
+
Aircana.human_logger.info(
|
109
|
+
"Skipping knowledge fetch. You can run 'aircana agents refresh #{normalized_agent_name}' later."
|
110
|
+
)
|
111
|
+
end
|
112
|
+
rescue Aircana::Error => e
|
113
|
+
Aircana.human_logger.warn "Failed to fetch knowledge: #{e.message}"
|
114
|
+
Aircana.human_logger.info "You can try again later with 'aircana agents refresh #{normalized_agent_name}'"
|
115
|
+
end
|
116
|
+
|
117
|
+
def prompt_for_agent_review(prompt, file_path)
|
118
|
+
Aircana.human_logger.info "Agent file created at: #{file_path}"
|
119
|
+
|
120
|
+
return unless prompt.yes?("Would you like to review and edit the agent file?")
|
121
|
+
|
122
|
+
open_file_in_editor(file_path)
|
123
|
+
end
|
124
|
+
|
125
|
+
def confluence_configured? # rubocop:disable Metrics/AbcSize
|
126
|
+
config = Aircana.configuration
|
127
|
+
|
128
|
+
base_url_present = !config.confluence_base_url.nil? && !config.confluence_base_url.empty?
|
129
|
+
username_present = !config.confluence_username.nil? && !config.confluence_username.empty?
|
130
|
+
token_present = !config.confluence_api_token.nil? && !config.confluence_api_token.empty?
|
131
|
+
|
132
|
+
base_url_present && username_present && token_present
|
133
|
+
end
|
134
|
+
|
135
|
+
def open_file_in_editor(file_path)
|
136
|
+
editor = ENV["EDITOR"] || find_available_editor
|
137
|
+
|
138
|
+
if editor
|
139
|
+
Aircana.human_logger.info "Opening #{file_path} in #{editor}..."
|
140
|
+
system("#{editor} '#{file_path}'")
|
141
|
+
else
|
142
|
+
Aircana.human_logger.warn "No editor found. Please edit #{file_path} manually."
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def find_available_editor
|
147
|
+
%w[code subl atom nano vim vi].find { |cmd| system("which #{cmd} > /dev/null 2>&1") }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../shell_command"
|
4
|
+
require_relative "../../contexts/relevant_files"
|
5
|
+
|
6
|
+
module Aircana
|
7
|
+
module CLI
|
8
|
+
module ClearFiles
|
9
|
+
class << self
|
10
|
+
def run
|
11
|
+
Contexts::RelevantFiles.remove_all
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require_relative "doctor_helpers"
|
5
|
+
require_relative "doctor_checks"
|
6
|
+
|
7
|
+
module Aircana
|
8
|
+
module CLI
|
9
|
+
module Doctor
|
10
|
+
class << self
|
11
|
+
include DoctorHelpers::Logging
|
12
|
+
include DoctorHelpers::SystemChecks
|
13
|
+
include DoctorHelpers::ConfigurationChecks
|
14
|
+
include DoctorHelpers::InstallCommands
|
15
|
+
include DoctorChecks::ClaudeIntegration
|
16
|
+
include DoctorChecks::AircanaConfiguration
|
17
|
+
include DoctorChecks::OptionalIntegrations
|
18
|
+
|
19
|
+
def run(verbose: false)
|
20
|
+
@verbose = verbose
|
21
|
+
@issues_found = false
|
22
|
+
|
23
|
+
Aircana.human_logger.info "🔍 Checking Aircana system health...\n"
|
24
|
+
|
25
|
+
check_required_dependencies
|
26
|
+
check_claude_integration
|
27
|
+
check_optional_dependencies
|
28
|
+
check_aircana_configuration
|
29
|
+
check_optional_integrations
|
30
|
+
|
31
|
+
display_summary
|
32
|
+
@issues_found ? 1 : 0
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def check_required_dependencies
|
38
|
+
Aircana.human_logger.info "Required Dependencies:"
|
39
|
+
|
40
|
+
check_command("git", "version control operations", required: true)
|
41
|
+
check_command("fzf", "interactive file selection", required: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_optional_dependencies
|
45
|
+
Aircana.human_logger.info "\nOptional Dependencies:"
|
46
|
+
|
47
|
+
check_optional_tool("bat", "Enhanced file previews available", "will use basic cat for previews")
|
48
|
+
check_optional_tool("fd", "Fast file searching available", "will use find command")
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_optional_tool(tool, success_message, fallback_message)
|
52
|
+
if command_available?(tool)
|
53
|
+
log_success(tool, success_message)
|
54
|
+
else
|
55
|
+
log_info(tool, "Not installed (#{fallback_message})")
|
56
|
+
log_remedy("Install with: #{install_command(tool)}") if @verbose
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def display_summary
|
61
|
+
Aircana.human_logger.info "\n#{"─" * 50}"
|
62
|
+
|
63
|
+
if @issues_found
|
64
|
+
Aircana.human_logger.error "❌ Some issues were found. Please review the remediation steps above."
|
65
|
+
else
|
66
|
+
Aircana.human_logger.success "✅ All checks passed! Aircana is ready to use."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_command(command, purpose, required: false)
|
71
|
+
if command_available?(command)
|
72
|
+
log_success(command, "Installed (#{purpose})")
|
73
|
+
elsif required
|
74
|
+
log_failure(command, "Not installed (required for #{purpose})")
|
75
|
+
log_remedy("Install with: #{install_command(command)}")
|
76
|
+
@issues_found = true
|
77
|
+
else
|
78
|
+
log_warning(command, "Not installed (#{purpose})")
|
79
|
+
log_remedy("Install with: #{install_command(command)}")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
|
5
|
+
module Aircana
|
6
|
+
module CLI
|
7
|
+
module DoctorChecks
|
8
|
+
module ClaudeIntegration
|
9
|
+
def check_claude_integration
|
10
|
+
Aircana.human_logger.info "\nClaude Code Integration:"
|
11
|
+
|
12
|
+
if claude_available?
|
13
|
+
log_success("claude", "Claude Code installed")
|
14
|
+
check_mcp_tools
|
15
|
+
else
|
16
|
+
log_failure("claude", "Claude Code not installed")
|
17
|
+
log_remedy("Install Claude Code from: https://claude.ai/download")
|
18
|
+
@issues_found = true
|
19
|
+
end
|
20
|
+
|
21
|
+
check_claude_directories
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_mcp_tools
|
25
|
+
claude_path = find_claude_path
|
26
|
+
return unless claude_path
|
27
|
+
|
28
|
+
check_jira_mcp_tool(claude_path)
|
29
|
+
rescue StandardError => e
|
30
|
+
log_warning("MCP Jira", "Could not check MCP tool: #{e.message}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_jira_mcp_tool(claude_path)
|
34
|
+
result = `#{claude_path} mcp get jira 2>&1`
|
35
|
+
if mcp_tool_installed?(result)
|
36
|
+
log_success("MCP Jira", "Atlassian/Jira MCP tool installed")
|
37
|
+
else
|
38
|
+
log_failure("MCP Jira", "Atlassian/Jira MCP tool not found")
|
39
|
+
log_remedy("Install with: claude mcp add --transport sse atlassian https://mcp.atlassian.com/v1/sse")
|
40
|
+
@issues_found = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_claude_directories
|
45
|
+
project_claude = File.join(Dir.pwd, ".claude")
|
46
|
+
if Dir.exist?(project_claude)
|
47
|
+
log_success(".claude", "Project Claude config directory exists")
|
48
|
+
else
|
49
|
+
log_warning(".claude", "Project Claude config directory not found")
|
50
|
+
log_remedy("Will be created when running 'aircana install'")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module AircanaConfiguration
|
56
|
+
def check_aircana_configuration
|
57
|
+
Aircana.human_logger.info "\nAircana Configuration:"
|
58
|
+
|
59
|
+
check_directory("~/.aircana", "Global Aircana directory")
|
60
|
+
check_directory(".aircana", "Project Aircana directory")
|
61
|
+
check_agents_status
|
62
|
+
check_relevant_files_status
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_agents_status
|
66
|
+
agents_dir = File.join(Dir.pwd, ".aircana", "agents")
|
67
|
+
if Dir.exist?(agents_dir) && !Dir.empty?(agents_dir)
|
68
|
+
agent_count = Dir.glob(File.join(agents_dir, "*.md")).size
|
69
|
+
log_success("agents", "#{agent_count} agent(s) configured")
|
70
|
+
elsif Dir.exist?(agents_dir)
|
71
|
+
log_info("agents", "Agents directory exists but is empty")
|
72
|
+
else
|
73
|
+
log_info("agents", "No agents configured yet")
|
74
|
+
log_remedy("Create agents with: aircana agents create")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_relevant_files_status
|
79
|
+
relevant_files_dir = File.join(Dir.pwd, ".aircana", "relevant_files")
|
80
|
+
if Dir.exist?(relevant_files_dir) && !Dir.empty?(relevant_files_dir)
|
81
|
+
file_count = Dir.glob(File.join(relevant_files_dir, "*")).size
|
82
|
+
log_success("relevant_files", "#{file_count} file(s) in context")
|
83
|
+
else
|
84
|
+
log_info("relevant_files", "No relevant files added yet")
|
85
|
+
log_remedy("Add files with: aircana add-files")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module OptionalIntegrations
|
91
|
+
def check_optional_integrations
|
92
|
+
Aircana.human_logger.info "\nOptional Integrations:"
|
93
|
+
|
94
|
+
check_confluence_config
|
95
|
+
check_editor_config
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_confluence_config
|
99
|
+
config = Aircana.configuration
|
100
|
+
|
101
|
+
if confluence_configured?(config)
|
102
|
+
log_success("Confluence", "API credentials configured")
|
103
|
+
else
|
104
|
+
log_info("Confluence", "Not configured")
|
105
|
+
log_remedy("Set CONFLUENCE_BASE_URL, CONFLUENCE_USERNAME, " \
|
106
|
+
"and CONFLUENCE_API_TOKEN for agent knowledge refresh")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_editor_config
|
111
|
+
editor = ENV.fetch("EDITOR", nil)
|
112
|
+
available_editors = find_available_editors
|
113
|
+
|
114
|
+
handle_editor_status(editor, available_editors)
|
115
|
+
end
|
116
|
+
|
117
|
+
def handle_editor_status(editor, available_editors)
|
118
|
+
if editor && command_available?(editor)
|
119
|
+
log_success("Editor", "EDITOR set to #{editor}")
|
120
|
+
elsif !available_editors.empty?
|
121
|
+
log_info("Editor", "Available editors: #{available_editors.join(", ")}")
|
122
|
+
log_remedy("Set EDITOR environment variable to prefer one")
|
123
|
+
else
|
124
|
+
log_warning("Editor", "No common editors found")
|
125
|
+
log_remedy("Install an editor or set EDITOR environment variable")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|