happyskills 0.42.0 → 0.43.1

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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.1] - 2026-05-11
11
+
12
+ ### Fixed
13
+ - Fix `status` (no skill argument) filtering out transitive dependencies. The command's help text describes it as "Show divergence status for installed skills," but the previous behavior filtered `data?.requested_by?.includes('__root__')` — so only direct installs appeared, hiding diverged or outdated transitive deps from the result set. The empty-state message ("No root-level skills found") leaked the filter as a user-facing concept. Now returns all entries in `skills-lock.json` (matching the help text and `list`'s behavior), and the empty-state message reads "No installed skills found." Targeted status (`happyskills status owner/name`) is unaffected — that path never went through the filter.
14
+
15
+ ## [0.43.0] - 2026-05-07
16
+
17
+ ### Added
18
+ - Route the default `search` path to the new `POST /repos:search` dispatcher (API v2.9.0+). The same `happyskills search foo` command now picks the right strategy automatically based on the query shape — natural-language queries go to semantic hybrid search (existing behavior, unchanged), single dashed/word tokens like `deploy-aws` go to typo-tolerant fuzzy slug search, and `workspace/skill` form like `letta-ai/remotion-best-practices` goes to fuzzy scoped search (typo-tolerant on both halves). The CLI doesn't pre-decide the mode — it sends the raw query and the server picks. Implementation: new `dispatch_search` in `cli/src/api/repos.js`; default branch in `cli/src/commands/search.js` now calls it.
19
+ - Surface the chosen mode in human output. The result header now shows a `[mode]` chip (e.g. `Skills for: "deploy-aws" [fuzzy_slug]`) so users can see how the dispatcher routed without inspecting JSON. JSON output gains a top-level `mode` field, plus `workspace_match` when the request was `fuzzy_scoped`.
20
+ - Add an honest-failure path for `workspace/skill` lookups when the workspace doesn't match. Typing `happyskills search helo/do-something` (where `helo` doesn't fuzzy-match any workspace) now prints `No workspace matched "helo".` with a hint to remove the prefix to search globally. Previously a workspace-fuzzy miss would have silently returned nothing useful — the new path explains *why*.
21
+
22
+ ### Changed
23
+ - Rewrite the `search` command's help text to describe the three+list modes explicitly, with one example query per mode. The `--exact` flag is preserved as the explicit "skip the smart routing, just do keyword FTS" escape hatch.
24
+ - Trust the server's `match_notice` field instead of recomputing it client-side. The "No strong matches found..." notice was previously recomputed by the CLI based on `match_quality` — which produced a misleading warning for fuzzy-mode results (a tier-1 partial match is exactly what the user typed, not "may not match your intent"). The server now decides whether to emit a notice (only for `semantic` and `fuzzy_scoped` workspace-not-found cases); the CLI displays whatever it sends, verbatim.
25
+ - The legacy `semantic_search` API client function is now a deprecated alias of `dispatch_search` that forces `mode='semantic'` — kept for any internal callers that still reference it. New code should use `dispatch_search`.
26
+
10
27
  ## [0.42.0] - 2026-05-05
11
28
 
