ask-skills 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/lib/ask/skills/formatter.rb +19 -1
- data/lib/ask/skills/skill.rb +48 -2
- data/lib/ask/skills/sources/filesystem.rb +74 -5
- data/lib/ask/skills/version.rb +1 -1
- data/lib/ask/skills.rb +22 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 768239027d0fbaa9696f54f17dab21af118661b7d36c89969e2e8408ff75d8c0
|
|
4
|
+
data.tar.gz: 02ac858fc1be5fa4d9ffd93909cf828d8dafb6b9d134a4aac41ba8e3dfe46f38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a242038997b6697ce44fcea6372bcc7ce797dc1ce510e2a5d36216256efdaa34f41d095c2909851f0db8dc2fc12137e934d826fa0cc3eb2bbc8ec1f854161045
|
|
7
|
+
data.tar.gz: 478b061e75189527782cff6c567c37b2f629b5594e6839427536a87260fcc89cc383adc4e6db172338d33a8289d5a09b851788532aab3fd07fd8b531611786c4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
## [0.4.0] - 2026-07-21
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Enhanced frontmatter** — skill metadata now supports `tags`, `version`, `author`, and any custom fields alongside `name` and `description`. Frontmatter parser handles multi-line values and preserves all metadata in `skill.metadata`.
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
---
|
|
9
|
+
name: rails_debug
|
|
10
|
+
description: Debug Rails database issues
|
|
11
|
+
tags: rails, database, debugging
|
|
12
|
+
version: 2
|
|
13
|
+
---
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- **Sibling files in skills** — skills now discover sibling files and directories in the skill directory alongside `SKILL.md`. Recognized categories are discovered automatically: `references/`, `scripts/`, `assets/`. Unrecognized directories and flat files are also collected.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
rails_debug/
|
|
20
|
+
├── SKILL.md → instructions
|
|
21
|
+
├── references/
|
|
22
|
+
│ ├── migration_guide.md
|
|
23
|
+
│ └── apis.md
|
|
24
|
+
└── scripts/
|
|
25
|
+
└── db_check.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Accessible at runtime via `skill.references`, `skill.scripts`, `skill.assets`, and `skill.siblings`.
|
|
29
|
+
|
|
30
|
+
- **XML formatter** now includes `<tags>` and `<siblings>` sections when present. Markdown prompt entries show tags inline (`[rails, database]`).
|
|
31
|
+
|
|
32
|
+
- **`askr skills` CLI commands** — new `askr` subcommands for managing skills:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
askr skills list # All skills with descriptions and tags
|
|
36
|
+
askr skills show rails_debug # Full details + instructions + siblings
|
|
37
|
+
askr skills search deploy # Filter by name, description, or tags
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- `Skill` data object now includes `metadata` (Hash) and `siblings` (Hash) fields with default empty values. Backward compatible — existing `Skill.new(...)` calls without these fields get empty defaults.
|
|
43
|
+
- `Source::Filesystem#parse_frontmatter` rewritten to support multi-line values and arbitrary metadata keys (not just `name`/`description`).
|
|
44
|
+
- `Ask::Skills.parse_frontmatter` updated to match, with new `process_metadata_value` helper.
|
|
45
|
+
|
|
46
|
+
### Tested
|
|
47
|
+
|
|
48
|
+
- 18 new tests for: tags parsing, metadata preservation, sibling discovery (references, scripts, assets, flat files), hidden file filtering, empty sibling handling, formatter output with tags/siblings, and CLI commands.
|
|
49
|
+
- Full suite: 97 tests, 265 assertions — 0 failures.
|
|
50
|
+
|
|
1
51
|
## [0.3.0] - 2026-07-21
|
|
2
52
|
|
|
3
53
|
### Added
|
data/lib/ask/skills/formatter.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ask
|
|
2
4
|
module Skills
|
|
3
5
|
class Formatter
|
|
@@ -10,7 +12,11 @@ module Ask
|
|
|
10
12
|
|
|
11
13
|
lines = ["", "## Available Skills", ""]
|
|
12
14
|
@skills.each_value do |skill|
|
|
13
|
-
|
|
15
|
+
line = skill.to_prompt_entry
|
|
16
|
+
if skill.tags.any?
|
|
17
|
+
line += " [#{skill.tags.join(", ")}]"
|
|
18
|
+
end
|
|
19
|
+
lines << line
|
|
14
20
|
end
|
|
15
21
|
lines << ""
|
|
16
22
|
lines << "When a task matches a skill's description, load it for step-by-step methodology."
|
|
@@ -26,6 +32,18 @@ module Ask
|
|
|
26
32
|
lines << " <skill>"
|
|
27
33
|
lines << " <name>#{escape_xml(skill.name)}</name>"
|
|
28
34
|
lines << " <description>#{escape_xml(skill.description)}</description>"
|
|
35
|
+
if skill.tags.any?
|
|
36
|
+
lines << " <tags>#{escape_xml(skill.tags.join(", "))}</tags>"
|
|
37
|
+
end
|
|
38
|
+
if skill.siblings.any?
|
|
39
|
+
lines << " <siblings>"
|
|
40
|
+
skill.siblings.each do |category, files|
|
|
41
|
+
lines << " <#{category}>"
|
|
42
|
+
files.each { |f| lines << " <file>#{escape_xml(f)}</file>" }
|
|
43
|
+
lines << " </#{category}>"
|
|
44
|
+
end
|
|
45
|
+
lines << " </siblings>"
|
|
46
|
+
end
|
|
29
47
|
lines << " </skill>"
|
|
30
48
|
end
|
|
31
49
|
lines << "</available_skills>"
|
data/lib/ask/skills/skill.rb
CHANGED
|
@@ -1,12 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ask
|
|
2
4
|
module Skills
|
|
3
|
-
|
|
5
|
+
# A discovered skill with its metadata, instructions, and sibling files.
|
|
6
|
+
#
|
|
7
|
+
# Skills live in directories following the SKILL.md convention:
|
|
8
|
+
#
|
|
9
|
+
# rails_debug/
|
|
10
|
+
# ├── SKILL.md → name, description, tags, instructions
|
|
11
|
+
# ├── references/ → reference documents (loaded on demand)
|
|
12
|
+
# ├── scripts/ → executable helpers
|
|
13
|
+
# └── assets/ → images, templates, other resources
|
|
14
|
+
#
|
|
15
|
+
# The +siblings+ hash maps category names to arrays of file paths,
|
|
16
|
+
# relative to the skill's directory. Categories are inferred from
|
|
17
|
+
# sibling directory names (references, scripts, assets) or files.
|
|
18
|
+
Skill = Data.define(:name, :description, :instructions, :source, :metadata, :siblings) do
|
|
19
|
+
def initialize(name:, description:, instructions:, source:, metadata: {}, siblings: {})
|
|
20
|
+
super(name: name, description: description, instructions: instructions,
|
|
21
|
+
source: source, metadata: metadata, siblings: siblings)
|
|
22
|
+
end
|
|
23
|
+
|
|
4
24
|
def to_s
|
|
5
25
|
"#{name}: #{description}"
|
|
6
26
|
end
|
|
7
27
|
|
|
8
28
|
def to_prompt_entry
|
|
9
|
-
"- **#{name}**: #{description}"
|
|
29
|
+
line = "- **#{name}**: #{description}"
|
|
30
|
+
line
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Reference documents bundled with this skill.
|
|
34
|
+
# @return [Array<String>] file paths relative to skill directory
|
|
35
|
+
def references
|
|
36
|
+
siblings["references"] || []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Executable scripts bundled with this skill.
|
|
40
|
+
# @return [Array<String>] file paths relative to skill directory
|
|
41
|
+
def scripts
|
|
42
|
+
siblings["scripts"] || []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Asset files bundled with this skill.
|
|
46
|
+
# @return [Array<String>] file paths relative to skill directory
|
|
47
|
+
def assets
|
|
48
|
+
siblings["assets"] || []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Comma-separated tags from frontmatter.
|
|
52
|
+
# @return [Array<String>]
|
|
53
|
+
def tags
|
|
54
|
+
raw = metadata["tags"] || metadata[:tags] || ""
|
|
55
|
+
raw.split(",").map(&:strip).reject(&:empty?)
|
|
10
56
|
end
|
|
11
57
|
end
|
|
12
58
|
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ask
|
|
2
4
|
module Skills
|
|
3
5
|
module Source
|
|
4
6
|
class Filesystem < Base
|
|
7
|
+
SIBLING_CATEGORIES = %w[references scripts assets].freeze
|
|
8
|
+
|
|
5
9
|
def initialize(dir: nil, project_dir: nil, user_dir: nil)
|
|
6
10
|
@path = dir || project_dir || (user_dir ? File.expand_path(user_dir) : nil)
|
|
7
11
|
end
|
|
@@ -35,25 +39,90 @@ module Ask
|
|
|
35
39
|
|
|
36
40
|
return nil if description.empty?
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
# Separate known metadata from reserved fields
|
|
43
|
+
reserved = %w[name description]
|
|
44
|
+
metadata = frontmatter.reject { |k, _| reserved.include?(k) }
|
|
45
|
+
|
|
46
|
+
# Discover sibling files
|
|
47
|
+
skill_dir = File.dirname(path)
|
|
48
|
+
siblings = discover_siblings(skill_dir)
|
|
49
|
+
|
|
50
|
+
Skill.new(
|
|
51
|
+
name: name,
|
|
52
|
+
description: description,
|
|
53
|
+
instructions: body,
|
|
54
|
+
source: path,
|
|
55
|
+
metadata: metadata,
|
|
56
|
+
siblings: siblings
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Scan the skill directory for sibling files and categorize them.
|
|
61
|
+
def discover_siblings(skill_dir)
|
|
62
|
+
siblings = {}
|
|
63
|
+
return siblings unless File.directory?(skill_dir)
|
|
64
|
+
|
|
65
|
+
Dir.entries(skill_dir).each do |entry|
|
|
66
|
+
next if entry.start_with?(".")
|
|
67
|
+
next if entry == "SKILL.md"
|
|
68
|
+
entry_path = File.join(skill_dir, entry)
|
|
69
|
+
|
|
70
|
+
if File.directory?(entry_path)
|
|
71
|
+
# Categorized sibling directories
|
|
72
|
+
category = entry
|
|
73
|
+
files = Dir.children(entry_path)
|
|
74
|
+
.reject { |f| f.start_with?(".") }
|
|
75
|
+
.map { |f| File.join(entry, f) }
|
|
76
|
+
siblings[category] = files if files.any?
|
|
77
|
+
else
|
|
78
|
+
# Individual files go into a general "files" category
|
|
79
|
+
siblings["files"] ||= []
|
|
80
|
+
siblings["files"] << entry
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
siblings
|
|
39
85
|
end
|
|
40
86
|
|
|
41
87
|
def parse_frontmatter(content)
|
|
42
88
|
return {} unless content.start_with?("---\n")
|
|
43
89
|
end_idx = content.index("\n---\n", 4)
|
|
44
90
|
return {} unless end_idx
|
|
91
|
+
|
|
45
92
|
yaml_str = content[4...end_idx]
|
|
46
93
|
yaml = {}
|
|
94
|
+
current_key = nil
|
|
95
|
+
current_value = nil
|
|
96
|
+
|
|
47
97
|
yaml_str.split("\n").each do |line|
|
|
48
|
-
if (m = line.match(/\A(\w
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
if (m = line.match(/\A(\w[\w_]*):\s*(.*)\z/))
|
|
99
|
+
# Store previous key if any
|
|
100
|
+
if current_key
|
|
101
|
+
yaml[current_key] = process_value(current_value.strip)
|
|
102
|
+
end
|
|
103
|
+
current_key = m[1]
|
|
104
|
+
current_value = m[2].strip
|
|
105
|
+
elsif current_key && line.match?(/\A\s+/)
|
|
106
|
+
# Continuation of multi-line value (e.g. tags on next line)
|
|
107
|
+
current_value << " #{line.strip}"
|
|
52
108
|
end
|
|
53
109
|
end
|
|
110
|
+
|
|
111
|
+
# Store last key
|
|
112
|
+
if current_key
|
|
113
|
+
yaml[current_key] = process_value(current_value.strip)
|
|
114
|
+
end
|
|
115
|
+
|
|
54
116
|
yaml
|
|
55
117
|
end
|
|
56
118
|
|
|
119
|
+
def process_value(value)
|
|
120
|
+
return value unless value
|
|
121
|
+
# Remove surrounding quotes
|
|
122
|
+
value = value.gsub(/\A"|"\z/, "").gsub(/\A'|'\z/, "")
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
57
126
|
def extract_body(content)
|
|
58
127
|
return content unless content.start_with?("---\n")
|
|
59
128
|
end_idx = content.index("\n---\n", 4)
|
data/lib/ask/skills/version.rb
CHANGED
data/lib/ask/skills.rb
CHANGED
|
@@ -61,18 +61,36 @@ module Ask
|
|
|
61
61
|
return {} unless content.start_with?("---\n")
|
|
62
62
|
end_idx = content.index("\n---\n", 4)
|
|
63
63
|
return {} unless end_idx
|
|
64
|
+
|
|
64
65
|
yaml_str = content[4...end_idx]
|
|
65
66
|
yaml = {}
|
|
67
|
+
current_key = nil
|
|
68
|
+
current_value = nil
|
|
69
|
+
|
|
66
70
|
yaml_str.split("\n").each do |line|
|
|
67
|
-
if (m = line.match(/\A(\w
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
if (m = line.match(/\A(\w[\w_]*):\s*(.*)\z/))
|
|
72
|
+
if current_key
|
|
73
|
+
yaml[current_key] = process_metadata_value(current_value.strip)
|
|
74
|
+
end
|
|
75
|
+
current_key = m[1]
|
|
76
|
+
current_value = m[2].strip
|
|
77
|
+
elsif current_key && line.match?(/\A\s+/)
|
|
78
|
+
current_value << " #{line.strip}"
|
|
71
79
|
end
|
|
72
80
|
end
|
|
81
|
+
|
|
82
|
+
if current_key
|
|
83
|
+
yaml[current_key] = process_metadata_value(current_value.strip)
|
|
84
|
+
end
|
|
85
|
+
|
|
73
86
|
yaml
|
|
74
87
|
end
|
|
75
88
|
|
|
89
|
+
def process_metadata_value(value)
|
|
90
|
+
return value unless value
|
|
91
|
+
value.gsub(/\A"|"\z/, "").gsub(/\A'|'\z/, "")
|
|
92
|
+
end
|
|
93
|
+
|
|
76
94
|
# Extract the markdown body from a file with frontmatter.
|
|
77
95
|
def extract_body(content)
|
|
78
96
|
return content unless content.start_with?("---\n")
|