happyskills 1.6.0 → 1.7.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 CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.0] - 2026-06-04
11
+
12
+ ### Added
13
+
14
+ - `search` now exposes `created_at` / `updated_at` on every result and accepts `--sort <relevance|recent>`. `--sort recent` orders results newest-created first — pair it with `--mine` to list your latest-created skills (`happyskills search --mine --sort recent --limit 100`); human output shows a `Created` column. Sorts the fetched page, so pass a sufficient `--limit`. No API change required.
15
+
10
16
  ## [1.6.0] - 2026-06-04
11
17
 
12
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
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)",
@@ -34,6 +34,8 @@ Options:
34
34
  Carried across re-searches in the clarification flow.
35
35
  --limit <n> Max results (required, 1-50)
36
36
  --min-quality <n> Minimum quality score 0-100
37
+ --sort <order> relevance (default) | recent (newest-created first).
38
+ Sorts the fetched page — pass a sufficient --limit.
37
39
  --json Output as JSON
38
40
 
39
41
  Aliases: s
@@ -43,6 +45,7 @@ Examples:
43
45
  happyskills search deploy-aws --limit 10 # → fuzzy slug (typo-tolerant)
44
46
  happyskills search letta-ai/remotion --limit 5 # → fuzzy scoped
45
47
  happyskills search --mine --limit 20
48
+ happyskills search --mine --sort recent --limit 100 # → your latest-created skills
46
49
  happyskills search --favorites --limit 20 # → your starred skills
47
50
  happyskills search deploy --favorites --limit 10 # → favorites matching "deploy"
48
51
  happyskills search deploy --workspace acme --limit 50
@@ -226,6 +229,10 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
226
229
  )
227
230
  }
228
231
 
232
+ // --sort recent: re-order newest-created first (the smart `data` shape
233
+ // already carries created_at). Sorts the fetched page.
234
+ if (args.flags.sort === 'recent') items = sort_by_created_desc(items)
235
+
229
236
  const next_step = build_search_next_step(response, query, { with_rerank, clarification_turns_used })
230
237
 
231
238
  // Telemetry beacons (fire-and-forget). Spec § 5.1 + § 5.3.
@@ -329,12 +336,24 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
329
336
  console.log(`\n${gray(`Showing ${items.length} result${items.length === 1 ? '' : 's'}. Install with: happyskills install <owner>/<name>`)}\n`)
330
337
  })
331
338
 
339
+ // Sort a result set newest-created first. Items without a created_at sink to the
340
+ // bottom. Pure — returns a new array. Used by --sort recent.
341
+ const sort_by_created_desc = (items) => items.slice().sort((a, b) => {
342
+ const da = a.created_at ? new Date(a.created_at).getTime() : 0
343
+ const db = b.created_at ? new Date(b.created_at).getTime() : 0
344
+ return db - da
345
+ })
346
+
332
347
  const run_keyword_search = (args, query, options, { is_exact } = {}) => catch_errors('Keyword search failed', async () => {
333
348
  const [errors, results] = await repos_api.search(query, options)
334
349
  if (errors) throw e('Search failed', errors)
335
350
 
336
- const items = Array.isArray(results) ? results : (results?.repos || results?.items || [])
351
+ let items = Array.isArray(results) ? results : (results?.repos || results?.items || [])
337
352
  const effective_scope = options.scope || 'all'
353
+ // --sort recent: order by creation date (newest first). Sorts the fetched
354
+ // page, so pass a --limit large enough to cover the set you care about.
355
+ const sort_recent = args.flags.sort === 'recent'
356
+ if (sort_recent) items = sort_by_created_desc(items)
338
357
 
339
358
  if (items.length === 0) {
340
359
  if (args.flags.json) {
@@ -354,26 +373,31 @@ const run_keyword_search = (args, query, options, { is_exact } = {}) => catch_er
354
373
  type: item.type || 'skill',
355
374
  description: item.description || '',
356
375
  version: item.latest_version || item.version || '-',
357
- visibility: item.visibility || 'public'
376
+ visibility: item.visibility || 'public',
377
+ created_at: item.created_at || null,
378
+ updated_at: item.updated_at || null
358
379
  }))
359
380
  const data = { query, scope: effective_scope, results: mapped, count: mapped.length }
381
+ if (sort_recent) data.sort = 'recent'
360
382
  if (is_exact) data.hint = 'Smart search (default) uses semantic matching for better results. Remove --exact to use it.'
361
383
  print_json({ data })
362
384
  return
363
385
  }
364
386
 