12
29
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.42.0",
3
+ "version": "0.43.1",
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)",
package/src/api/repos.js CHANGED
@@ -87,9 +87,32 @@ const get_tree = (owner, repo, ref) => catch_errors(`Get tree for ${owner}/${rep
87
87
  return data
88
88
  })
89
89
 
90
+ // POST /repos:search — intelligent dispatcher.
91
+ // The server inspects `q` and routes to semantic / fuzzy_slug / fuzzy_scoped
92
+ // automatically. We pass the raw query through; trim + lowercase happens
93
+ // server-side. Always sends auth — when logged in, accessible private skills
94
+ // are included; when not, the client silently skips the auth header.
95
+ const dispatch_search = (query, options = {}) => catch_errors('Search failed', async () => {
96
+ const body = {
97
+ q: query,
98
+ tags: options.tags ? options.tags.split(',').map(t => t.trim()).filter(Boolean) : null,
99
+ type: options.type || null,
100
+ limit: options.limit || 10,
101
+ min_stars: null,
102
+ workspace_slugs: options.workspace_slug ? options.workspace_slug.split(',').map(s => s.trim()).filter(Boolean) : null,
103
+ scope: options.workspace_slug ? null : (options.scope || null),
104
+ }
105
+ const [errors, data] = await client.post('/repos:search', body, { auth: true })
106
+ if (errors) throw errors[errors.length - 1]
107
+ return data
108
+ })
109
+
110
+ // Deprecated alias — kept for backwards compatibility with older code paths.
111
+ // Prefer `dispatch_search`. Forces mode='semantic' on the server.
90
112
  const semantic_search = (query, options = {}) => catch_errors('Semantic search failed', async () => {
91
113
  const body = {
92
114
  q: query,
115
+ mode: 'semantic',
93
116
  tags: options.tags ? options.tags.split(',').map(t => t.trim()).filter(Boolean) : null,
94
117
  type: options.type || null,
95
118
  limit: options.limit || 10,
@@ -97,12 +120,9 @@ const semantic_search = (query, options = {}) => catch_errors('Semantic search f
97
120
  workspace_slugs: options.workspace_slug ? options.workspace_slug.split(',').map(s => s.trim()).filter(Boolean) : null,
98
121
  scope: options.workspace_slug ? null : (options.scope || null),
99
122
  }
100
- // Always attempt auth for smart search if the user is logged in, the API
101
- // returns public + accessible private skills. If not logged in, the client
102
- // silently skips the auth header and the API defaults to public-only.
103
- const [errors, data] = await client.post('/repos:semantic-search', body, { auth: true })
123
+ const [errors, data] = await client.post('/repos:search', body, { auth: true })
104
124
  if (errors) throw errors[errors.length - 1]
105
125
  return data
106
126
  })
107
127
 
108
- module.exports = { search, semantic_search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates, del_repo, patch_repo, compare, get_blob, get_tree }
128
+ module.exports = { search, dispatch_search, semantic_search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates, del_repo, patch_repo, compare, get_blob, get_tree }
@@ -8,10 +8,10 @@ const { load_token } = require('../auth/token_store')
8
8
 
9
9
  const HELP_TEXT = `Usage: happyskills search [query] [options]
10
10
 
11
- Search the skill registry.
12
-
13
- When a query is provided, smart search (semantic + quality ranking) is used by
14
- default. Use --exact to fall back to keyword-only matching.
11
+ Search the skill registry. The server picks the right strategy based on the
12
+ query shape — natural-language goes to semantic, a single dashed word goes to
13
+ fuzzy slug, and "workspace/skill" form goes to fuzzy scoped (both parts are
14
+ typo-tolerant). Use --exact to force keyword-only FTS instead.
15
15
 
16
16
  Arguments:
17
17
  query Search term (optional with --mine, --personal, or --workspace)
@@ -22,7 +22,7 @@ Options:
22
22
  --personal Search only your personal workspace
23
23
  --tags <tags> Filter by tags (comma-separated)
24
24
  --type <type> Filter by type (skill, kit)
25
- --exact Use keyword-only matching instead of smart search
25
+ --exact Force keyword-only FTS matching (skip smart routing)
26
26
  --limit <n> Max results (required, 1-50)
27
27
  --min-quality <n> Minimum quality score 0-100
28
28
  --json Output as JSON
@@ -30,14 +30,13 @@ Options:
30
30
  Aliases: s
31
31
 
32
32
  Examples:
33
- happyskills search deploy --limit 10
34
- happyskills search "REST API with Node.js" --limit 10
33
+ happyskills search "deploy infra to AWS" --limit 10 # → semantic
34
+ happyskills search deploy-aws --limit 10 # fuzzy slug (typo-tolerant)
35
+ happyskills search letta-ai/remotion --limit 5 # → fuzzy scoped
35
36
  happyskills search --mine --limit 20
36
37
  happyskills search deploy --workspace acme --limit 50
37
38
  happyskills search --type kit --limit 10
38
- happyskills s --personal --json --limit 20
39
- happyskills search "deploy to AWS" --limit 5
40
- happyskills search deploy --exact --limit 10`
39
+ happyskills search deploy --exact --limit 10 # → keyword FTS only`
41
40
 
42
41
  const QUALITY_TIERS = [
43
42
  { min: 80, label: 'High quality', color: cyan },
@@ -119,10 +118,15 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
119
118
  const min_quality = args.flags['min-quality'] != null ? parseInt(args.flags['min-quality']) : null
120
119
 
121
120
  const search_opts = { ...options, limit: capped_limit }
122
- const [errors, results] = await repos_api.semantic_search(query, search_opts)
123
- if (errors) throw e('Semantic search failed', errors)
121
+ const [errors, response] = await repos_api.dispatch_search(query, search_opts)
122
+ if (errors) throw e('Search failed', errors)
124
123
 
125
- let items = Array.isArray(results) ? results : (results?.repos || results?.items || [])
124
+ // Response shape: { data: [...], mode, workspace_match, match_notice }
125
+ const items_raw = Array.isArray(response) ? response : (response?.data || response?.repos || response?.items || [])
126
+ let items = items_raw
127
+ const mode = response?.mode || null
128
+ const workspace_match = response?.workspace_match
129
+ const server_match_notice = response?.match_notice || null
126
130
 
127
131
  // Client-side quality filter (only when explicitly requested)
128
132
  if (min_quality != null) {
@@ -133,21 +137,26 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
133
137
 
134
138
  if (items.length === 0) {
135
139
  if (args.flags.json) {
136
- print_json({ data: { query, mode: 'smart', results: [], count: 0 } })
140
+ const data = { query, mode, results: [], count: 0 }
141
+ if (workspace_match !== undefined) data.workspace_match = workspace_match
142
+ if (server_match_notice) data.match_notice = server_match_notice
143
+ print_json({ data })
137
144
  return
138
145
  }
139
- const msg = `No skills found for "${query}".`
140
- print_info(msg)
141
- print_hint('Try broader terms or remove filters.')
146
+ if (mode === 'fuzzy_scoped' && workspace_match === null) {
147
+ const ws_part = query.includes('/') ? query.split('/')[0] : query
148
+ print_info(`No workspace matched "${ws_part}".`)
149
+ print_hint('Check spelling, or remove the workspace prefix to search globally.')
150
+ } else {
151
+ print_info(`No skills found for "${query}".`)
152
+ print_hint('Try broader terms or remove filters.')
153
+ }
142
154
  return
143
155
  }
144
156
 
145
- const has_strong_or_good = items.some(item =>
146
- item.match_quality === 'strong' || item.match_quality === 'good'
147
- )
148
- const match_notice = !has_strong_or_good && items.length > 0
149
- ? 'No strong matches found. The results below are the closest available but may not match your intent.'
150
- : null
157
+ // Trust the server's notice — only emitted for semantic mode where
158
+ // intent-matching is the relevant concept. Fuzzy modes return null.
159
+ const match_notice = server_match_notice
151
160
 
152
161
  if (args.flags.json) {
153
162
  const mapped = items.map(item => ({
@@ -158,14 +167,15 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
158
167
  similarity: member.similarity != null ? member.similarity : null,
159
168
  })),
160
169
  }))
161
- const data = { query, mode: 'smart', results: mapped, count: mapped.length }
170
+ const data = { query, mode, results: mapped, count: mapped.length }
171
+ if (workspace_match !== undefined) data.workspace_match = workspace_match
162
172
  if (match_notice) data.match_notice = match_notice
163
173
  print_json({ data })
164
174
  return
165
175
  }
166
176
 
167
- // Human-readable smart output
168
- console.log(`\n${bold(`Skills for: "${query}"`)}\n`)
177
+ // Human-readable output
178
+ console.log(`\n${bold(`Skills for: "${query}"`)}${mode ? dim(` [${mode}]`) : ''}\n`)
169
179
  if (match_notice) {
170
180
  console.log(` ${yellow(match_notice)}\n`)
171
181
  }
@@ -56,14 +56,14 @@ const run = (args) => catch_errors('Status failed', async () => {
56
56
  const all_skills = get_all_locked_skills(lock_data)
57
57
  const entries = target_skill
58
58
  ? [[target_skill, all_skills[target_skill]]]
59
- : Object.entries(all_skills).filter(([, data]) => data?.requested_by?.includes('__root__'))
59
+ : Object.entries(all_skills).filter(([, data]) => data !== null)
60
60
 
61
61
  if (entries.length === 0) {
62
62
  if (args.flags.json) {
63
63
  print_json({ data: { results: [] } })
64
64
  return
65
65
  }
66
- print_info('No root-level skills found.')
66
+ print_info('No installed skills found.')
67
67
  return
68
68
  }
69
69