happyskills 1.5.0 → 1.6.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.6.0] - 2026-06-04
11
+
12
+ ### Added
13
+
14
+ - Extend `stats` (all additive): `--group-by skill` on `--scope my_activity` (rank which skills *you* installed); a new `installs` metric on `--scope my_skills_reach` that counts total installs of your authored skills (you + others; bot excluded), with `--exclude-self` to drop your own. Availability warnings are now softer — suppressed for `--period` presets, terser for explicit `--from`. Requires API v5.4.0+.
15
+
10
16
  ## [1.5.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.5.0",
3
+ "version": "1.6.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)",
@@ -15,10 +15,10 @@ const token_store = require('../auth/token_store')
15
15
 
16
16
  const METRICS_BY_SCOPE = {
17
17
  my_activity: ['installs', 'updates', 'searches', 'uninstalls'],
18
- my_skills_reach: ['installs_by_others', 'distinct_installers'],
18
+ my_skills_reach: ['installs', 'installs_by_others', 'distinct_installers'],
19
19
  }
20
20
  const GROUP_BY_BY_SCOPE = {
21
- my_activity: ['none', 'day', 'week', 'month', 'surface'],
21
+ my_activity: ['none', 'day', 'week', 'month', 'surface', 'skill'],
22
22
  my_skills_reach: ['none', 'day', 'week', 'month', 'skill'],
23
23
  }
24
24
  const VALID_PRESETS = ['7d', '30d', '90d', '6mo', '12mo']
@@ -35,21 +35,25 @@ Two scopes:
35
35
  Options:
36
36
  --scope <scope> my_activity | my_skills_reach (required)
37
37
  --metric <metric> my_activity: installs, updates, searches, uninstalls
38
- my_skills_reach: installs_by_others, distinct_installers
38
+ my_skills_reach: installs (everyone), installs_by_others,
39
+ distinct_installers
39
40
  --period <preset> ${VALID_PRESETS.join(' | ')}
40
41
  --from <ISO date> Explicit window start (use instead of --period)
41
42
  --to <ISO date> Explicit window end (default: now)
42
- --group-by <bucket> my_activity: none, day, week, month, surface
43
+ --group-by <bucket> my_activity: none, day, week, month, surface, skill
43
44
  my_skills_reach: none, day, week, month, skill
44
45
  (default: none)
45
46
  --skill <owner/skill> Limit my_skills_reach to one skill you authored
47
+ --exclude-self my_skills_reach 'installs' only: exclude your own installs
48
+ (installs counts you by default)
46
49
  --json Output as JSON envelope
47
50
 
48
51
  Examples:
49
52
  happyskills stats --scope my_activity --metric installs --period 30d
53
+ happyskills stats --scope my_activity --metric installs --period 30d --group-by skill
50
54
  happyskills stats --scope my_activity --metric searches --period 90d --group-by week --json
51
- happyskills stats --scope my_skills_reach --metric distinct_installers --period 12mo
52
- happyskills stats --scope my_skills_reach --metric installs_by_others --period 12mo --group-by skill
55
+ happyskills stats --scope my_skills_reach --metric installs --period 12mo --group-by skill
56
+ happyskills stats --scope my_skills_reach --metric installs --period 12mo --exclude-self
53
57
  happyskills stats --scope my_skills_reach --metric distinct_installers --period 12mo --skill acme/deploy-aws
