happyskills 0.42.0 → 0.43.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/api/repos.js +25 -5
- package/src/commands/search.js +36 -26
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.43.0] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- 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.
|
|
14
|
+
- 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`.
|
|
15
|
+
- 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*.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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`.
|
|
21
|
+
|
|
10
22
|
## [0.42.0] - 2026-05-05
|
|
11
23
|
|
|
12
24
|
### Added
|
package/package.json
CHANGED
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
|
-
|
|
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 }
|
package/src/commands/search.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
123
|
-
if (errors) throw e('
|
|
121
|
+
const [errors, response] = await repos_api.dispatch_search(query, search_opts)
|
|
122
|
+
if (errors) throw e('Search failed', errors)
|
|
124
123
|
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
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
|
|
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
|
}
|