Class: Norn::Skill

Inherits:
Object
  • Object
show all
Defined in:
lib/norn/skill.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, triggers:, invocable:, argument_hint:, instructions:, location:) ⇒ Skill

Returns a new instance of Skill.



7
8
9
10
11
12
13
14
15
# File 'lib/norn/skill.rb', line 7

def initialize(name:, description:, triggers:, invocable:, argument_hint:, instructions:, location:)
  @name = name.to_s.strip
  @description = description.to_s.strip
  @triggers = Array(triggers).map { |t| t.to_s.strip.downcase }
  @invocable = !!invocable
  @argument_hint = argument_hint ? argument_hint.to_s.strip : nil
  @instructions = instructions.to_s.strip
  @location = location ? File.expand_path(location) : nil
end

Instance Attribute Details

#argument_hintObject (readonly)

Returns the value of attribute argument_hint.



5
6
7
# File 'lib/norn/skill.rb', line 5

def argument_hint
  @argument_hint
end

#descriptionObject (readonly)

Returns the value of attribute description.



5
6
7
# File 'lib/norn/skill.rb', line 5

def description
  @description
end

#instructionsObject (readonly)

Returns the value of attribute instructions.



5
6
7
# File 'lib/norn/skill.rb', line 5

def instructions
  @instructions
end

#invocableObject (readonly)

Returns the value of attribute invocable.



5
6
7
# File 'lib/norn/skill.rb', line 5

def invocable
  @invocable
end

#locationObject (readonly)

Returns the value of attribute location.



5
6
7
# File 'lib/norn/skill.rb', line 5

def location
  @location
end

#nameObject (readonly)

Returns the value of attribute name.



5
6
7
# File 'lib/norn/skill.rb', line 5

def name
  @name
end

#triggersObject (readonly)

Returns the value of attribute triggers.



5
6
7
# File 'lib/norn/skill.rb', line 5

def triggers
  @triggers
end

Class Method Details

.parse_content(content, filepath = nil) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/norn/skill.rb', line 51

def parse_content(content, filepath = nil)
  # Match YAML frontmatter between --- and ---
  match = content.match(/\A---\s*\n(.*?)\n---\s*\n(.*)/m)
  unless match
    return nil
  end

  frontmatter_raw = match[1]
  body = match[2].strip

  frontmatter = nil
  begin
    frontmatter = YAML.safe_load(frontmatter_raw, permitted_classes: [Symbol, Date, Time])
  rescue => e
    # Retry with sanitized YAML
    begin
      sanitized = sanitize_malformed_yaml(frontmatter_raw)
      frontmatter = YAML.safe_load(sanitized, permitted_classes: [Symbol, Date, Time])
    rescue => e2
      warn "Norn Skill Parser Error: Failed to parse frontmatter YAML at #{filepath || 'string'}: #{e2.message}"
      return nil
    end
  end

  return nil unless frontmatter.is_a?(Hash)

  # Normalize keys (symbolize, downcase, substitute hyphens)
  normalized_fm = {}
  frontmatter.each do |k, v|
    norm_key = k.to_s.downcase.gsub("-", "_").to_sym
    normalized_fm[norm_key] = v
  end

  # Validation
  name = normalized_fm[:name]&.to_s&.strip
  if (name.nil? || name.empty?) && filepath
    name = File.basename(File.dirname(filepath))
  end

  if name.nil? || name.empty?
    warn "Norn Skill Parser Warning: Skill is missing a 'name' field."
    return nil
  end

  description = normalized_fm[:description]&.to_s&.strip

  if description.nil? || description.empty?
    warn "Norn Skill Parser Warning: Skill '#{name}' is missing a 'description' field."
    return nil
  end

  # Check directory name matches
  if filepath
    parent_dir_name = File.basename(File.dirname(filepath))
    if name != parent_dir_name
      warn "Norn Skill Parser Warning: Skill name '#{name}' does not match its parent directory name '#{parent_dir_name}'."
    end
  end

  new(
    name: name,
    description: description,
    triggers: normalized_fm[:triggers] || [],
    invocable: normalized_fm[:invocable],
    argument_hint: normalized_fm[:argument_hint] || normalized_fm[:argument_hint_hint], # handle various spellings
    instructions: body,
    location: filepath
  )
end

.parse_file(filepath) ⇒ Object



45
46
47
48
49
# File 'lib/norn/skill.rb', line 45

def parse_file(filepath)
  return nil unless File.exist?(filepath)
  content = File.read(filepath)
  parse_content(content, filepath)
end

Instance Method Details

#base_directoryObject

Return the parent directory of the skill file



18
19
20
21
# File 'lib/norn/skill.rb', line 18

def base_directory
  return nil unless @location
  File.dirname(@location)
end

#matches?(text) ⇒ Boolean

Checks if a given text string matches any of the skill's triggers

Returns:

  • (Boolean)


37
38
39
40
41
42
# File 'lib/norn/skill.rb', line 37

def matches?(text)
  normalized_text = text.to_s.downcase
  @triggers.any? do |trigger|
    normalized_text.include?(trigger)
  end
end

#resourcesObject

Scan the base directory for supporting resources (all files excluding SKILL.md itself)



24
25
26
27
28
29
30
31
32
33
34
# File 'lib/norn/skill.rb', line 24

def resources
  dir = base_directory
  return [] unless dir && Dir.exist?(dir)

  # Find all files recursively under the skill directory
  Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH)
    .select { |f| File.file?(f) }
    .map { |f| File.expand_path(f) }
    .reject { |f| File.basename(f) == "SKILL.md" }
    .map { |f| Pathname.new(f).relative_path_from(Pathname.new(dir)).to_s }
end