54
58
  happyskills stats --scope my_activity --metric installs --from 2026-05-01 --to 2026-06-01`
55
59
 
@@ -98,6 +102,14 @@ const build_request = (flags) => {
98
102
  body.repo_slug = flags.skill
99
103
  }
100
104
 
105
+ // --exclude-self refines my_skills_reach `installs` (which counts you by
106
+ // default). Meaningless elsewhere — the server enforces this too.
107
+ if (flags['exclude-self']) {
108
+ if (scope !== 'my_skills_reach')
109
+ throw new UsageError('--exclude-self is only valid for --scope my_skills_reach.')
110
+ body.exclude_self = true
111
+ }
112
+
101
113
  return body
102
114
  }
103
115
 
@@ -161,14 +173,15 @@ const schema = {
161
173
  input: {
162
174
  positional: [],
163
175
  flags: [
164
- { name: 'scope', type: 'string', required: true, description: 'my_activity | my_skills_reach' },
165
- { name: 'metric', type: 'string', required: true, description: 'my_activity: installs|updates|searches|uninstalls; my_skills_reach: installs_by_others|distinct_installers' },
166
- { name: 'period', type: 'string', default: undefined, description: 'Window preset: 7d|30d|90d|6mo|12mo (use instead of --from/--to)' },
167
- { name: 'from', type: 'string', default: undefined, description: 'Explicit window start (ISO date)' },
168
- { name: 'to', type: 'string', default: undefined, description: 'Explicit window end (ISO date, default now)' },
169
- { name: 'group-by', type: 'string', default: 'none', description: 'my_activity: none|day|week|month|surface; my_skills_reach: none|day|week|month|skill' },
170
- { name: 'skill', type: 'string', default: undefined, description: 'my_skills_reach only: limit to one authored skill (owner/skill)' },
171
- { name: 'json', type: 'boolean', default: false, description: 'Output as JSON' },
176
+ { name: 'scope', type: 'string', required: true, description: 'my_activity | my_skills_reach' },
177
+ { name: 'metric', type: 'string', required: true, description: 'my_activity: installs|updates|searches|uninstalls; my_skills_reach: installs|installs_by_others|distinct_installers' },
178
+ { name: 'period', type: 'string', default: undefined, description: 'Window preset: 7d|30d|90d|6mo|12mo (use instead of --from/--to)' },
179
+ { name: 'from', type: 'string', default: undefined, description: 'Explicit window start (ISO date)' },
180
+ { name: 'to', type: 'string', default: undefined, description: 'Explicit window end (ISO date, default now)' },
181
+ { name: 'group-by', type: 'string', default: 'none', description: 'my_activity: none|day|week|month|surface|skill; my_skills_reach: none|day|week|month|skill' },
182
+ { name: 'skill', type: 'string', default: undefined, description: 'my_skills_reach only: limit to one authored skill (owner/skill)' },
183
+ { name: 'exclude-self', type: 'boolean', default: false, description: "my_skills_reach 'installs' only: exclude your own installs (counted by default)" },
184
+ { name: 'json', type: 'boolean', default: false, description: 'Output as JSON' },
172
185
  ],
173
186
  },
174
187
  output: {
@@ -190,9 +203,10 @@ const schema = {
190
203
  { code: 'NETWORK_ERROR', next_step: { kind: 'recovery', action: 'retry' } },
191
204
  ],
192
205
  examples: [
193
- 'happyskills stats --scope my_activity --metric installs --period 30d',
194
- 'happyskills stats --scope my_skills_reach --metric distinct_installers --period 12mo --group-by skill',
195
- 'happyskills stats --scope my_skills_reach --metric installs_by_others --period 12mo --skill acme/deploy-aws',
206
+ 'happyskills stats --scope my_activity --metric installs --period 30d --group-by skill',
207
+ 'happyskills stats --scope my_skills_reach --metric installs --period 12mo --group-by skill',
208
+ 'happyskills stats --scope my_skills_reach --metric installs --period 12mo --exclude-self',
209
+ 'happyskills stats --scope my_skills_reach --metric distinct_installers --period 12mo --skill acme/deploy-aws',
196
210
  ],
197
211
  }
198
212
 
@@ -59,6 +59,26 @@ describe('build_request — closed grammar', () => {
59
59
  it('rejects a --skill without owner/skill form', () => {
60
60
  assert.throws(() => build_request({ scope: 'my_skills_reach', metric: 'installs_by_others', period: '12mo', skill: 'deploy' }), /owner\/skill/)
61
61
  })
62
+
63
+ it('accepts group-by skill on my_activity (rank your own installs by skill)', () => {
64
+ const body = build_request({ scope: 'my_activity', metric: 'installs', period: '30d', 'group-by': 'skill' })
65
+ assert.equal(body.group_by, 'skill')
66
+ })
67
+
68
+ it('accepts the my_skills_reach `installs` metric', () => {
69
+ const body = build_request({ scope: 'my_skills_reach', metric: 'installs', period: '12mo' })
70
+ assert.equal(body.metric, 'installs')
71
+ assert.equal(body.exclude_self, undefined) // self included by default
72
+ })
73
+
74
+ it('maps --exclude-self to exclude_self for my_skills_reach', () => {
75
+ const body = build_request({ scope: 'my_skills_reach', metric: 'installs', period: '12mo', 'exclude-self': true })
76
+ assert.equal(body.exclude_self, true)
77
+ })
78
+
79
+ it('rejects --exclude-self on my_activity', () => {
80
+ assert.throws(() => build_request({ scope: 'my_activity', metric: 'installs', period: '30d', 'exclude-self': true }), /--exclude-self is only valid/)
81
+ })
62
82
  })
63
83
 
64
84
  describe('schema export', () => {