Class: Aircana::Generators::SkillsGenerator

Inherits:
BaseGenerator show all
Defined in:
lib/aircana/generators/skills_generator.rb

Overview

rubocop:disable Metrics/ClassLength

Instance Attribute Summary collapse

Attributes inherited from BaseGenerator

#file_in, #file_out

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseGenerator

#generate

Constructor Details

#initialize(kb_name:, short_description: nil, skill_description: nil, knowledge_files: [], file_in: nil, file_out: nil) ⇒ SkillsGenerator

rubocop:disable Metrics/ParameterLists



13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/aircana/generators/skills_generator.rb', line 13

def initialize(kb_name:, short_description: nil, skill_description: nil, knowledge_files: [], file_in: nil,
               file_out: nil)
  @kb_name = kb_name
  @short_description = short_description
  @skill_description = skill_description || generate_skill_description
  @knowledge_files = knowledge_files

  super(
    file_in: file_in || default_template_path,
    file_out: file_out || default_output_path
  )
end

Instance Attribute Details

#kb_nameObject (readonly)

Returns the value of attribute kb_name.



10
11
12
# File 'lib/aircana/generators/skills_generator.rb', line 10

def kb_name
  @kb_name
end

#knowledge_filesObject (readonly)

Returns the value of attribute knowledge_files.



10
11
12
# File 'lib/aircana/generators/skills_generator.rb', line 10

def knowledge_files
  @knowledge_files
end

#short_descriptionObject (readonly)

Returns the value of attribute short_description.



10
11
12
# File 'lib/aircana/generators/skills_generator.rb', line 10

def short_description
  @short_description
end

#skill_descriptionObject (readonly)

Returns the value of attribute skill_description.



10
11
12
# File 'lib/aircana/generators/skills_generator.rb', line 10

def skill_description
  @skill_description
end

Class Method Details

.extract_files_from_disk(manifest, kb_dir) ⇒ Object

rubocop:disable Metrics/MethodLength



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/aircana/generators/skills_generator.rb', line 68

def self.extract_files_from_disk(manifest, kb_dir)
  actual_files = Dir.glob(File.join(kb_dir, "*.md"))
                    .reject { |f| File.basename(f) == "SKILL.md" }
                    .sort

  # Build file list with summaries from manifest
  actual_files.map do |filepath|
    filename = File.basename(filepath)
    summary = find_summary_for_file(manifest, filename)

    {
      summary: summary || File.basename(filename, ".md").tr("-", " ").capitalize,
      filename: filename
    }
  end
end

.extract_files_from_manifest_metadata(manifest) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength rubocop:disable Metrics/PerceivedComplexity



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/aircana/generators/skills_generator.rb', line 88

def self.(manifest)
  files = []

  manifest["sources"]&.each do |source|
    case source["type"]
    when "confluence"
      source["pages"]&.each do |page|
        files << {
          summary: page["summary"] || "Documentation",
          filename: "#{sanitize_filename_from_id(page["id"])}.md"
        }
      end
    when "web"
      source["urls"]&.each do |url_entry|
        files << {
          summary: url_entry["summary"] || "Web resource",
          filename: "#{sanitize_filename_from_url(url_entry["url"])}.md"
        }
      end
    end
  end

  files
end

.extract_knowledge_files_from_manifest(manifest) ⇒ Object

Class methods for manifest processing



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/aircana/generators/skills_generator.rb', line 53

def self.extract_knowledge_files_from_manifest(manifest)
  kb_name = manifest["name"]

  # If kb_dir exists, scan actual files on disk (preferred method)
  if kb_name && Aircana.configuration.kb_knowledge_dir
    kb_dir = Aircana.configuration.kb_path(kb_name)

    return extract_files_from_disk(manifest, kb_dir) if Dir.exist?(kb_dir)
  end

  # Fallback: extract from manifest metadata (for tests or before files are created)
  (manifest)
end

.find_summary_for_file(manifest, filename) ⇒ Object

Find the summary for a given filename from the manifest rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength rubocop:disable Metrics/PerceivedComplexity



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/aircana/generators/skills_generator.rb', line 118

def self.find_summary_for_file(manifest, filename)
  manifest["sources"]&.each do |source|
    case source["type"]
    when "confluence"
      source["pages"]&.each do |page|
        # Match by page ID (for old manifests) or by sanitized title (for new manifests)
        return page["summary"] if filename.include?(page["id"])

        # Try matching by sanitized title if available
        if page["title"]
          sanitized_title = sanitize_filename(page["title"])
          return page["summary"] if filename.include?(sanitized_title)
        end
      end
    when "web"
      source["urls"]&.each do |url_entry|
        sanitized_url_part = sanitize_filename_from_url(url_entry["url"])
        return url_entry["summary"] if filename.include?(sanitized_url_part)
      end
    end
  end

  nil
end

.from_manifest(kb_name) ⇒ Object

Generate SKILL.md based on manifest data rubocop:disable Metrics/MethodLength

Raises:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/aircana/generators/skills_generator.rb', line 29

def self.from_manifest(kb_name)
  manifest = Contexts::Manifest.read_manifest(kb_name)
  raise Error, "No manifest found for knowledge base '#{kb_name}'" unless manifest

  knowledge_files = extract_knowledge_files_from_manifest(manifest)

  # Warn if no knowledge files were found
  if knowledge_files.empty?
    Aircana.human_logger.warn "No knowledge files found for KB '#{kb_name}'. " \
                              "SKILL.md will be generated but will be empty. " \
                              "Run 'aircana kb refresh #{kb_name}' to fetch knowledge."
  end

  skill_description = generate_skill_description_from_manifest(manifest, kb_name)

  new(
    kb_name: kb_name,
    skill_description: skill_description,
    knowledge_files: knowledge_files
  )
end

.generate_skill_description_from_manifest(manifest, kb_name) ⇒ Object



159
160
161
162
163
# File 'lib/aircana/generators/skills_generator.rb', line 159

def self.generate_skill_description_from_manifest(manifest, kb_name)
  # Generate a description optimized for Claude's skill discovery
  source_count = manifest["sources"]&.size || 0
  "Discover critical context for #{kb_name.split("-").join(" ")} from #{source_count} knowledge sources"
end

.sanitize_filename(title) ⇒ Object

rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength rubocop:enable Metrics/PerceivedComplexity



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/aircana/generators/skills_generator.rb', line 145

def self.sanitize_filename(title)
  # Match the sanitization logic in Local#sanitize_filename
  sanitized = title.strip
                   .gsub(%r{[<>:"/\\|?*]}, "-")
                   .gsub(/\s+/, "-")
                   .gsub(/-+/, "-")
                   .gsub(/^-|-$/, "")

  sanitized = "untitled" if sanitized.empty?
  sanitized = sanitized[0, 200] if sanitized.length > 200

  sanitized
end

.sanitize_filename_from_id(page_id) ⇒ Object



165
166
167
# File 'lib/aircana/generators/skills_generator.rb', line 165

def self.sanitize_filename_from_id(page_id)
  "page_#{page_id}"
end

.sanitize_filename_from_url(url) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/aircana/generators/skills_generator.rb', line 169

def self.sanitize_filename_from_url(url)
  # Extract meaningful name from URL
  uri = URI.parse(url)
  path_segments = uri.path.split("/").reject(&:empty?)

  if path_segments.any?
    path_segments.last.gsub(/[^a-z0-9\-_]/i, "-").gsub(/-+/, "-").downcase
  else
    uri.host.gsub(/[^a-z0-9\-_]/i, "-").gsub(/-+/, "-").downcase
  end
rescue URI::InvalidURIError
  "web_resource"
end