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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aircana
4
+ module CLI
5
+ module DoctorHelpers
6
+ module Logging
7
+ def log_success(label, message)
8
+ Aircana.human_logger.success " ✅ #{label.ljust(15)} #{message}"
9
+ end
10
+
11
+ def log_failure(label, message)
12
+ Aircana.human_logger.error " ❌ #{label.ljust(15)} #{message}"
13
+ end
14
+
15
+ def log_warning(label, message)
16
+ Aircana.human_logger.warn " ⚠️ #{label.ljust(15)} #{message}"
17
+ end
18
+
19
+ def log_info(label, message)
20
+ Aircana.human_logger.info " ℹ️ #{label.ljust(15)} #{message}"
21
+ end
22
+
23
+ def log_remedy(message)
24
+ Aircana.human_logger.info " → #{message}"
25
+ end
26
+ end
27
+
28
+ module SystemChecks
29
+ def command_available?(command)
30
+ system("which #{command}", out: File::NULL, err: File::NULL)
31
+ end
32
+
33
+ def claude_available?
34
+ claude_path = find_claude_path
35
+ !claude_path.nil? && File.executable?(claude_path)
36
+ end
37
+
38
+ def find_claude_path
39
+ possible_paths = [
40
+ File.expand_path("~/.claude/local/claude"),
41
+ `which claude 2>/dev/null`.strip,
42
+ "/usr/local/bin/claude"
43
+ ]
44
+
45
+ possible_paths.each do |path|
46
+ return path if !path.empty? && File.exist?(path) && File.executable?(path)
47
+ end
48
+
49
+ return "claude" if system("which claude > /dev/null 2>&1")
50
+
51
+ nil
52
+ end
53
+
54
+ def mcp_tool_installed?(result)
55
+ $CHILD_STATUS.success? && !result.include?("not found") && !result.include?("error")
56
+ end
57
+
58
+ def detect_os
59
+ return "macOS" if RUBY_PLATFORM.match?(/darwin/)
60
+ return "Ubuntu/Debian" if File.exist?("/etc/debian_version")
61
+
62
+ "Other"
63
+ end
64
+ end
65
+
66
+ module ConfigurationChecks
67
+ def confluence_configured?(config)
68
+ !config.confluence_base_url.to_s.empty? &&
69
+ !config.confluence_username.to_s.empty? &&
70
+ !config.confluence_api_token.to_s.empty?
71
+ end
72
+
73
+ def find_available_editors
74
+ %w[code subl atom nano vim vi].select { |cmd| command_available?(cmd) }
75
+ end
76
+
77
+ def check_directory(path, description)
78
+ expanded_path = File.expand_path(path)
79
+ if Dir.exist?(expanded_path)
80
+ log_success(File.basename(path), "#{description} exists")
81
+ else
82
+ log_info(File.basename(path), "#{description} not found")
83
+ log_remedy("Will be created on first use")
84
+ end
85
+ end
86
+ end
87
+
88
+ module InstallCommands
89
+ INSTALL_COMMANDS = {
90
+ "git" => {
91
+ "macOS" => "brew install git",
92
+ "Ubuntu/Debian" => "apt install git",
93
+ "Other" => "https://git-scm.com/downloads"
94
+ },
95
+ "fzf" => {
96
+ "macOS" => "brew install fzf",
97
+ "Ubuntu/Debian" => "apt install fzf",
98
+ "Other" => "https://github.com/junegunn/fzf#installation"
99
+ },
100
+ "bat" => {
101
+ "macOS" => "brew install bat",
102
+ "Ubuntu/Debian" => "apt install bat",
103
+ "Other" => "https://github.com/sharkdp/bat#installation"
104
+ },
105
+ "fd" => {
106
+ "macOS" => "brew install fd",
107
+ "Ubuntu/Debian" => "apt install fd-find",
108
+ "Other" => "https://github.com/sharkdp/fd#installation"
109
+ }
110
+ }.freeze
111
+
112
+ def install_command(tool)
113
+ os = detect_os
114
+ INSTALL_COMMANDS.dig(tool, os) || INSTALL_COMMANDS.dig(tool, "Other") || "Check package manager"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,23 @@
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 DumpContext
9
+ class << self
10
+ def run(_agent_name:, verbose: true)
11
+ Aircana.logger.level = Logger::ERROR
12
+ Contexts::RelevantFiles.print(verbose:)
13
+ end
14
+
15
+ private
16
+
17
+ def print(context)
18
+ Aircana.configuration.stream.puts context
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../generators/relevant_files_command_generator"
4
+ require_relative "../../generators/relevant_files_verbose_results_generator"
5
+ require_relative "../../generators/agents_generator"
6
+
7
+ module Aircana
8
+ module CLI
9
+ module Generate
10
+ class << self
11
+ def generators
12
+ @generators ||= [
13
+ Aircana::Generators::RelevantFilesVerboseResultsGenerator.new,
14
+ Aircana::Generators::RelevantFilesCommandGenerator.new
15
+ ]
16
+ end
17
+
18
+ def run
19
+ generators.each(&:generate)
20
+ generate_default_agents
21
+ Aircana.human_logger.success("Re-generated #{Aircana.configuration.output_dir} files.")
22
+ end
23
+
24
+ private
25
+
26
+ def generate_default_agents
27
+ Aircana::Generators::AgentsGenerator.available_default_agents.each do |agent_name|
28
+ Aircana::Generators::AgentsGenerator.create_default_agent(agent_name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generate"
4
+
5
+ module Aircana
6
+ module CLI
7
+ module Install
8
+ class << self
9
+ def run
10
+ ensure_output_exists
11
+ install_commands_to_claude
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_output_exists
17
+ return if Dir.exist?(Aircana.configuration.output_dir)
18
+
19
+ Aircana.human_logger.warn("No generated output files-auto generating now...")
20
+ Generate.run
21
+ end
22
+
23
+ def install_commands_to_claude
24
+ claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
25
+ Aircana.create_dir_if_needed(claude_commands_dir)
26
+
27
+ copy_command_files(claude_commands_dir)
28
+ install_agents_to_claude
29
+ end
30
+
31
+ def copy_command_files(destination_dir)
32
+ Dir.glob("#{Aircana.configuration.output_dir}/commands/*").each do |file|
33
+ Aircana.human_logger.success("Installing #{file} to #{destination_dir}")
34
+ FileUtils.cp(file, destination_dir)
35
+ end
36
+ end
37
+
38
+ def install_agents_to_claude
39
+ claude_agents_dir = File.join(Aircana.configuration.claude_code_project_config_path, "agents")
40
+ Aircana.create_dir_if_needed(claude_agents_dir)
41
+
42
+ copy_agent_files(claude_agents_dir)
43
+ end
44
+
45
+ def copy_agent_files(destination_dir)
46
+ agent_files_pattern = File.join(Aircana.configuration.claude_code_project_config_path, "agents", "*.md")
47
+ Dir.glob(agent_files_pattern).each do |file|
48
+ agent_name = File.basename(file, ".md")
49
+ next unless default_agent?(agent_name)
50
+
51
+ destination_file = File.join(destination_dir, File.basename(file))
52
+ # Skip copying if source and destination are the same
53
+ next if File.expand_path(file) == File.expand_path(destination_file)
54
+
55
+ Aircana.human_logger.success("Installing default agent #{file} to #{destination_dir}")
56
+ FileUtils.cp(file, destination_dir)
57
+ end
58
+ end
59
+
60
+ def default_agent?(agent_name)
61
+ require_relative "../../generators/agents_generator"
62
+ Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generate"
4
+ require_relative "install"
5
+ require_relative "../../generators/agents_generator"
6
+
7
+ module Aircana
8
+ module CLI
9
+ module Plan
10
+ class << self
11
+ def run
12
+ ensure_planner_agent_installed
13
+ launch_claude_with_planner
14
+ end
15
+
16
+ private
17
+
18
+ def ensure_planner_agent_installed
19
+ planner_agent_path = File.join(
20
+ Aircana.configuration.claude_code_config_path,
21
+ "agents",
22
+ "planner.md"
23
+ )
24
+
25
+ return if File.exist?(planner_agent_path)
26
+
27
+ Aircana.human_logger.info("Planner agent not found. Generating and installing...")
28
+ Generate.run
29
+ Install.run
30
+ end
31
+
32
+ def launch_claude_with_planner
33
+ prompt = "Start a planning session with the 'planner' sub-agent"
34
+
35
+ Aircana.human_logger.info("Launching Claude Code with planner agent...")
36
+
37
+ claude_path = find_claude_path
38
+ if claude_path
39
+ system("#{claude_path} \"#{prompt}\"")
40
+ else
41
+ handle_claude_not_found(prompt)
42
+ end
43
+ end
44
+
45
+ def handle_claude_not_found(prompt)
46
+ error_message = "Claude Code command not found. " \
47
+ "Please make sure Claude Code is installed and in your PATH."
48
+ Aircana.human_logger.error(error_message)
49
+ Aircana.human_logger.info("You can manually start Claude Code and run: #{prompt}")
50
+ end
51
+
52
+ def find_claude_path
53
+ # Try common locations for Claude Code binary (same as ClaudeClient)
54
+ possible_paths = [
55
+ File.expand_path("~/.claude/local/claude"),
56
+ `/usr/bin/which claude`.strip,
57
+ "/usr/local/bin/claude"
58
+ ]
59
+
60
+ possible_paths.each do |path|
61
+ return path if !path.empty? && File.executable?(path)
62
+ end
63
+
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generate"
4
+ require_relative "install"
5
+ require_relative "../../generators/agents_generator"
6
+
7
+ module Aircana
8
+ module CLI
9
+ module Work
10
+ class << self
11
+ def run
12
+ ensure_worker_agent_installed
13
+ launch_claude_with_worker
14
+ end
15
+
16
+ private
17
+
18
+ def ensure_worker_agent_installed
19
+ worker_agent_path = File.join(
20
+ Aircana.configuration.claude_code_config_path,
21
+ "agents",
22
+ "worker.md"
23
+ )
24
+
25
+ return if File.exist?(worker_agent_path)
26
+
27
+ Aircana.human_logger.info("Worker agent not found. Generating and installing...")
28
+ Generate.run
29
+ Install.run
30
+ end
31
+
32
+ def launch_claude_with_worker
33
+ prompt = "Start a work session with the 'worker' sub-agent"
34
+
35
+ Aircana.human_logger.info("Launching Claude Code with worker agent...")
36
+
37
+ claude_path = find_claude_path
38
+ if claude_path
39
+ system("#{claude_path} \"#{prompt}\"")
40
+ else
41
+ handle_claude_not_found(prompt)
42
+ end
43
+ end
44
+
45
+ def handle_claude_not_found(prompt)
46
+ error_message = "Claude Code command not found. " \
47
+ "Please make sure Claude Code is installed and in your PATH."
48
+ Aircana.human_logger.error(error_message)
49
+ Aircana.human_logger.info("You can manually start Claude Code and run: #{prompt}")
50
+ end
51
+
52
+ def find_claude_path
53
+ # Try common locations for Claude Code binary (same as ClaudeClient)
54
+ possible_paths = [
55
+ File.expand_path("~/.claude/local/claude"),
56
+ `/usr/bin/which claude`.strip,
57
+ "/usr/local/bin/claude"
58
+ ]
59
+
60
+ possible_paths.each do |path|
61
+ return path if !path.empty? && File.executable?(path)
62
+ end
63
+
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aircana
4
+ module CLI
5
+ class ShellCommand
6
+ def self.run(command_string)
7
+ # Command execution logged by human_logger elsewhere if needed
8
+
9
+ `#{command_string}`
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Aircana
6
+ module CLI
7
+ class Subcommand < Thor
8
+ def self.banner(command, _namespace = nil, _subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
9
+ "#{basename} #{subcommand_prefix} #{command.usage}"
10
+ end
11
+
12
+ def self.subcommand_prefix
13
+ name.gsub(/.*::/, "").gsub(/^[A-Z]/) do |match|
14
+ match[0].downcase
15
+ end.gsub(/[A-Z]/) { |match| "-#{match[0].downcase}" } # rubocop:disable Style/MultilineBlockChain
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cli/app"
4
+
5
+ module Aircana
6
+ module CLI
7
+ end
8
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aircana
4
+ class Configuration
5
+ attr_accessor :global_dir, :project_dir, :relevant_project_files_dir, :stream, :output_dir,
6
+ :claude_code_config_path, :claude_code_project_config_path, :agent_knowledge_dir,
7
+ :confluence_base_url, :confluence_username, :confluence_api_token
8
+
9
+ def initialize
10
+ setup_directory_paths
11
+ setup_claude_code_paths
12
+ setup_stream
13
+ setup_confluence_config
14
+ end
15
+
16
+ private
17
+
18
+ def setup_directory_paths
19
+ @global_dir = File.join(Dir.home, ".aircana")
20
+ @project_dir = Dir.pwd
21
+ @relevant_project_files_dir = File.join(@project_dir, ".aircana", "relevant_files")
22
+ @output_dir = File.join(@global_dir, "aircana.out")
23
+ @agent_knowledge_dir = File.join(@project_dir, ".aircana", "agents")
24
+ end
25
+
26
+ def setup_claude_code_paths
27
+ @claude_code_config_path = File.join(Dir.home, ".claude")
28
+ @claude_code_project_config_path = File.join(Dir.pwd, ".claude")
29
+ end
30
+
31
+ def setup_stream
32
+ @stream = $stdout
33
+ end
34
+
35
+ def setup_confluence_config
36
+ @confluence_base_url = nil
37
+ @confluence_username = nil
38
+ @confluence_api_token = nil
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "reverse_markdown"
5
+ require_relative "local"
6
+ require_relative "confluence_logging"
7
+ require_relative "confluence_http"
8
+ require_relative "confluence_content"
9
+ require_relative "confluence_setup"
10
+
11
+ module Aircana
12
+ module Contexts
13
+ class Confluence
14
+ include HTTParty
15
+ include ConfluenceLogging
16
+ include ConfluenceHttp
17
+ include ConfluenceContent
18
+ include ConfluenceSetup
19
+
20
+ LABEL_PREFIX = "global"
21
+
22
+ def initialize
23
+ @local_storage = Local.new
24
+ end
25
+
26
+ def fetch_pages_for(agent:)
27
+ validate_configuration!
28
+ setup_httparty
29
+
30
+ pages = search_and_log_pages(agent)
31
+ return 0 if pages.empty?
32
+
33
+ process_pages(pages, agent)
34
+ pages.size
35
+ end
36
+
37
+ def search_and_log_pages(agent)
38
+ pages = ProgressTracker.with_spinner("Searching for pages labeled '#{agent}'") do
39
+ fetch_pages_by_label(agent)
40
+ end
41
+ log_pages_found(pages.size, agent)
42
+ pages
43
+ end
44
+
45
+ def process_pages(pages, agent)
46
+ ProgressTracker.with_batch_progress(pages, "Processing pages") do |page, _index|
47
+ store_page_as_markdown(page, agent)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def validate_configuration!
54
+ config = Aircana.configuration
55
+
56
+ validate_base_url(config)
57
+ validate_username(config)
58
+ validate_api_token(config)
59
+ end
60
+
61
+ def validate_base_url(config)
62
+ return unless config.confluence_base_url.nil? || config.confluence_base_url.empty?
63
+
64
+ raise Error, "Confluence base URL not configured"
65
+ end
66
+
67
+ def validate_username(config)
68
+ return unless config.confluence_username.nil? || config.confluence_username.empty?
69
+
70
+ raise Error, "Confluence username not configured"
71
+ end
72
+
73
+ def validate_api_token(config)
74
+ return unless config.confluence_api_token.nil? || config.confluence_api_token.empty?
75
+
76
+ raise Error, "Confluence API token not configured"
77
+ end
78
+
79
+ def fetch_pages_by_label(agent)
80
+ label_id = find_label_id(agent)
81
+ return [] if label_id.nil?
82
+
83
+ response = get_pages_for_label(label_id)
84
+ response["results"] || []
85
+ rescue HTTParty::Error, StandardError => e
86
+ handle_api_error("fetch pages for agent '#{agent}'", e, "Failed to fetch pages from Confluence")
87
+ end
88
+
89
+ def find_label_id(agent_name)
90
+ path = "/wiki/api/v2/labels"
91
+ query_params = { limit: 250, prefix: LABEL_PREFIX }
92
+ page_number = 1
93
+
94
+ label_id = search_labels_pagination(path, query_params, agent_name, page_number)
95
+ clear_pagination_line
96
+ label_id
97
+ end
98
+
99
+ def search_labels_pagination(path, query_params, agent_name, page_number)
100
+ loop do
101
+ response = fetch_labels_page(path, query_params, page_number)
102
+ label_id = find_matching_label_id(response, agent_name)
103
+ return label_id if label_id
104
+
105
+ next_cursor = extract_next_cursor(response)
106
+ break unless next_cursor
107
+
108
+ query_params[:cursor] = next_cursor
109
+ page_number += 1
110
+ end
111
+
112
+ nil
113
+ end
114
+
115
+ def fetch_labels_page(path, query_params, page_number)
116
+ log_request("GET", path, query_params.merge("Page" => page_number), pagination: true)
117
+ response = self.class.get(path, { query: query_params })
118
+ log_response(response, "Labels lookup (page #{page_number})", pagination: true)
119
+ validate_response(response)
120
+ response
121
+ end
122
+
123
+ def find_matching_label_id(response, agent_name)
124
+ labels = response["results"] || []
125
+ matching_label = labels.find { |label| label["name"] == agent_name }
126
+ matching_label&.[]("id")
127
+ end
128
+
129
+ def extract_next_cursor(response)
130
+ next_url = get_next_page_url(response)
131
+ return nil unless next_url&.include?("cursor=")
132
+
133
+ next_url.match(/cursor=([^&]+)/)[1]
134
+ end
135
+
136
+ def clear_pagination_line
137
+ print "\r\e[K"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aircana
4
+ module Contexts
5
+ module ConfluenceContent
6
+ def fetch_page_content(page_id)
7
+ Aircana.human_logger.info("Looking for page with ID `#{page_id}`")
8
+ response = get_page_content(page_id)
9
+ response.dig("body", "storage", "value") || ""
10
+ rescue HTTParty::Error, StandardError => e
11
+ handle_api_error("fetch content for page #{page_id}", e, "Failed to fetch page content")
12
+ end
13
+
14
+ def convert_to_markdown(html_content)
15
+ return "" if html_content.nil? || html_content.empty?
16
+
17
+ ReverseMarkdown.convert(html_content, github_flavored: true)
18
+ end
19
+
20
+ def log_pages_found(count, agent)
21
+ Aircana.human_logger.info "Found #{count} pages for agent '#{agent}'"
22
+ end
23
+
24
+ def store_page_as_markdown(page, agent)
25
+ content = page&.dig("body", "storage", "value") || fetch_page_content(page&.[]("id"))
26
+ markdown_content = convert_to_markdown(content)
27
+
28
+ @local_storage.store_content(
29
+ title: page&.[]("title"),
30
+ content: markdown_content,
31
+ agent: agent
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aircana
4
+ module Contexts
5
+ module ConfluenceHttp
6
+ def get_pages_for_label(label_id)
7
+ path = "/wiki/api/v2/labels/#{label_id}/pages"
8
+ query_params = { "body-format" => "storage", limit: 100 }
9
+
10
+ log_request("GET", path, query_params)
11
+
12
+ response = self.class.get(path, { query: query_params })
13
+ log_response(response, "Pages for label")
14
+ validate_response(response)
15
+ response
16
+ end
17
+
18
+ def get_page_content(page_id)
19
+ path = "/rest/api/content/#{page_id}"
20
+ query_params = { expand: "body.storage" }
21
+
22
+ log_request("GET", path, query_params)
23
+
24
+ response = self.class.get(path, { query: query_params })
25
+ log_response(response, "Page content")
26
+ validate_response(response)
27
+ response
28
+ end
29
+
30
+ def validate_response(response)
31
+ return if response.success?
32
+
33
+ raise Error, "HTTP #{response.code}: #{response.message}"
34
+ end
35
+
36
+ def get_next_page_url(response)
37
+ response.dig("_links", "next")
38
+ end
39
+ end
40
+ end
41
+ end