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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/devcontainer.json +36 -0
  3. data/.dockerignore +14 -0
  4. data/.rspec_status +106 -0
  5. data/.rubocop.yml +33 -0
  6. data/CHANGELOG.md +19 -0
  7. data/CLAUDE.md +58 -0
  8. data/Dockerfile +17 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +251 -0
  11. data/Rakefile +12 -0
  12. data/SECURITY.md +15 -0
  13. data/compose.yml +13 -0
  14. data/exe/aircana +13 -0
  15. data/lib/aircana/cli/app.rb +93 -0
  16. data/lib/aircana/cli/commands/add_directory.rb +148 -0
  17. data/lib/aircana/cli/commands/add_files.rb +26 -0
  18. data/lib/aircana/cli/commands/agents.rb +152 -0
  19. data/lib/aircana/cli/commands/clear_files.rb +16 -0
  20. data/lib/aircana/cli/commands/doctor.rb +85 -0
  21. data/lib/aircana/cli/commands/doctor_checks.rb +131 -0
  22. data/lib/aircana/cli/commands/doctor_helpers.rb +119 -0
  23. data/lib/aircana/cli/commands/dump_context.rb +23 -0
  24. data/lib/aircana/cli/commands/generate.rb +34 -0
  25. data/lib/aircana/cli/commands/install.rb +67 -0
  26. data/lib/aircana/cli/commands/plan.rb +69 -0
  27. data/lib/aircana/cli/commands/work.rb +69 -0
  28. data/lib/aircana/cli/shell_command.rb +13 -0
  29. data/lib/aircana/cli/subcommand.rb +19 -0
  30. data/lib/aircana/cli.rb +8 -0
  31. data/lib/aircana/configuration.rb +41 -0
  32. data/lib/aircana/contexts/confluence.rb +141 -0
  33. data/lib/aircana/contexts/confluence_content.rb +36 -0
  34. data/lib/aircana/contexts/confluence_http.rb +41 -0
  35. data/lib/aircana/contexts/confluence_logging.rb +71 -0
  36. data/lib/aircana/contexts/confluence_setup.rb +15 -0
  37. data/lib/aircana/contexts/local.rb +47 -0
  38. data/lib/aircana/contexts/relevant_files.rb +78 -0
  39. data/lib/aircana/fzf_helper.rb +117 -0
  40. data/lib/aircana/generators/agents_generator.rb +75 -0
  41. data/lib/aircana/generators/base_generator.rb +61 -0
  42. data/lib/aircana/generators/helpers.rb +16 -0
  43. data/lib/aircana/generators/relevant_files_command_generator.rb +36 -0
  44. data/lib/aircana/generators/relevant_files_verbose_results_generator.rb +34 -0
  45. data/lib/aircana/generators.rb +10 -0
  46. data/lib/aircana/human_logger.rb +143 -0
  47. data/lib/aircana/initializers.rb +8 -0
  48. data/lib/aircana/llm/claude_client.rb +86 -0
  49. data/lib/aircana/progress_tracker.rb +55 -0
  50. data/lib/aircana/system_checker.rb +177 -0
  51. data/lib/aircana/templates/agents/base_agent.erb +30 -0
  52. data/lib/aircana/templates/agents/defaults/planner.erb +126 -0
  53. data/lib/aircana/templates/agents/defaults/worker.erb +185 -0
  54. data/lib/aircana/templates/commands/add_relevant_files.erb +3 -0
  55. data/lib/aircana/templates/relevant_files_verbose_results.erb +18 -0
  56. data/lib/aircana/version.rb +5 -0
  57. data/lib/aircana.rb +53 -0
  58. data/sig/aircana.rbs +4 -0
  59. 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