happyskills 0.35.0 → 0.35.2
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.
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/agents/linker.js +6 -2
- package/src/agents/linker.test.js +6 -2
- package/src/commands/search.js +24 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.2] - 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add match quality labels to smart search output — each result shows its confidence level (Strong match, Good match, Partial match, Weak match) based on cosine similarity
|
|
14
|
+
- Add weak-match notice when no results have strong or good match quality, warning that results may not match the user's intent
|
|
15
|
+
- Add `match_quality` and `match_notice` fields to JSON search output
|
|
16
|
+
|
|
17
|
+
## [0.35.1] - 2026-04-10
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Fix multi-agent skill symlinks using absolute paths instead of relative — absolute symlinks break when the repo is cloned or pulled on a different machine; now uses `path.relative()` for portable symlinks
|
|
21
|
+
- Fix enable skip-check failing to recognize existing relative symlinks — resolves both relative and absolute link targets before comparing
|
|
22
|
+
|
|
10
23
|
## [0.35.0] - 2026-04-10
|
|
11
24
|
|
|
12
25
|
### Added
|
package/package.json
CHANGED
package/src/agents/linker.js
CHANGED
|
@@ -43,8 +43,10 @@ const _exists = async (target_path) => {
|
|
|
43
43
|
*/
|
|
44
44
|
const _link_or_copy = async (source, target) => {
|
|
45
45
|
try {
|
|
46
|
+
// Use relative path so symlinks work across machines (clones/pulls)
|
|
47
|
+
const relative_source = path.relative(path.dirname(target), source)
|
|
46
48
|
// 'junction' works on Windows without admin; ignored on macOS/Linux
|
|
47
|
-
await fs.promises.symlink(
|
|
49
|
+
await fs.promises.symlink(relative_source, target, 'junction')
|
|
48
50
|
return { method: 'symlink' }
|
|
49
51
|
} catch (err) {
|
|
50
52
|
if (err.code === 'EPERM' || err.code === 'ENOTSUP') {
|
|
@@ -77,7 +79,9 @@ const link_to_agents = (source_dir, agents, options = {}) => catch_errors('Agent
|
|
|
77
79
|
if (is_link) {
|
|
78
80
|
try {
|
|
79
81
|
const link_target = await fs.promises.readlink(target)
|
|
80
|
-
|
|
82
|
+
// Resolve relative or absolute symlink to compare against source
|
|
83
|
+
const resolved_link = path.resolve(path.dirname(target), link_target)
|
|
84
|
+
if (resolved_link === source_dir) {
|
|
81
85
|
results.push({ agent_id: agent.id, path: target, method: 'symlink', skipped: true })
|
|
82
86
|
continue
|
|
83
87
|
}
|
|
@@ -71,7 +71,9 @@ describe('link_to_agents', () => {
|
|
|
71
71
|
const link_path = path.join(tmp, '.claude', 'skills', 'deploy-aws')
|
|
72
72
|
const stat = fs.lstatSync(link_path)
|
|
73
73
|
assert.ok(stat.isSymbolicLink())
|
|
74
|
-
|
|
74
|
+
// Symlink should be relative, not absolute
|
|
75
|
+
const link_target = fs.readlinkSync(link_path)
|
|
76
|
+
assert.equal(link_target, path.relative(path.dirname(link_path), source))
|
|
75
77
|
})
|
|
76
78
|
|
|
77
79
|
it('creates symlinks for multiple agents', async () => {
|
|
@@ -126,7 +128,9 @@ describe('link_to_agents', () => {
|
|
|
126
128
|
assert.equal(results[0].skipped, undefined)
|
|
127
129
|
|
|
128
130
|
const link_path = path.join(tmp, '.claude', 'skills', 'deploy-aws')
|
|
129
|
-
|
|
131
|
+
// Symlink should be relative, not absolute
|
|
132
|
+
const link_target = fs.readlinkSync(link_path)
|
|
133
|
+
assert.equal(link_target, path.relative(path.dirname(link_path), source))
|
|
130
134
|
})
|
|
131
135
|
})
|
|
132
136
|
|
package/src/commands/search.js
CHANGED
|
@@ -46,6 +46,13 @@ const QUALITY_TIERS = [
|
|
|
46
46
|
{ min: 20, label: 'Low quality', color: yellow },
|
|
47
47
|
]
|
|
48
48
|
|
|
49
|
+
const MATCH_QUALITY_LABELS = {
|
|
50
|
+
strong: { label: 'Strong match', color: cyan },
|
|
51
|
+
good: { label: 'Good match', color: cyan },
|
|
52
|
+
partial: { label: 'Partial match', color: yellow },
|
|
53
|
+
weak: { label: 'Weak match', color: yellow },
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
const get_quality_label = (score) => {
|
|
50
57
|
if (score == null) return null
|
|
51
58
|
for (const tier of QUALITY_TIERS) {
|
|
@@ -67,10 +74,12 @@ const format_smart_result = (item, index) => {
|
|
|
67
74
|
const name = `${item.workspace_slug}/${item.name}`
|
|
68
75
|
const stars = item.star_count || 0
|
|
69
76
|
const tier = get_quality_label(item.quality_score)
|
|
77
|
+
const match = item.match_quality ? MATCH_QUALITY_LABELS[item.match_quality] : null
|
|
70
78
|
|
|
71
79
|
const star_str = `★ ${stars}`
|
|
72
80
|
const quality_str = tier ? tier.color(tier.label) : ''
|
|
73
|
-
const
|
|
81
|
+
const match_str = match ? match.color(match.label) : ''
|
|
82
|
+
const meta_parts = [match_str, star_str, quality_str].filter(Boolean).join(' · ')
|
|
74
83
|
|
|
75
84
|
const num = ` ${String(index + 1).padStart(2)}. `
|
|
76
85
|
const name_and_meta = `${bold(name)}${meta_parts ? ` ${dim(meta_parts)}` : ''}`
|
|
@@ -114,6 +123,13 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
|
|
|
114
123
|
return
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
const has_strong_or_good = items.some(item =>
|
|
127
|
+
item.match_quality === 'strong' || item.match_quality === 'good'
|
|
128
|
+
)
|
|
129
|
+
const match_notice = !has_strong_or_good && items.length > 0
|
|
130
|
+
? 'No strong matches found. The results below are the closest available but may not match your intent.'
|
|
131
|
+
: null
|
|
132
|
+
|
|
117
133
|
if (args.flags.json) {
|
|
118
134
|
const mapped = items.map(item => ({
|
|
119
135
|
skill: `${item.workspace_slug}/${item.name}`,
|
|
@@ -126,17 +142,23 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
|
|
|
126
142
|
quality_score: item.quality_score != null ? item.quality_score : null,
|
|
127
143
|
quality_tier: get_quality_tier_name(item.quality_score),
|
|
128
144
|
relevance_score: item.relevance_score != null ? item.relevance_score : null,
|
|
145
|
+
match_quality: item.match_quality || null,
|
|
129
146
|
tags: item.tags || [],
|
|
130
147
|
download_count: item.download_count || 0,
|
|
131
148
|
created_at: item.created_at,
|
|
132
149
|
updated_at: item.updated_at,
|
|
133
150
|
}))
|
|
134
|
-
|
|
151
|
+
const data = { query, mode: 'smart', results: mapped, count: mapped.length }
|
|
152
|
+
if (match_notice) data.match_notice = match_notice
|
|
153
|
+
print_json({ data })
|
|
135
154
|
return
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
// Human-readable smart output
|
|
139
158
|
console.log(`\n${bold(`Skills for: "${query}"`)}\n`)
|
|
159
|
+
if (match_notice) {
|
|
160
|
+
console.log(` ${yellow(match_notice)}\n`)
|
|
161
|
+
}
|
|
140
162
|
items.forEach((item, i) => {
|
|
141
163
|
console.log(format_smart_result(item, i))
|
|
142
164
|
if (i < items.length - 1) console.log('')
|