happyskills 1.3.0 → 1.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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/api/repos.js +16 -1
- package/src/api/repos.test.js +48 -0
- package/src/commands/search.js +21 -8
- package/src/commands/search.test.js +60 -1
- package/src/commands/star.js +82 -0
- package/src/commands/unstar.js +81 -0
- package/src/constants.js +2 -0
- package/src/index.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.0] - 2026-06-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add `star <owner/name>` and `unstar <owner/name>` commands to curate your favorites. Both require auth and are idempotent (starring an already-starred skill, or unstarring one you haven't starred, succeeds without error).
|
|
15
|
+
- Add `--favorites` flag to `search` — query only the skills you've starred (`scope=favorites`). Routes through the smart dispatcher; a bare `search --favorites` (no query) browses all your starred skills. Requires auth; cannot be combined with `--exact`.
|
|
16
|
+
|
|
10
17
|
## [1.3.0] - 2026-06-03
|
|
11
18
|
|
|
12
19
|
### Added
|
package/package.json
CHANGED
package/src/api/repos.js
CHANGED
|
@@ -105,6 +105,21 @@ const patch_repo = (owner, name, fields) => catch_errors(`Failed to update ${own
|
|
|
105
105
|
return data
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
// Star / unstar a repo. Both endpoints are idempotent server-side (starring an
|
|
109
|
+
// already-starred repo, or unstarring an unstarred one, succeeds). The user's
|
|
110
|
+
// own stars power the `favorites` search scope.
|
|
111
|
+
const star = (owner, name) => catch_errors(`Failed to star ${owner}/${name}`, async () => {
|
|
112
|
+
const [errors, data] = await client.post(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/star`)
|
|
113
|
+
if (errors) throw errors[errors.length - 1]
|
|
114
|
+
return data
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const unstar = (owner, name) => catch_errors(`Failed to unstar ${owner}/${name}`, async () => {
|
|
118
|
+
const [errors, data] = await client.del(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/star`)
|
|
119
|
+
if (errors) throw errors[errors.length - 1]
|
|
120
|
+
return data
|
|
121
|
+
})
|
|
122
|
+
|
|
108
123
|
const compare = (owner, repo, base_commit) => catch_errors(`Compare ${owner}/${repo} failed`, async () => {
|
|
109
124
|
const [errors, data] = await client.post(`/repos/${owner}/${repo}/compare`, { base_commit })
|
|
110
125
|
if (errors) throw errors[errors.length - 1]
|
|
@@ -195,4 +210,4 @@ const semantic_search = (query, options = {}) => catch_errors('Semantic search f
|
|
|
195
210
|
return data
|
|
196
211
|
})
|
|
197
212
|
|
|
198
|
-
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 }
|
|
213
|
+
module.exports = { search, dispatch_search, semantic_search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates, del_repo, patch_repo, star, unstar, compare, get_blob, get_tree }
|
package/src/api/repos.test.js
CHANGED
|
@@ -169,3 +169,51 @@ describe('repos.get_blob — content integrity verification', () => {
|
|
|
169
169
|
assert.strictEqual(data.size, content.length)
|
|
170
170
|
})
|
|
171
171
|
})
|
|
172
|
+
|
|
173
|
+
// Capturing client stub: records (method, path) for each call so star/unstar
|
|
174
|
+
// path construction can be asserted. Mirrors stub_client above.
|
|
175
|
+
const stub_client_capture = () => {
|
|
176
|
+
const calls = []
|
|
177
|
+
const record = (method) => async (path) => { calls.push([method, path]); return [null, { message: 'ok' }] }
|
|
178
|
+
const client_path = require.resolve('./client')
|
|
179
|
+
require.cache[client_path] = {
|
|
180
|
+
id: client_path,
|
|
181
|
+
filename: client_path,
|
|
182
|
+
loaded: true,
|
|
183
|
+
exports: {
|
|
184
|
+
get: record('get'),
|
|
185
|
+
post: record('post'),
|
|
186
|
+
put: record('put'),
|
|
187
|
+
patch: record('patch'),
|
|
188
|
+
del: record('del'),
|
|
189
|
+
request: record('request'),
|
|
190
|
+
get_base_url: () => 'http://test'
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
delete require.cache[require.resolve('./repos')]
|
|
194
|
+
return { repos: require('./repos'), calls }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
describe('repos.star / repos.unstar — endpoint + path', () => {
|
|
198
|
+
afterEach(restore)
|
|
199
|
+
|
|
200
|
+
it('star() POSTs to /repos/<owner>/<name>/star', async () => {
|
|
201
|
+
const { repos, calls } = stub_client_capture()
|
|
202
|
+
const [err] = await repos.star('acme', 'deploy-aws')
|
|
203
|
+
assert.strictEqual(err, null)
|
|
204
|
+
assert.deepStrictEqual(calls[0], ['post', '/repos/acme/deploy-aws/star'])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('unstar() DELETEs /repos/<owner>/<name>/star', async () => {
|
|
208
|
+
const { repos, calls } = stub_client_capture()
|
|
209
|
+
const [err] = await repos.unstar('acme', 'deploy-aws')
|
|
210
|
+
assert.strictEqual(err, null)
|
|
211
|
+
assert.deepStrictEqual(calls[0], ['del', '/repos/acme/deploy-aws/star'])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('URL-encodes owner and name in the star path', async () => {
|
|
215
|
+
const { repos, calls } = stub_client_capture()
|
|
216
|
+
await repos.star('acme corp', 'deploy/aws')
|
|
217
|
+
assert.strictEqual(calls[0][1], `/repos/${encodeURIComponent('acme corp')}/${encodeURIComponent('deploy/aws')}/star`)
|
|
218
|
+
})
|
|
219
|
+
})
|
package/src/commands/search.js
CHANGED
|
@@ -16,12 +16,13 @@ fuzzy slug, and "workspace/skill" form goes to fuzzy scoped (both parts are
|
|
|
16
16
|
typo-tolerant). Use --exact to force keyword-only FTS instead.
|
|
17
17
|
|
|
18
18
|
Arguments:
|
|
19
|
-
query Search term (optional with --mine, --personal, or --
|
|
19
|
+
query Search term (optional with --mine, --personal, --workspace, or --favorites)
|
|
20
20
|
|
|
21
21
|
Options:
|
|
22
22
|
-w, --workspace <slug> Search within specific workspace(s) (comma-separated)
|
|
23
23
|
--mine Search across all your workspaces
|
|
24
24
|
--personal Search only your personal workspace
|
|
25
|
+
--favorites Search only skills you've starred (requires auth)
|
|
25
26
|
--tags <tags> Filter by tags (comma-separated)
|
|
26
27
|
--type <type> Filter by type (skill, kit)
|
|
27
28
|
--exact Force keyword-only FTS matching (skip smart routing)
|
|
@@ -42,6 +43,8 @@ Examples:
|
|
|
42
43
|
happyskills search deploy-aws --limit 10 # → fuzzy slug (typo-tolerant)
|
|
43
44
|
happyskills search letta-ai/remotion --limit 5 # → fuzzy scoped
|
|
44
45
|
happyskills search --mine --limit 20
|
|
46
|
+
happyskills search --favorites --limit 20 # → your starred skills
|
|
47
|
+
happyskills search deploy --favorites --limit 10 # → favorites matching "deploy"
|
|
45
48
|
happyskills search deploy --workspace acme --limit 50
|
|
46
49
|
happyskills search --type kit --limit 10
|
|
47
50
|
happyskills search deploy --exact --limit 10 # → keyword FTS only`
|
|
@@ -380,15 +383,19 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
380
383
|
}
|
|
381
384
|
|
|
382
385
|
const query = args._.join(' ') || null
|
|
383
|
-
const { mine, personal, workspace, tags, type } = args.flags
|
|
386
|
+
const { mine, personal, workspace, tags, type, favorites } = args.flags
|
|
384
387
|
const is_exact = !!args.flags.exact
|
|
385
388
|
const with_rerank = !!args.flags['with-rerank']
|
|
386
|
-
// --smart / -S accepted for backward compat (now the default when a query is provided)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
// --smart / -S accepted for backward compat (now the default when a query is provided).
|
|
390
|
+
// --favorites also forces the smart (POST /repos:search) path even with no query:
|
|
391
|
+
// favorites is a dispatcher-only scope, so it must never fall through to the
|
|
392
|
+
// legacy keyword GET, which doesn't understand it. A bare `--favorites` browses
|
|
393
|
+
// the user's starred skills (list mode).
|
|
394
|
+
const use_smart = (query || favorites) && !is_exact
|
|
395
|
+
const has_scope_flag = mine || personal || workspace || favorites
|
|
389
396
|
|
|
390
397
|
if (!query && !has_scope_flag && !tags && !type) {
|
|
391
|
-
throw new UsageError('Please provide a search query or use --mine, --personal, or --
|
|
398
|
+
throw new UsageError('Please provide a search query or use --mine, --personal, --workspace, or --favorites.')
|
|
392
399
|
}
|
|
393
400
|
|
|
394
401
|
if (!args.flags.limit) {
|
|
@@ -405,9 +412,15 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
405
412
|
throw new UsageError('--with-rerank cannot be combined with --exact. The rerank protocol applies only to semantic-mode dispatches.')
|
|
406
413
|
}
|
|
407
414
|
|
|
408
|
-
|
|
415
|
+
// Favorites is a dispatcher-only scope (POST /repos:search); --exact forces the
|
|
416
|
+
// legacy keyword endpoint, which doesn't support it. Refuse the combination.
|
|
417
|
+
if (favorites && is_exact) {
|
|
418
|
+
throw new UsageError('--favorites cannot be combined with --exact. Favorites search uses the smart dispatcher, not keyword-only FTS.')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const scope = favorites ? 'favorites' : mine ? 'mine' : personal ? 'personal' : undefined
|
|
409
422
|
|
|
410
|
-
if (mine || personal) {
|
|
423
|
+
if (mine || personal || favorites) {
|
|
411
424
|
const [, token_data] = await load_token()
|
|
412
425
|
if (!token_data) {
|
|
413
426
|
throw new AuthError('Authentication required. Run `happyskills login` first.')
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// short names (`clarify`, `present_to_user` alone) no longer apply where the
|
|
10
10
|
// new enum supersedes them.
|
|
11
11
|
|
|
12
|
-
const { describe, it } = require('node:test')
|
|
12
|
+
const { describe, it, afterEach } = require('node:test')
|
|
13
13
|
const assert = require('node:assert/strict')
|
|
14
14
|
const { build_search_next_step } = require('./search')
|
|
15
15
|
const {
|
|
@@ -146,3 +146,62 @@ describe('build_search_next_step — envelope shape', () => {
|
|
|
146
146
|
assert.strictEqual(opts[3].refined_query_hint, null)
|
|
147
147
|
})
|
|
148
148
|
})
|
|
149
|
+
|
|
150
|
+
// --- search --favorites wiring (spec 260601-02 § 4.3) ---------------------
|
|
151
|
+
// Stub the API client + token store so run() exercises the flag-routing logic
|
|
152
|
+
// without a network call. favorites is a dispatcher-only scope, so the key
|
|
153
|
+
// assertions are: it sets scope='favorites' on dispatch_search (POST) and never
|
|
154
|
+
// falls through to the keyword GET, and a bare --favorites (no query) is allowed.
|
|
155
|
+
|
|
156
|
+
const stub_search_deps = (capture) => {
|
|
157
|
+
const repos_path = require.resolve('../api/repos')
|
|
158
|
+
require.cache[repos_path] = {
|
|
159
|
+
id: repos_path, filename: repos_path, loaded: true,
|
|
160
|
+
exports: {
|
|
161
|
+
dispatch_search: async (query, opts) => {
|
|
162
|
+
capture.dispatch = { query, opts }
|
|
163
|
+
return [null, { data: [], mode: 'list', match_notice: null }]
|
|
164
|
+
},
|
|
165
|
+
search: async (query, opts) => { capture.keyword = { query, opts }; return [null, []] },
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
const token_path = require.resolve('../auth/token_store')
|
|
169
|
+
require.cache[token_path] = {
|
|
170
|
+
id: token_path, filename: token_path, loaded: true,
|
|
171
|
+
exports: {
|
|
172
|
+
load_token: async () => [null, { token: 'test-token' }],
|
|
173
|
+
require_token: async () => ({ token: 'test-token' }),
|
|
174
|
+
save_token: async () => {}, clear_token: async () => {},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
delete require.cache[require.resolve('./search')]
|
|
178
|
+
return require('./search')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const restore_search_deps = () => {
|
|
182
|
+
delete require.cache[require.resolve('../api/repos')]
|
|
183
|
+
delete require.cache[require.resolve('../auth/token_store')]
|
|
184
|
+
delete require.cache[require.resolve('./search')]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
describe('search --favorites — scope wiring', () => {
|
|
188
|
+
afterEach(restore_search_deps)
|
|
189
|
+
|
|
190
|
+
it('bare --favorites (no query) is allowed and dispatches scope=favorites via POST, not keyword GET', async () => {
|
|
191
|
+
const capture = {}
|
|
192
|
+
const search = stub_search_deps(capture)
|
|
193
|
+
await search.run({ _: [], flags: { favorites: true, limit: '20', json: true } })
|
|
194
|
+
assert.ok(capture.dispatch, 'dispatch_search (POST) must be called for favorites')
|
|
195
|
+
assert.strictEqual(capture.keyword, undefined, 'keyword GET must NOT be used for favorites')
|
|
196
|
+
assert.strictEqual(capture.dispatch.opts.scope, 'favorites')
|
|
197
|
+
assert.strictEqual(capture.dispatch.query, null, 'bare --favorites sends a null query (list mode)')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('--favorites with a query keeps scope=favorites and forwards the query', async () => {
|
|
201
|
+
const capture = {}
|
|
202
|
+
const search = stub_search_deps(capture)
|
|
203
|
+
await search.run({ _: ['deploy'], flags: { favorites: true, limit: '10', json: true } })
|
|
204
|
+
assert.strictEqual(capture.dispatch.opts.scope, 'favorites')
|
|
205
|
+
assert.strictEqual(capture.dispatch.query, 'deploy')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const { star } = require('../api/repos')
|
|
3
|
+
const { require_token } = require('../auth/token_store')
|
|
4
|
+
const { print_help, print_json, print_success } = require('../ui/output')
|
|
5
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
6
|
+
const { EXIT_CODES } = require('../constants')
|
|
7
|
+
|
|
8
|
+
const HELP_TEXT = `Usage: happyskills star <owner/name> [options]
|
|
9
|
+
|
|
10
|
+
Star a skill. Your starred skills are your favorites — search them with
|
|
11
|
+
\`happyskills search --favorites\`. Idempotent: starring an already-starred
|
|
12
|
+
skill succeeds without error.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
owner/name Skill to star (e.g., acme/deploy-aws)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--json Output as JSON
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
happyskills star acme/deploy-aws
|
|
22
|
+
happyskills star acme/deploy-aws --json`
|
|
23
|
+
|
|
24
|
+
const run = (args) => catch_errors('Star failed', async () => {
|
|
25
|
+
if (args.flags._show_help) {
|
|
26
|
+
print_help(HELP_TEXT)
|
|
27
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const skill = args._[0]
|
|
31
|
+
if (!skill) {
|
|
32
|
+
throw new UsageError('Please specify a skill to star (e.g., happyskills star acme/deploy-aws).')
|
|
33
|
+
}
|
|
34
|
+
if (!skill.includes('/')) {
|
|
35
|
+
throw new UsageError('Skill must be in owner/name format (e.g., acme/deploy-aws).')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await require_token()
|
|
39
|
+
|
|
40
|
+
const [owner, name] = skill.split('/')
|
|
41
|
+
const [errors, result] = await star(owner, name)
|
|
42
|
+
if (errors) throw errors[errors.length - 1]
|
|
43
|
+
|
|
44
|
+
if (args.flags.json) {
|
|
45
|
+
print_json({ data: { starred: true, skill, ...(result || {}) } })
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
print_success(`Starred "${skill}". Browse your favorites with: happyskills search --favorites`)
|
|
50
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
51
|
+
|
|
52
|
+
const schema = {
|
|
53
|
+
name: 'star',
|
|
54
|
+
audience: 'consumer',
|
|
55
|
+
purpose: 'Star a skill. Starred skills power the favorites search scope. Idempotent.',
|
|
56
|
+
mutation: true,
|
|
57
|
+
interactive_in_text_mode: false,
|
|
58
|
+
input: {
|
|
59
|
+
positional: [
|
|
60
|
+
{ name: 'skill', required: true, type: 'string', pattern: '<owner>/<name>' },
|
|
61
|
+
],
|
|
62
|
+
flags: [
|
|
63
|
+
{ name: 'json', type: 'boolean', default: false },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
output: {
|
|
67
|
+
data_shape: {
|
|
68
|
+
starred: 'boolean',
|
|
69
|
+
skill: 'string',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
errors: [
|
|
73
|
+
{ code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
|
|
74
|
+
{ code: 'AUTH_REQUIRED', next_step: { kind: 'recovery', action: 'login' } },
|
|
75
|
+
],
|
|
76
|
+
examples: [
|
|
77
|
+
'happyskills star acme/deploy-aws',
|
|
78
|
+
'happyskills star acme/deploy-aws --json',
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { run, schema }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const { unstar } = require('../api/repos')
|
|
3
|
+
const { require_token } = require('../auth/token_store')
|
|
4
|
+
const { print_help, print_json, print_success } = require('../ui/output')
|
|
5
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
6
|
+
const { EXIT_CODES } = require('../constants')
|
|
7
|
+
|
|
8
|
+
const HELP_TEXT = `Usage: happyskills unstar <owner/name> [options]
|
|
9
|
+
|
|
10
|
+
Remove a skill from your favorites. Idempotent: unstarring a skill you have
|
|
11
|
+
not starred succeeds without error.
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
owner/name Skill to unstar (e.g., acme/deploy-aws)
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--json Output as JSON
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
happyskills unstar acme/deploy-aws
|
|
21
|
+
happyskills unstar acme/deploy-aws --json`
|
|
22
|
+
|
|
23
|
+
const run = (args) => catch_errors('Unstar failed', async () => {
|
|
24
|
+
if (args.flags._show_help) {
|
|
25
|
+
print_help(HELP_TEXT)
|
|
26
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const skill = args._[0]
|
|
30
|
+
if (!skill) {
|
|
31
|
+
throw new UsageError('Please specify a skill to unstar (e.g., happyskills unstar acme/deploy-aws).')
|
|
32
|
+
}
|
|
33
|
+
if (!skill.includes('/')) {
|
|
34
|
+
throw new UsageError('Skill must be in owner/name format (e.g., acme/deploy-aws).')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await require_token()
|
|
38
|
+
|
|
39
|
+
const [owner, name] = skill.split('/')
|
|
40
|
+
const [errors, result] = await unstar(owner, name)
|
|
41
|
+
if (errors) throw errors[errors.length - 1]
|
|
42
|
+
|
|
43
|
+
if (args.flags.json) {
|
|
44
|
+
print_json({ data: { starred: false, skill, ...(result || {}) } })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
print_success(`Unstarred "${skill}".`)
|
|
49
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
50
|
+
|
|
51
|
+
const schema = {
|
|
52
|
+
name: 'unstar',
|
|
53
|
+
audience: 'consumer',
|
|
54
|
+
purpose: 'Remove a skill from your favorites (unstar). Idempotent.',
|
|
55
|
+
mutation: true,
|
|
56
|
+
interactive_in_text_mode: false,
|
|
57
|
+
input: {
|
|
58
|
+
positional: [
|
|
59
|
+
{ name: 'skill', required: true, type: 'string', pattern: '<owner>/<name>' },
|
|
60
|
+
],
|
|
61
|
+
flags: [
|
|
62
|
+
{ name: 'json', type: 'boolean', default: false },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
output: {
|
|
66
|
+
data_shape: {
|
|
67
|
+
starred: 'boolean',
|
|
68
|
+
skill: 'string',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
errors: [
|
|
72
|
+
{ code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
|
|
73
|
+
{ code: 'AUTH_REQUIRED', next_step: { kind: 'recovery', action: 'login' } },
|
|
74
|
+
],
|
|
75
|
+
examples: [
|
|
76
|
+
'happyskills unstar acme/deploy-aws',
|
|
77
|
+
'happyskills unstar acme/deploy-aws --json',
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { run, schema }
|
package/src/constants.js
CHANGED
package/src/index.js
CHANGED
|
@@ -103,6 +103,8 @@ Commands:
|
|
|
103
103
|
visibility <owner/skill> Check or set visibility (alias: vis)
|
|
104
104
|
list List installed skills (alias: ls)
|
|
105
105
|
search <query> Search the registry (alias: s)
|
|
106
|
+
star <owner/skill> Star a skill (add to your favorites)
|
|
107
|
+
unstar <owner/skill> Remove a skill from your favorites
|
|
106
108
|
postlex Apply deterministic rerank finalization (agent-only; see happyskills-help skill)
|
|
107
109
|
versions <owner/skill> List all published versions of a skill
|
|
108
110
|
changelog <owner/skill> Print a skill's CHANGELOG.md
|