Class: KairosMcp::AnthropicSkillParser

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/anthropic_skill_parser.rb

Overview

AnthropicSkillParser: Parses Anthropic skills format (YAML frontmatter + Markdown)

Directory structure:

skill_name/
├── skill_name.md       # Required: YAML frontmatter + Markdown content
├── scripts/            # Optional: Executable scripts (Python, Bash, Node, etc.)
├── assets/             # Optional: Templates, images, CSS, etc.
└── references/         # Optional: Reference materials, datasets

Defined Under Namespace

Classes: SkillEntry

Class Method Summary collapse

Class Method Details

.atomic_write(target_path, content) ⇒ Object

Atomic-rename write. Tempfile in same directory then File.rename to the target. Crash mid-write leaves target either fully replaced or untouched — never truncated. Security-grounded (don't destroy someone else's valid record), not durability-grounded.



134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 134

def atomic_write(target_path, content)
  dir = File.dirname(target_path)
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
  tempname = "#{File.basename(target_path)}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
  temp_path = File.join(dir, tempname)
  begin
    File.write(temp_path, content)
    File.rename(temp_path, target_path)
  ensure
    File.delete(temp_path) if File.exist?(temp_path)
  end
end

.create(base_dir, name, content, create_subdirs: false) ⇒ SkillEntry

Create a new skill directory with the Anthropic format

Parameters:

  • base_dir (String)

    Base directory (knowledge/ or context/session_id/)

  • name (String)

    Skill name

  • content (String)

    Full content including YAML frontmatter

  • create_subdirs (Boolean) (defaults to: false)

    Whether to create scripts/assets/references dirs

Returns:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 101

def create(base_dir, name, content, create_subdirs: false)
  skill_dir = File.join(base_dir, name)
  FileUtils.mkdir_p(skill_dir)

  md_file = File.join(skill_dir, "#{name}.md")
  atomic_write(md_file, content)

  if create_subdirs
    FileUtils.mkdir_p(File.join(skill_dir, 'scripts'))
    FileUtils.mkdir_p(File.join(skill_dir, 'assets'))
    FileUtils.mkdir_p(File.join(skill_dir, 'references'))
  end

  parse(skill_dir)
end

.extract_frontmatter(content) ⇒ Array<Hash, String>

Extract frontmatter and body from content

Parameters:

  • content (String)

    Full file content

Returns:

  • (Array<Hash, String>)

    [frontmatter, body]



214
215
216
217
218
219
220
221
222
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 214

def extract_frontmatter(content)
  if content =~ /\A---\r?\n(.+?)\r?\n---\r?\n(.*)/m
    frontmatter = YAML.safe_load($1, permitted_classes: [Symbol, Date, Time]) || {}
    body = $2
    [frontmatter, body]
  else
    [{}, content]
  end
end

.generate_content(frontmatter, body) ⇒ String

Generate YAML frontmatter + Markdown content

Parameters:

  • frontmatter (Hash)

    Frontmatter data

  • body (String)

    Markdown body content

Returns:

  • (String)

    Complete file content



205
206
207
208
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 205

def generate_content(frontmatter, body)
  yaml_str = frontmatter.to_yaml.sub(/^---\n/, '')
  "---\n#{yaml_str}---\n\n#{body}"
end

.list_assets(skill) ⇒ Array<Hash>

List all assets in a skill

Parameters:

Returns:

  • (Array<Hash>)

    List of asset info



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 168

def list_assets(skill)
  return [] unless skill.has_assets?

  Dir[File.join(skill.assets_path, '**/*')].select { |f| File.file?(f) && inside_entry?(skill.base_path, f) }.map do |f|
    {
      name: File.basename(f),
      path: f,
      relative_path: f.sub(skill.assets_path + '/', ''),
      size: File.size(f),
      extension: File.extname(f)
    }
  end
end

.list_references(skill) ⇒ Array<Hash>

List all references in a skill

Parameters:

Returns:

  • (Array<Hash>)

    List of reference info



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 186

def list_references(skill)
  return [] unless skill.has_references?

  Dir[File.join(skill.references_path, '**/*')].select { |f| File.file?(f) && inside_entry?(skill.base_path, f) }.map do |f|
    {
      name: File.basename(f),
      path: f,
      relative_path: f.sub(skill.references_path + '/', ''),
      size: File.size(f),
      extension: File.extname(f)
    }
  end
end

.list_scripts(skill) ⇒ Array<Hash>

List all scripts in a skill

Parameters:

Returns:

  • (Array<Hash>)

    List of script info



151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 151

def list_scripts(skill)
  return [] unless skill.has_scripts?

  Dir[File.join(skill.scripts_path, '*')].select { |f| inside_entry?(skill.base_path, f) }.map do |f|
    {
      name: File.basename(f),
      path: f,
      executable: File.executable?(f),
      size: File.size(f)
    }
  end
end

.parse(skill_dir) ⇒ SkillEntry?

Parse a skill directory and return a SkillEntry

Parameters:

  • skill_dir (String)

    Path to the skill directory

Returns:

  • (SkillEntry, nil)

    Parsed skill entry or nil if invalid



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
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 67

def parse(skill_dir)
  return nil unless File.directory?(skill_dir)

  md_file = find_md_file(skill_dir)
  return nil unless md_file

  content = File.read(md_file, encoding: 'UTF-8')
  frontmatter, body = extract_frontmatter(content)

  skill_name = File.basename(skill_dir)

  SkillEntry.new(
    name: frontmatter['name'] || skill_name,
    description: frontmatter['description'],
    version: frontmatter['version'],
    layer: frontmatter['layer'],
    tags: normalize_tags(frontmatter['tags']),
    content: body.strip,
    frontmatter: frontmatter,
    base_path: skill_dir,
    md_file_path: md_file,
    scripts_path: File.join(skill_dir, 'scripts'),
    assets_path: File.join(skill_dir, 'assets'),
    references_path: File.join(skill_dir, 'references')
  )
end

.update(skill_dir, new_content) ⇒ SkillEntry

Update an existing skill's content

Parameters:

  • skill_dir (String)

    Path to the skill directory

  • new_content (String)

    New content including YAML frontmatter

Returns:



122
123
124
125
126
127
128
# File 'lib/kairos_mcp/anthropic_skill_parser.rb', line 122

def update(skill_dir, new_content)
  md_file = find_md_file(skill_dir)
  raise "No markdown file found in #{skill_dir}" unless md_file

  atomic_write(md_file, new_content)
  parse(skill_dir)
end