387
+ // When sorting by creation date, swap the Description column for a Created
388
+ // column — that's the field the user is sorting on and wants to see.
389
+ const headers = sort_recent ? ['Skill', 'Created', 'Version'] : ['Skill', 'Description', 'Version']
365
390
  const rows = items.map(item => {
366
391
  const item_type = item.type || 'skill'
367
392
  const name = `${item.owner || item.workspace_slug}/${item.name}`
368
393
  const display_name = item_type === 'kit' ? `${name} [kit]` : name
369
- return [
370
- display_name,
371
- item.description || '',
372
- item.latest_version || item.version || '-'
373
- ]
394
+ const version = item.latest_version || item.version || '-'
395
+ return sort_recent
396
+ ? [display_name, item.created_at ? String(item.created_at).slice(0, 10) : '-', version]
397
+ : [display_name, item.description || '', version]
374
398
  })
375
399
 
376
- print_table(['Skill', 'Description', 'Version'], rows)
400
+ print_table(headers, rows)
377
401
  })
378
402
 
379
403
  const run = (args) => catch_errors('Search failed', async () => {
@@ -402,6 +426,10 @@ const run = (args) => catch_errors('Search failed', async () => {
402
426
  throw new UsageError('--limit is required. Specify the number of results you want (e.g. --limit 10).')
403
427
  }
404
428
 
429
+ if (args.flags.sort != null && !['relevance', 'recent'].includes(args.flags.sort)) {
430
+ throw new UsageError('--sort must be one of: relevance, recent.')
431
+ }
432
+
405
433
  if (type && !VALID_SKILL_TYPES.includes(type)) {
406
434
  throw new UsageError(`--type must be one of: ${VALID_SKILL_TYPES.join(', ')}`)
407
435
  }
@@ -467,6 +495,7 @@ const schema = {
467
495
  { name: 'clarification-turns-used', type: 'number', default: 0, description: 'Clarification budget already spent (0-2)' },
468
496
  { name: 'limit', type: 'number', required: true, description: 'Max results (1-50)' },
469
497
  { name: 'min-quality', type: 'number', default: undefined, description: 'Minimum quality score 0-100' },
498
+ { name: 'sort', type: 'string', default: 'relevance', description: 'relevance (default) | recent (newest-created first; sorts the fetched page)' },
470
499
  { name: 'json', type: 'boolean', default: false, description: 'Output as JSON' },
471
500
  ],
472
501
  },
@@ -490,7 +519,8 @@ const schema = {
490
519
  examples: [
491
520
  'happyskills search "deploy infra to AWS" --limit 10',
492
521
  'happyskills search "deploy infra to AWS" --with-rerank --json --limit 50',
522
+ 'happyskills search --mine --sort recent --limit 100',
493
523
  ],
494
524
  }
495
525
 
496
- module.exports = { run, build_search_next_step, schema }
526
+ module.exports = { run, build_search_next_step, sort_by_created_desc, schema }
@@ -11,13 +11,33 @@
11
11
 
12
12
  const { describe, it, afterEach } = require('node:test')
13
13
  const assert = require('node:assert/strict')
14
- const { build_search_next_step } = require('./search')
14
+ const { build_search_next_step, sort_by_created_desc } = require('./search')
15
15
  const {
16
16
  NEXT_STEP_KINDS,
17
17
  NEXT_STEP_ACTIONS,
18
18
  kind_for_action,
19
19
  } = require('../constants/next_step_actions')
20
20
 
21
+ // --sort recent ordering (Phase B): newest created_at first, nulls last, pure.
22
+ describe('sort_by_created_desc — --sort recent', () => {
23
+ it('orders newest-created first and sinks items with no created_at', () => {
24
+ const out = sort_by_created_desc([
25
+ { name: 'a', created_at: '2026-05-01T00:00:00Z' },
26
+ { name: 'b', created_at: '2026-06-01T00:00:00Z' },
27
+ { name: 'c', created_at: null },
28
+ { name: 'd', created_at: '2026-05-20T00:00:00Z' },
29
+ ])
30
+ assert.deepEqual(out.map(x => x.name), ['b', 'd', 'a', 'c'])
31
+ })
32
+
33
+ it('is pure — does not mutate the input array', () => {
34
+ const input = [{ name: 'a', created_at: '2026-01-01T00:00:00Z' }, { name: 'b', created_at: '2026-02-01T00:00:00Z' }]
35
+ const before = input.map(x => x.name).join(',')
36
+ sort_by_created_desc(input)
37
+ assert.equal(input.map(x => x.name).join(','), before)
38
+ })
39
+ })
40
+
21
41
  const is_empty = (o) => o && typeof o === 'object' && !Array.isArray(o) && Object.keys(o).length === 0
22
42
 
23
43
  const assert_envelope_next_step = (ns, expected_action, label) => {