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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.35.0",
3
+ "version": "0.35.2",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -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(source, target, 'junction')
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
- if (link_target === source_dir) {
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
- assert.equal(fs.readlinkSync(link_path), source)
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
- assert.equal(fs.readlinkSync(link_path), source)
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
 
@@ -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 meta_parts = [star_str, quality_str].filter(Boolean).join(' · ')
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
- print_json({ data: { query, mode: 'smart', results: mapped, count: mapped.length } })
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('')